[32ビット・システム] 関数呼び出し時のスタックの動きのメモ

32ビット・システムにおける関数呼び出し時のスタックの動きについて。
こちらの記事でも書いた内容を改めてまとめ直した。

まずは関連用語の覚書

スタック
関数の引数、関数内部のローカル変数や戻りアドレスが格納される。LIFO (Last In, First Out 後入れ先出し)。
スタック領域はメモリの上位アドレスから下位アドレスに向かって確保されていく。スタック領域はプログラム実行時にコンパイラやOSによって自動的に割り当てられる。

ヒープ
プログラムの実行中に動的に確保されるメモリ領域。例えばmallocによって動的にメモリ領域を確保する場合は、ヒープ領域上に確保される。ヒープ領域はメモリの下位アドレスから上位アドレスに向かって確保されていく。

EIP
次に実行する命令文のアドレスを格納している。命令文が実行されるたび、EIPの値は加算される (どのくらい加算されるかは実行された命令文のサイズによる)

ESP
スタック・ポインタ。スタック領域のトップに積まれたデータのメモリー・アドレスを格納する。スタックはメモリの上位アドレスから下位アドレスに向かって積み上がっていくので、ESPは最下位のアドレスに格納されたデータを指す。

EBP
ベース・ポインタ。データの格納領域の基点のメモリー・アドレスを格納する。通常、関数の最初において、EBPにはESPの値が設定される。これは関数の引数とローカル変数を追跡するためである。ローカル変数へのアクセスはEBPからオフセットを引くことによって行われる。関数の引数へのアクセスはEBPにオフセットを加算することによって行われる。
※ただし、gccなどのGNUコンパイラは関数の引数・変数へのアクセスにEBPではなく、ESPを用いる。

以下のサンプル・プログラムを例にスタックの動きを追ってみる。


#include <stdio.h>

int sum(int a, int b)
{
    int result;
    result = a + b;
    printf("%d\n", result);

    return result;
}

int main(void)
{
    sum(5, 10);

    return 0;
}

上記のソースコードをコンパイルする。

gcc stack-layout.c -o stack-layout.out -fno-pie -fno-stack-protector -m32

-fno-pie: PIE形式でコンパイルしないためのオプション
-fno-stack-protector: スタックの保護機能を無効化するためのオプション
-m32: 32ビットのバイナリ形式でコンパイルすためのオプション

コンパイルしたプログラムをobjdumpコマンドで逆アセンブルする。

objdump -d -M intel stack-layout.out

main関数の逆アセンブル: sum関数を引数付きで呼び出す。(引数は5と10)


08048446 <main>:
 8048446:    55                      push   ebp
 8048447:    89 e5                   mov    ebp,esp
 8048449:    83 e4 f0                and    esp,0xfffffff0
 804844c:    83 ec 10                sub    esp,0x10
 804844f:    c7 44 24 04 0a 00 00    mov    DWORD PTR [esp+0x4],0xa
 8048456:    00 
 8048457:    c7 04 24 05 00 00 00    mov    DWORD PTR [esp],0x5
 804845e:    e8 ba ff ff ff          call   804841d <sum>
 8048463:    b8 00 00 00 00          mov    eax,0x0
 8048468:    c9                      leave  
 8048469:    c3                      ret    
 804846a:    66 90                   xchg   ax,ax
 804846c:    66 90                   xchg   ax,ax
 804846e:    66 90                   xchg   ax,ax

sum関数の逆アセンブル: 2つの整数の合計を求める


0804841d <sum>:
 804841d:    55                      push   ebp
 804841e:    89 e5                   mov    ebp,esp
 8048420:    83 ec 28                sub    esp,0x28
 8048423:    8b 45 0c                mov    eax,DWORD PTR [ebp+0xc]
 8048426:    8b 55 08                mov    edx,DWORD PTR [ebp+0x8]
 8048429:    01 d0                   add    eax,edx
 804842b:    89 45 f4                mov    DWORD PTR [ebp-0xc],eax
 804842e:    8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 8048431:    89 44 24 04             mov    DWORD PTR [esp+0x4],eax
 8048435:    c7 04 24 00 85 04 08    mov    DWORD PTR [esp],0x8048500
 804843c:    e8 af fe ff ff          call   80482f0 <printf@plt>
 8048441:    8b 45 f4                mov    eax,DWORD PTR [ebp-0xc]
 8048444:    c9                      leave  
 8048445:    c3                      ret  

