アセンブリメモ -スタック-
関数呼び出し時のスタックの動きなどのメモ.
#include <stdio.h> int func(int a, int b, int c, int d){ int y = 20; char buf[10] = "ABCD"; return a+b+c+d; } int main(){ int x = 10; func(1, 2, 3, 4); }
main()の逆アセンブル結果
0x08048400 <+0>: push %ebp 0x08048401 <+1>: mov %esp,%ebp 0x08048403 <+3>: sub $0x10,%esp 0x08048406 <+6>: movl $0xa,-0x4(%ebp) 0x0804840d <+13>: push $0x4 0x0804840f <+15>: push $0x3 0x08048411 <+17>: push $0x2 0x08048413 <+19>: push $0x1 0x08048415 <+21>: call 0x80483cb <func> 0x0804841a <+26>: add $0x10,%esp 0x0804841d <+29>: leave 0x0804841e <+30>: ret
func()の逆アセンブル結果
0x080483cb <+0>: push %ebp 0x080483cc <+1>: mov %esp,%ebp 0x080483ce <+3>: sub $0x10,%esp 0x080483d1 <+6>: movl $0x14,-0x4(%ebp) 0x080483d8 <+13>: movl $0x44434241,-0xe(%ebp) 0x080483df <+20>: movl $0x0,-0xa(%ebp) 0x080483e6 <+27>: movw $0x0,-0x6(%ebp) 0x080483ec <+33>: mov 0x8(%ebp),%edx 0x080483ef <+36>: mov 0xc(%ebp),%eax 0x080483f2 <+39>: add %eax,%edx 0x080483f4 <+41>: mov 0x10(%ebp),%eax 0x080483f7 <+44>: add %eax,%edx 0x080483f9 <+46>: mov 0x14(%ebp),%eax 0x080483fc <+49>: add %edx,%eax 0x080483fe <+51>: leave
先頭のpush, mov, subといった一連の命令が共通していることが分かる.
これは"関数プロローグ"などと呼ばれ, ebpをスタック上に退避させ, その関数で使用する局所変数のメモリをスタック上に確保する処理である.
main()から順に処理を見ていく.
関数プロローグの処理はfunc()で詳しく追いかけるとし, 0x0804840dからのpush命令から見ていく.
callで関数を呼び出す際はスタックに引数をpushしていく.
0x0804840d <+13>: push $0x4 0x0804840f <+15>: push $0x3 0x08048411 <+17>: push $0x2 0x08048413 <+19>: push $0x1 0x08048415 <+21>: call 0x80483cb <func>
func()呼び出し前に引数が逆順(FILO)でスタック上に確保pushされていることが確認できる.
続いてcallでfuncが呼ばれると, 処理は0x080483cbに移る.
この時, callは自分が呼ばれたアドレスを覚えておかないといけないため, 戻るべきアドレス(リターンアドレス)をスタックにプッシュする.
ここでのリターンアドレスは0x0804841aである.
ここまでのスタックを整理すると以下のようになる.
| | 0xbffff444 --> 0x804841a (Return Addr) | | 0xbffff448 --> 0x1 | | 0xbffff44c --> 0x2 | | 0xbffff450 --> 0x3 |______| 0xbffff454 --> 0x4
続いてfunc()の処理を見ていく.
(gdb) b *0x80483cb Breakpoint 1, 0x080483cb in func () (gdb) x/4i $pc => 0x80483cb <func>: push ebp 0x80483cc <func+1>: mov ebp,esp 0x80483ce <func+3>: sub esp,0x10 0x80483d1 <func+6>: mov DWORD PTR [ebp-0x4],0x14 (gdb) i r ebp ebp 0xbffff468 0xbffff468
func()の関数プロローグである.
ebpの値は0xbffff468で, スタックにpushされる.
これは退避されたフレームポインタ(SFP)と呼ばれ, main()に処理を戻す際にebpを元に戻すために使用される.
結果, スタックは以下のようになる.
| | 0xbffff440 --> 0xbffff468 --> 0x0 (SFP) | | 0xbffff444 --> 0x804841a (Return Addr) | | 0xbffff448 --> 0x1 | | 0xbffff44c --> 0x2 | | 0xbffff450 --> 0x3 |______| 0xbffff454 --> 0x4
続くmov命令でebpの値がespに, つまり, スタックの底とスタックの頂点が同じになる.
さらにsub命令でespが0x10減算される.
これによって0x10バイト分のメモリが確保された.
ここまでが関数プロローグである.
続いてfunc()では局所変数をyとbufの2種類を確保している.
変数確保後(0x080483df)でのスタックは以下のようになる.
(gdb) x/12 $esp 0xbffff430: 0x4241f666 0x00004443 0x080496ac 0x00000014 0xbffff440: 0xbffff468 0x0804841a 0x00000001 0x00000002 0xbffff450: 0x00000003 0x00000004 0x0804842b 0xb7fb6000 (gdb) x/s 0xbffff432 0xbffff432: "ABCD" (gdb) x/u 0xbffff43c 0xbffff43c: 20
| | 0xbffff430 --> 0x4241f666 | | 0xbffff434 --> 0x4443 ('CD') | | 0xbffff43c --> 0x14 | | 0xbffff440 --> 0xbffff468 --> 0x0 (SFP) | | 0xbffff444 --> 0x804841a (Return Addr) | | 0xbffff448 --> 0x1 | | 0xbffff44c --> 0x2 | | 0xbffff450 --> 0x3 |______| 0xbffff454 --> 0x4
このようにスタックはメモリの低位へ成長していく.
続くreturn a+b+c+dは続くmov, addの部分.
0x080483ec <+33>: mov edx,DWORD PTR [ebp+0x8] 0x080483ef <+36>: mov eax,DWORD PTR [ebp+0xc] 0x080483f2 <+39>: add edx,eax 0x080483f4 <+41>: mov eax,DWORD PTR [ebp+0x10] 0x080483f7 <+44>: add edx,eax 0x080483f9 <+46>: mov eax,DWORD PTR [ebp+0x14] 0x080483fc <+49>: add eax,edx
edxにebp+0x8(0xbffff448)の値である0x1を, eaxにebp+0xc(0xbffff44c)の値である0x2を代入し, edxにeaxを加算.
さらにeaxに0x3を代入しedxに加算...のようになり, 最終的にleave命令でeaxが戻り値として返る.
最後のret命令でリターンアドレスであるmain()の0x804841aに処理が戻る.