Hello World!を逆アセンブル①
こんな本を買ったので、最近はバイナリの勉強をしていたりします。

Hacking: 美しき策謀 第2版 ―脆弱性攻撃の理論と実際
- 作者: Jon Erickson,村上雅章
- 出版社/メーカー: オライリージャパン
- 発売日: 2011/10/22
- メディア: 単行本(ソフトカバー)
- 購入: 9人 クリック: 163回
- この商品を含むブログ (19件) を見る
というわけで(?)、もはや何番煎じかわからないですが、今日はHello Worldのアセンブリを見ていきます。 上記の本だと32bit版で解説されていますが、今回は64bitの環境で試したので、レジスタの名前が違ったりしました。
環境
Ubuntu 16.04を使っています。 gcc(コンパイラ)のバージョンは以下の通り。
$ gcc --version gcc (Ubuntu 7.3.0-16ubuntu3) 7.3.0 Copyright (C) 2017 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
プログラムの作成
どんなC言語の入門書でも必ず最初に出てくるような次のコードをhelloWorld.cとして保存。
#include <stdio.h> int main(){ printf("Hello World!\n"); return 0; }
コンパイルします。
$ gcc helloWorld.c -o helloWorld
出来上がったものを実行すると、標準出力に文字列が返ります。
$ ./helloWorld Hello World!
逆アセンブルしてみる
早速objdumpで逆アセンブルしてみます。
$ objdump -M intel -S helloWorld #中略 000000000000063a <main>: 63a: 55 push rbp 63b: 48 89 e5 mov rbp,rsp 63e: 48 8d 3d 9f 00 00 00 lea rdi,[rip+0x9f] # 6e4 <_IO_stdin_used+0x4> 645: e8 c6 fe ff ff call 510 <puts@plt> 64a: b8 00 00 00 00 mov eax,0x0 64f: 5d pop rbp 650: c3 ret 651: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0] 658: 00 00 00 65b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0] #中略
アセンブリはAT&T記法とIntel記法がありますが、後者の方が見やす気がするのでそちらで表示しています。
-Mオプションで指定します。
スタック領域の切り替え
最初の2行は関数が実行されるときに最初に実行される処理です。
main()関数を開始するためにスタック領域の切り替えを行います。
63a: 55 push rbp 63b: 48 89 e5 mov rbp,rsp
rbp(ベースポインタ)がスタックの最古(底)のアドレスで、rsp(スタックポインタ)が最新(一番上)のアドレスを指しています。
pushは指定したレジスタの値をスタックに積む(保存する)命令で、こうすることで現在(main()の呼び出し元)のスタックのアドレスをバックアップします。
バックアップしたアドレスはmain()関数が終了し、main()関数の呼び出し元の処理を再開するときに使用します。
バックアップできたら、movでrspの値をrbpへ書き込みます。
最新のスタックのアドレスがmain()のスタックのはじめ(底)のアドレスとして使用されるようになるわけです。
printf()の実行
続いてprintf()の呼び出しに関する部分です。
63e: 48 8d 3d 9f 00 00 00 lea rdi,[rip+0x9f] # 6e4 <_IO_stdin_used+0x4> 645: e8 c6 fe ff ff call 510 <puts@plt>
leaでrip(インストラクションポインタ)に0x9f足したアドレスをrdi(ディスティネーションインデックス)へ書き込んでいます。
詳しくは次回に回しますが、rip+0x9fは"Hello World!"の先頭アドレスで、rdiを使用することで引数としてprintf()へ渡しているようです。
続いてprintf()の実行。これは関数の先頭アドレスをcallするだけなので単純ですね。
戻り値を返す
ソースで言うとreturn 0の部分です。
64a: b8 00 00 00 00 mov eax,0x0
eax(アキュムレータ)は戻り値の格納に使用するレジスタで、このレジスタに0をmovで書き込んでいます。
スタックをもとに戻す
一通りの処理が終わったので、最初に切り替えたスタックをもとに戻します。
64f: 5d pop rbp
main()の開始時にスタックへ呼び出し元関数のスタックのベースアドレス(底のアドレス)を保存しました。
この処理ではそのアドレスをrbpへ戻しています。今回はスタックを使用していないので、ありがたみが感じられないですね。
呼び出し元へ戻る
main()関数の処理がすべて完了したので、呼び出し元へ処理を戻します。
650: c3 ret
何のためか不明な命令
最後の3行は何のためのものかよくわかりませんでした。
651: 66 2e 0f 1f 84 00 00 nop WORD PTR cs:[rax+rax*1+0x0] 658: 00 00 00 65b: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]
nopはNo Operation、つまり何もしないという命令らしいですが、何のために入っているかよくわからず、、、パディングかな?