「最も簡単な逆アセンブル(分岐) - あしのあしあと」の続き。今回は、ループを入れてみる。
a から b までを足し合わせる sum という関数*1で実験する。
int __cdecl sum(int a, int b) { /* TODO: 引数のチェック */ int sum = 0; int i; for (i = a; i <= b; i++) { sum = sum + i; } return sum; } int main(void) { return sum(0x07, 0x0A); }
逆アセンブルして出力されたコードが次。ちょっと量がでてきた。とはいっても、まだまだ短いので、とりあえず、全部載せてみる。環境も初回と変わらず、Windows 10 上の、Visual Studio Community 2017。
_sum: 00363B20 push ebp ; (01) 00363B21 mov ebp,esp ; (02) 00363B23 sub esp,0D8h ; (03) 00363B29 push ebx ; (04) 00363B2A push esi ; (05) 00363B2B push edi ; (06) 00363B2C lea edi,[ebp+FFFFFF28h] ; (07) [ebp-0D8h] 00363B32 mov ecx,36h ; (08) 00363B37 mov eax,0CCCCCCCCh ; (09) 00363B3C rep stos dword ptr es:[edi] ; (10) 00363B3E mov dword ptr [ebp-8],0 ; (11) 00363B45 mov eax,dword ptr [ebp+8] ; (12) 00363B48 mov dword ptr [ebp-14h],eax ; (13) 00363B4B jmp 00363B56 ; (14) 00363B4D mov eax,dword ptr [ebp-14h] ; (15) 00363B50 add eax,1 ; (16) 00363B53 mov dword ptr [ebp-14h],eax ; (17) 00363B56 mov eax,dword ptr [ebp-14h] ; (18) 00363B59 cmp eax,dword ptr [ebp+0Ch] ; (19) 00363B5C jg 00363B69 ; (20) 00363B5E mov eax,dword ptr [ebp-8] ; (21) 00363B61 add eax,dword ptr [ebp-14h] ; (22) 00363B64 mov dword ptr [ebp-8],eax ; (23) 00363B67 jmp 00363B4D ; (24) 00363B69 mov eax,dword ptr [ebp-8] ; (25) 00363B6C pop edi ; (26) 00363B6D pop esi ; (27) 00363B6E pop ebx ; (28) 00363B6F mov esp,ebp ; (29) 00363B71 pop ebp ; (30) 00363B72 ret ; (31)
(10) 行目まで、および (26) 行目以降は、これまでと全くいっしょ。詳細は、依然としてよくわからないまま。
メモリの状況は、次の図の通り。グレーの部分は、初回同様。main 関数が、引数である「0x07」と「0x0A」をスタックに入れて、sum 関数をコールしている。なお、EBX, ESI, EDI の退避(push)については、毎度おなじみなので、省略している。
EBP は、(2) 行目で、関数がコールされたときのスタックポインタを保持するので、sum 関数では、EBP が基準となる。[EBP - 8h] が、変数 sum の値が格納されるアドレスを、[EBP - 14h] が、変数 i の値が格納されるアドレスを表している。なぜ、とびとびのアドレスになっているのかは、よくわからない。
(11) 行目で、sum が初期化され、(13) 行目で、i に 0x07 が入る。この値は、(16) 行目でカウントアップされ、(17) 行目で i に格納される。(22) 〜 (23) 行目で、sum の値に、i の値が足される。
これを踏まえて、ループの処理を 1 行ずつ、詳しくみていく。(11) 行目から (25) 行目の処理は、次のようになっている。
とにかく、ひたすら EAX を使う。EAX の値を入れ替えて、演算。この繰り返し。
ここまで丁寧に絵にしなくても、さすがに処理内容は理解できる。少々やりすぎたか。
2017/09/20 追記
初めは、もっともっと、簡単な処理を試せばよかったのだよ、おそらくは。
ということで、0 から 99 まで表示するだけのプログラムを試してみる。
int main(void) { for (int i = 0; i < 100; i++) { printf("%d\r\n", i); } return; }
逆アセンブルすると、次のようになる。あれ?
あまり簡単になっていないような。
01004060 push ebp ; (01) 01004061 mov ebp,esp ; (02) 01004063 sub esp,0CCh ; (03) 01004069 push ebx ; (04) 0100406A push esi ; (05) 0100406B push edi ; (06) 0100406C lea edi,[ebp-0CCh] ; (07) 01004072 mov ecx,33h ; (08) 01004077 mov eax,0CCCCCCCCh ; (09) 0100407C rep stos dword ptr es:[edi] ; (10) 0100407E mov dword ptr [ebp-8],0 ; (11) 初期化: int i = 0; 01004085 jmp main+30h (01004090h) ; (12) ループに入る 01004087 mov eax,dword ptr [ebp-8] ; (13) インクリメント 0100408A add eax,1 ; (14) インクリメント 0100408D mov dword ptr [ebp-8],eax ; (15) インクリメント 01004090 cmp dword ptr [ebp-8],64h ; (16) 比較: i < 100; 01004094 jge main+49h (010040A9h) ; (17) 比較: 100 以上ならループを抜ける 01004096 mov eax,dword ptr [ebp-8] ; (18) 01004099 push eax ; (19) printf: 第 2 引数 0100409A push offset string "%d\r\n" (01006BCCh) ; (20) printf: 第 1 引数 0100409F call _printf (01001361h) ; (21) printf: コール 010040A4 add esp,8 ; (22) スタックポインタを戻す 010040A7 jmp main+27h (01004087h) ; (23) ループする 010040A9 xor eax,eax ; (24) 010040AB pop edi ; (25) 010040AC pop esi ; (26) 010040AD pop ebx ; (27) 010040AE add esp,0CCh ; (28) 010040B4 cmp ebp,esp ; (29) 010040B6 call __RTC_CheckEsp (01001118h) ; (30) 010040BB mov esp,ebp ; (31) 010040BD pop ebp ; (32) 010040BE ret ; (33)
(01) 〜 (10) 行目と、(24) 〜 (33) 行目は、「最も簡単な逆アセンブル - あしのあしあと」 の最後、『あと、一応 main 関数も書いておくことにした。』に書いた通り。(11) 〜 (23) 行目が本丸。「EBP - 8」が、「i」を表している。演算は EAX で行うため、「EBP - 8」と「EAX」の間で、頻繁に値の入れかえをしている。
なるほど。構造は見えた。
こんな for ループであれば、逆アセンブルすると、次のような構造をしている。
ループの構造のバリエーションが、たくさんあるとは思えない。こんなものだと思う。
で、最後に、exe ファイルを IDA に食わせてやると、次のようなグラフビューを出力してくれる。
な、なんと。こんなにシンプルにしてくれるとは。やはり、簡単な処理を試しておいてよかった。
2017/09/23 追記
で、もともとの、a から b までの合計を求める処理に戻る。a と b が固定値だと、IDA では、合計値を事前に計算してくれるだろう。これはありがたいことではあるが、グラフビューがループの構造にならず、ループの勉強にならない。そこで、やはり a と b を引数で渡すことにした。
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { if (argc != 3) return -1; int a = atoi(argv[1]); int b = atoi(argv[2]); int sum = 0; int i; for (i = a; i <= b; i++) { sum = sum + i; } printf("%d\r\n", sum); return 0; }
これなら、ループの構造になるだろう。きっと。
え。読めないんですけど。なんでこんなことに?
2017/09/24 追記
while 文を忘れていた。おそらく、すごいシンプルになるだろうが、一応やっておく。
#include <stdio.h> #include <stdlib.h> int main(void) { int i = 0; while (i < 100) { printf("%d\r\n", i); i++; } return; }
逆アセンブルすると、次のようになる。ほらね。
00234060 push ebp ; (01) 00234061 mov ebp,esp ; (02) 00234063 sub esp,0CCh ; (03) 00234069 push ebx ; (04) 0023406A push esi ; (05) 0023406B push edi ; (06) 0023406C lea edi,[ebp-0CCh] ; (07) 00234072 mov ecx,33h ; (08) 00234077 mov eax,0CCCCCCCCh ; (09) 0023407C rep stos dword ptr es:[edi] ; (10) 0023407E mov dword ptr [ebp-8],0 ; (11) i = 0; 00234085 cmp dword ptr [ebp-8],64h ; (12) ● 条件 0x64 以上? 00234089 jge main+47h (02340A7h) ; (13) ■ ジャンプ 0023408B mov eax,dword ptr [ebp-8] ; (14) 第 2 引数 & ESP - 4 0023408E push eax ; (15) 第 1 引数 & ESP - 4 0023408F push offset string "%d\r\n" (0236BCCh) ; (16) _printf 00234094 call _printf (0231361h) ; (17) _printf 00234099 add esp,8 ; (18) ESP + 8(もとに戻す) 0023409C mov eax,dword ptr [ebp-8] ; (19) 0023409F add eax,1 ; (20) 002340A2 mov dword ptr [ebp-8],eax ; (21) 002340A5 jmp main+25h (0234085h) ; (22) ● 無条件ジャンプ 002340A7 xor eax,eax ; (23) ■ 002340A9 pop edi ; (24) 002340AA pop esi ; (25) 002340AB pop ebx ; (26) 002340AC add esp,0CCh ; (27) 002340B2 cmp ebp,esp ; (28) 002340B4 call __RTC_CheckEsp (0231118h) ; (29) 002340B9 mov esp,ebp ; (30) 002340BB pop ebp ; (31) 002340BC ret ; (32)
(22) 行目から (12) 行目への無条件(アンコンディショナル)ジャンプが、ループのキモ。
for 文のときと違って、インクリメントのセクションがない。もちろん、(19) 〜 (21) 行目で、i をインクリメントしているのだが。
そして、(13) 行目のコンディショナルジャンプで、ループを抜ける。
*1:引数の名前は、これまでとそろえるために a, b のままにした。本当は、from, to とかにすればよいのだろうけど。