なんと、アセンブラが「全く」読めない。基本情報技術者試験の午後は、当然「アセンブラ」を選択し、「はじめて読む Pentium マシン語 - あしのあしあと」でも遊び、他にも色々実験したのに(軽くショック)。
なので、初心にかえって、少しずつ、アセンブラを読んでいこうと思う。最初は、最もシンプルに、足し算をするだけの plus という関数で実験した。
int __cdecl plus(int a, int b) { return a + b; } int main(void) { return plus(0x07, 0x0A); }
逆アセンブルして出力されたコードが次。とりあえず、全部載せてみた。なお、環境は、Windows 10 上の、Visual Studio Community 2017。コンパイルオプションは、デフォルトのまま。関数の呼び出し規約も、デフォルト(のはず)の __cdecl。
_plus: 011C16A0 push ebp ; (01) 011C16A1 mov ebp,esp ; (02) 011C16A3 sub esp,0C0h ; (03) 011C16A9 push ebx ; (04) 011C16AA push esi ; (05) 011C16AB push edi ; (06) 011C16AC lea edi,[ebp+FFFFFF40h] ; (07) [ebp-0C0h] 011C16B2 mov ecx,30h ; (08) 011C16B7 mov eax,0CCCCCCCCh ; (09) 011C16BC rep stos dword ptr es:[edi] ; (10) 011C16BE mov eax,dword ptr [ebp+8] ; (11) 第 1 引数 'a' 011C16C1 add eax,dword ptr [ebp+0Ch] ; (12) EAX ← a + b 011C16C4 pop edi ; (13) 011C16C5 pop esi ; (14) 011C16C6 pop ebx ; (15) 011C16C7 mov esp,ebp ; (16) 011C16C9 pop ebp ; (17) 011C16CA ret ; (18)
ステップ実行したときの、メモリとレジスタの様子をメモしたのが次の図。コメントの先頭に、行数を追記した。
グレーの部分が、plus 関数が呼び出されたときの、メモリの様子。main 関数が、引数である「7」と「A」をスタックに入れて、plus をコールしている。
(11) 行目と (12) 行目の 2 行がコア。他は、前処理と後処理のようなもの。
(1) 〜 (3) 行目と、(17) 〜 (18) 行目は、関数内でスタックを利用する場合の、お約束のコード。これまでのスタックのベースポインタを、バックアップしておいて、新しいポインタを使う。関数が終了すると、これらを元に戻す。
(4) 〜 (6) 行目、(13) 〜 (15) 行目は、もともとの値をスタックに退避させ、関数が終了する際に、元に戻す処理。
(7) 〜 (10) 行目が、謎。どこかで調べる。
- ESI(ソースインデックス)、EDI(ディスティネーションインデックス)の使いどころ。それを言ったら、ECX(カウントレジスタ)もよくわからない
- (10) 行目の処理。そもそも、何でメモリを「C」で埋めたいのだろう(初期化?)。とりあえず、図では、赤で示しておく
思っていたのとは少し違った。もっとシンプルだったと思ったが。
2017/7/20 追記
もう一回、環境を作らなければいけなくなった。Visual Studio Community 2017 で、空のプロジェクトを作成する。プログラムを実行したときに、コマンドプロンプトが消えないように、プロジェクトのプロパティから、次のオプションを有効にする。
2017/9/18 追記
体力的にきつく、なかなか進まないのだが、ほんの少し勉強した。しかし、ココ「assembly - Why is the stack filled with 0xCCCCCCCC - Stack Overflow」に書いてあることは、まだよくわからない。
とりあえず、わかったこと。CPU は、命令を実行するたびに、割り込み要求を確認すること。割り込み発生時には、割り込み番号に対応するサブルーチンを実行すること。この割り込み番号をオペランドとする 2 バイトの命令が int であること。ただし、割り込み割り込み番号 3 だけは、1 バイト(CC)で表されること。どうやら int 3 は、デバッガでブレークポイントを設定するのに便利であるっぽいこと。
2017/9/20 追記
あと、一応 main 関数も書いておくことにした。この先、よく出てくるから。
_main: 00A116A0 push ebp ; (01) ベースポインタを退避(1 つ前のスタックフレームの先頭) 00A116A1 mov ebp,esp ; (02) ベースポインタを、スタックポインタの初期値に設定 00A116A3 sub esp,0C0h ; (03) スタックポインタを 48×4 先へ 00A116A9 push ebx ; (04) EBX を退避 00A116AA push esi ; (05) ESI(ソースレジスタ)を退避 00A116AB push edi ; (06) EDI(ディスティネーションレジスタ)を退避 00A116AC lea edi,[ebp-0C0h] ; (07) EDI ← ベースポインタの 48×4 先のアドレス 00A116B2 mov ecx,30h ; (08) ECX ← 48 00A116B7 mov eax,0CCCCCCCCh ; (09) EAX ← 0xCCCCCCCC 00A116BC rep stos dword ptr es:[edi] ; (10) ES:[EDI] を EAX で埋める × 48 00A116BE push 0Ah ; (11) 第 2 引数 10('b') 00A116C0 push 7 ; (12) 第 1 引数 7('a') 00A116C2 call _plus (0A112C1h) ; (13) plus 関数の呼び出し 00A116C7 add esp,8 ; (14) スタックポインタを移動 00A116CA pop edi ; (15) EDI を復元 00A116CB pop esi ; (16) ESI を復元 00A116CC pop ebx ; (17) EBX を復元 00A116CD add esp,0C0h ; (18) スタックポインタを 48×4 戻す 00A116D3 cmp ebp,esp ; (19) EBP と ESP の比較(結果は __RTC_CheckEsp 冒頭で検証) 00A116D5 call __RTC_CheckEsp (0A11113h) ; (20) ESP の整合性をチェックする 00A116DA mov esp,ebp ; (21) スタックポインタを復元 00A116DC pop ebp ; (22) ベースポインタを復元 → スタックフレームの破棄 00A116DD ret ; (23)
__RTC_CheckEsp については、「c++ - How's __RTC_CheckEsp implemented? - Stack Overflow」 を参考にした。