コース目次 / 第2章
呼出規約とスタックフレーム — 関数はどう呼ばれるか
小さなadd関数をobjdumpで逆アセンブルし、System V AMD64呼出規約(rdi/rsiで引数を渡す)とスタックフレームの構築(push rbp/mov rbp,rsp)・解体(pop rbp/ret)を、実物のコードで読みます。
小さな関数を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章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。