コース目次 / 第2章

呼出規約とスタックフレーム — 関数はどう呼ばれるか

小さなadd関数をobjdumpで逆アセンブルし、System V AMD64呼出規約(rdi/rsiで引数を渡す)とスタックフレームの構築(push rbp/mov rbp,rsp)・解体(pop rbp/ret)を、実物のコードで読みます。

第2章 / 全5章目安 約12分この章のゴール: 呼出規約とスタックフレームの構造を、実際のアセンブリで読めるようになる

小さな関数を1つコンパイルし、そのアセンブリを読みます。add.c として保存します。

int add(int a, int b) {
    int result = a + b;
    return result;
}
int main(void) {
    return add(3, 4);
}
gcc -O0 -g -o add add.c
objdump -d -M intel --disassemble=add add
0000000000001129 <add>:
    1129:	f3 0f 1e fa          	endbr64
    112d:	55                   	push   rbp
    112e:	48 89 e5             	mov    rbp,rsp
    1131:	89 7d ec             	mov    DWORD PTR [rbp-0x14],edi
    1134:	89 75 e8             	mov    DWORD PTR [rbp-0x18],esi
    1137:	8b 55 ec             	mov    edx,DWORD PTR [rbp-0x14]
    113a:	8b 45 e8             	mov    eax,DWORD PTR [rbp-0x18]
    113d:	01 d0                	add    eax,edx
    113f:	89 45 fc             	mov    DWORD PTR [rbp-0x4],eax
    1142:	8b 45 fc             	mov    eax,DWORD PTR [rbp-0x4]
    1145:	5d                   	pop    rbp
    1146:	c3                   	ret

呼出規約 — 引数は、どこに来るのか

add(3, 4) と C で呼んだとき、3 と 4 はどこに渡っているのか。命令の3・4行目を見てください。

mov [rbp-0x14], edi と mov [rbp-0x18], esi——1つめの引数(a)は edi、2つめの引数(b)は esi というレジスタに入って届き、それを関数がローカルの置き場(rbp からの相対アドレス)にコピーしています。
これは偶然ではなく、System V AMD64 呼出規約という約束事です——最初の6個までの整数・ポインタ引数は、順に rdi, rsi, rdx, rcx, r8, r9 というレジスタで渡されます(32bit幅で使うときは edi, esi, edx, ecx, r8d, r9d)。7個目以降はスタック経由になりますが、多くの関数はそこまで届きません。

gdb で、実際にこの目で確かめます。

gdb -q -batch \
  -ex "break add" \
  -ex "run" \
  -ex "print \$rdi" \
  -ex "print \$rsi" \
  ./add
Breakpoint 1, add (a=3, b=4) at add.c:2
$1 = 3
$2 = 4

$rdi = 3、$rsi = 4——C コードの add(3, 4) が、そのまま「rdi に 3、rsi に 4 を積んで呼ぶ」という形で実現されていることが、実測で確認できました。

プロローグとエピローグ — 関数の入り口と出口

先頭と末尾の命令に、決まった型があります。

プロローグ(入り口)——push rbp(呼び出し元の rbp を退避)、mov rbp, rsp(現在のスタックの先端を、この関数の基準点にする)。これで「この関数のローカル変数は、この rbp から測る」という土台ができます。
エピローグ(出口)——pop rbp(退避しておいた、呼び出し元の rbp を戻す)、ret(スタックに積まれている戻り先アドレスを取り出して、そこへジャンプする)。
この「基準点を作って、使って、戻す前に元に戻す」という一組が、スタックフレームです。関数を呼ぶたびに、この小さな作業領域が積み上がり、関数が終わるたびに畳まれます。

sec-c の観察と、つながる

sec-c のバッファオーバーフローで見た「戻り先アドレスが上書きされて壊れる」という現象は、いまの言葉で言えます——ret 命令は、スタックのどこかに積まれている「戻り先アドレス」を信じてジャンプするだけです。もし、その戻り先アドレスの場所が、バッファのあふれで書き換わっていたら——ret は壊れた行き先に、律儀にジャンプします。あのクラッシュは、まさにここで起きていました。

持ち帰る一言

引数は rdi, rsi, rdx… の順にレジスタで届く。 プロローグでスタックフレームを作り、エピローグで畳んで ret で戻る。この定型句を覚えると、初めて見る関数のアセンブリでも、引数の数や大まかな構造がすぐ読めます。次は、条件分岐とループが、どんな命令の並びになるかを読みます。

こうなっていればOK

卒業まであと2章です。

この章はまだ完了していません。