アセンブリメモ -スタック-

関数呼び出し時のスタックの動きなどのメモ.

#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に処理が戻る.