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、つまり何もしないという命令らしいですが、何のために入っているかよくわからず、、、パディングかな?