コンパイルしたプログラムをデバッグしてスタックの内容を確認してみる。

main関数内でsum関数を呼び出している箇所(アドレス 0x804845e)にブレークポイントをセットし、ステップイン実行する。


gdb ./stack-layout.out

(gdb) b *0x804845e
Breakpoint 1 at 0x804845e
(gdb) r
Starting program: /home/sansforensics/Desktop/stack-layout.out 

Breakpoint 1, 0x0804845e in main ()
(gdb) si
0x0804841d in sum ()

スタックの最上位にsum関数が実行されたあとの次の命令のアドレス0x08048463が格納されている。


(gdb) x/x $esp
0xffffd0ac:    0x08048463

その下にはsum関数の第一引数 5が格納されている。


(gdb) x/d $esp+0x4
0xffffd0b0:    5

さらに、その下にはsum関数の第2引数 10が格納されている。


(gdb) x/d $esp+0x8
0xffffd0b4:    10

まとめると、引数が2つスタックに積まれ、さらにsum関数が実行されたあとの次の命令のアドレスがリターンアドレスとしてスタックに積まれる。この操作はcallが実行されると自動的に行われる。

sum関数に処理が移る。 まず、push ebp命令によってmain関数のEBPレジスタの値(0xffffd0c8)がスタックに積まれる。これにより、sum関数の実行が完了したあとにmain関数に戻れるようになる。
そして、mov ebp, esp命令によりEBPとESPの指すアドレス位置が等しくなる。

push ebpの直前


Breakpoint 1, 0x0804845e in main ()
(gdb) si
0x0804841d in sum ()
(gdb) p $ebp
$6 = (void *) 0xffffd0c8
(gdb) x/x $esp
0xffffd0ac:    0x08048463

push ebpの実行後。EBPの値(0xffffd0c8)がESPの最上位に格納された。


0x0804841e in sum ()
(gdb) p $ebp
$8 = (void *) 0xffffd0c8
(gdb) x/x $esp
0xffffd0a8:    0xffffd0c8
(gdb) p $esp
$9 = (void *) 0xffffd0a8

mov ebp,espの実行後。EBPとESPの指すアドレス位置が等しくなった。


0x08048420 in sum ()
(gdb) p $ebp
$12 = (void *) 0xffffd0a8
(gdb) p $esp
$13 = (void *) 0xffffd0a8
(gdb)

sub esp, 0x28命令によってESPレジスタの指す位置を下位のアドレスに移動させる。EBPレジスタの指す位置とESPレジスタの指す位置の間にできた領域はローカル変数の格納に利用される。この場合は0x28、つまり10進数で40バイトの領域がローカル変数の領域として割り当てられる。

2つの引数を加算して結果をprintfによって出力した後、leave命令を呼び出す。

leave命令によってESPの指す位置がEBPの指す位置と同じ位置に戻され、保存していたmain関数のEBPレジスタの値(0xffffd0c8)が復元される。


Breakpoint 1, 0x08048444 in sum ()
(gdb) si
0x08048445 in sum ()
(gdb) p $ebp
$3 = (void *) 0xffffd0c8
(gdb)

leave命令はmov esp, ebp; pop ebpと等しい動作をする。
leave命令完了後は、ESPはリターンアドレス0x08048463を指している。


(gdb) si
0x08048445 in sum ()
(gdb) x/x $esp
0xffffd0ac:    0x08048463
(gdb) 

最後にret命令を呼び出す。ret命令はスタックのトップから値をpopし、そこへjmpする (pop eipと等しい)。今回の場合スタック上位に積まれているのはsum関数実行後のリターンアドレス0x08048463である。 retによってsum関数からmain関数のアドレス0x08048463に処理が移っていることが下記から読み取れる。


0x08048445 in sum ()
(gdb) x/x $esp
0xffffd0ac:    0x08048463
(gdb) si
0x08048463 in main ()
(gdb)

Leave a Reply

Your email address will not be published. Required fields are marked *