アセンブラ短歌で遊ぼう!
こんにちは。弥生の狩野です。Misoca+弥生+ALTOA Advent Calendar 2018 - Qiita の15日目の記事です。
みなさん、アセンブラ短歌詠んでますか?
アセンブラ短歌とは
「アセンブラ短歌」は五・七・五・七・七の三十一バイト(みそひとバイト)から成る 機械語コードでプログラムを書いてみるという近未来の文化的趣味であり,近年, 国内のハッカー間で密かなブームが起きています.(アセンブラ短歌より)
風流ですね。
まだご存知ない方は、ぜひ、アセンブラ短歌 - 坂井弘亮やアセンブラ短歌をご覧ください! 楽しい世界が広がっています。
詠んでみる
アーキテクチャは、x86/Linux/32bitです。
53 0f 31 6a 04 5a 83 e0 01 74 10 90 68 a1 f9 d7 c4 89 e1 89 c3 c1 e0 02 cd 80 58 31 c0 5b c3
実行する
標準出力に☆彡が流れたり流れなかったりします。流れたら幸運ですね。
解説
上にあげた機械語コードを逆アセンブルしたものが以下のリストです。 このアセンブリをもとに、簡単に解説します。
000004ed <main>: 4ed: 53 push %ebx 4ee: 0f 31 rdtsc 4f0: 6a 04 push $0x4 4f2: 5a pop %edx 4f3: 83 e0 01 and $0x1,%eax 4f6: 74 10 je 508 <happyholidays> 4f8: 90 nop 4f9: 68 a1 f9 d7 c4 push $0xc4d7f9a1 4fe: 89 e1 mov %esp,%ecx 500: 89 c3 mov %eax,%ebx 502: c1 e0 02 shl $0x2,%eax 505: cd 80 int $0x80 507: 58 pop %eax 00000508 <happyholidays>: 508: 31 c0 xor %eax,%eax 50a: 5b pop %ebx 50b: c3 ret
関数を作る
実際には、関数の体を保っていなくても動いたりするのですが、せっかくなので関数のお約束を守りたいと思います。 関数である!と言うには、以下の3つのコードが必要です。
%ebxの退避と復帰
%ebxは関数から返るときに、値が変わっていないことが期待されるレジスタです。 なので、関数の体を保つために、%ebxを保存する必要があります。
まず、プログラムの最初で%ebxをスタックに積んでおきましょう。
push %ebx
そして、関数の最後でスタックから%ebxを復旧させます。
pop %ebx
%eaxを0にする
関数からの戻り値は%eaxに入っています。 なので、正常終了の場合は、%eaxに0を設定する必要があります。
xor %eax, %eax
xorは排他的論理和の命令です。 同じ値同士の排他的論理和は必ず0になるので、それを利用して2byteで0を%eaxに設定しています。
戻る
ret
呼び出し元に戻る命令です。
標準出力
関数の形が整ったら、☆彡を流すことを考えます。
アセンブラで標準出力を使うには、int $0x80を使います。 int $0x80はソフトウェア割込みを発生させる命令で、レジスタを適切に設定することで、様々なシステムコールを使うことができます。 今回は標準出力に☆彡を流したいので、以下のようにレジスタを設定します。
レジスタ | 値 | 意味 |
---|---|---|
%eax | 4 | システムコール番号 |
%ebx | 1 | 標準出力 |
%ecx | %esp (スタックポインタのアドレス) |
表示する文字列の先頭アドレス |
%edx | 4 | 出力byte数 |
この設定をしているのが以下のコードです。
%edxの設定
push $0x4 pop %edx
movではなくpushとpopを使ったのは、movで即値を設定すると5byte必要なのに比べて、pushが2byte、popが1byteの合計3byteで書けるからです。
%ecxの設定
push $0xc4d7f9a1 mov %esp, %ecx
$0xc4d7f9a1は、☆彡のEUCコードをリトルエンディアンにしたものです。 これをいったんスタックに積んで、そのアドレスを%ecxに設定しています。
%ebxの設定
mov %eax, %ebx
後述しますが、この処理に入る段階で、%eaxは必ず1になっているので%ebxには1が設定されます。 push+popは3byteでしたが、レジスタ同士のmovなら2byteで書けます。
%eaxの設定
shl $0x2, %eax
shlは算術シフトの命令です。 ここでは、左に2bit分シフトすることで1を4倍して、%eaxに4を設定しています。
ランダムっぽくする
厳密に乱数を発生させる必要はないので、ランダムっぽく見える数値を使います。
まず、rdtscでCPUクロックごとに加算されるタイムスタンプカウンタを取得します。 取得した値は%eaxに入ります。
rdtsc
次に、%eax(今はタイムスタンプカウンタの値が入っている)と1との論理和を取ります。
and $1, %eax
こうすることで、タイムスタンプカウンタの最下位ビットが0ならZF(ゼロフラグ)が1で、最下位ビットが1ならZF(ゼロフラグ)が0という状況になります。
そして、ZFが1の場合にジャンプし、ZFが0の場合はジャンプしないje命令を使って、分岐させます。 今回は、ZFが1の場合、つまり、タイムスタンプカウンタの最下位ビットが0の場合は、happyholidaysというラベルまでジャンプして☆彡は流れません。
je 508 <happyholidays> ~略~ happyholidays: ~略~
余談ですが、このje命令、機械語では74 10となっていて、0x10byteつまり17byte先にジャンプするようになっています。 なので、機械語ではラベルが書かれていなくてもジャンプ先がわかるのですね。
byte数調整
普通の人間はクロックを認識することはできないので、これでなんとなくランダムっぽい感じに☆彡を標準出力に流すことができるようになりました。 最後に、命令の順番を調整して、五・七・五・七・七の形にすればできあがりですが、ここで問題が…1byte足りない!
nop
空の命令です。何もしません。 nopを入れずに五・七・五・七・七にできたら教えてください!
ありがとうございました
ところで、12月22日(土)、23日(日)のSECCON 2018 Akihabaraでは、アセンブラ短歌六歌仙の一人である坂井弘亮さんによるバイナリかるたのワークショップもあります。
みなさんもアセンブラ短歌とバイナリかるたで、楽しいホリデーシーズンを過ごしましょう!
明日は@daisuke_kaihoさんによるCoderetreatとBDD、ATDDのお話です。楽しみですね。