関数のプロローグについて
関数のプロローグについて
関数のプロローグとは、関数を呼び出して実行し、呼び出し元に戻る際に必要となる情報(局所変数のメモリ、rbp、戻りアドレスなど)をスタックに積み上げる処理のことを指す
関数のプロローグを実際に確認するため、以下のようなプログラムを考えてみる。あくまで、関数のプロローグを見るための適当なプログラムである。
#include <stdio.h>
void func(void)
{
int x = 100;
char str[] = "Hello, World!";
}
int main(void)
{
func();
return 0;
}
これをコンパイルしつつデバッグ情報を付与した後に、GDBデバッガを起動させる。
gcc -o test test.c -g
gdb -q ./test
GDBデバッガに入ったら、まずは個人的に分かりやすいと思っているintel記法へと設定。
(gdb) set disassembly-flavor intel
いよいよ本題。ソースコードをアセンブリとして表示してみる。
(gdb) disass main
Dump of assembler code for function main:
0x000055555555519d <+0>: endbr64
0x00005555555551a1 <+4>: push rbp
0x00005555555551a2 <+5>: mov rbp,rsp
0x00005555555551a5 <+8>: call 0x555555555149 <func>
0x00005555555551aa <+13>: mov eax,0x0
0x00005555555551af <+18>: pop rbp
0x00005555555551b0 <+19>: ret
End of assembler dump.
(gdb) disass func
0x0000555555555149 <+0>: endbr64
0x000055555555514d <+4>: push rbp
0x000055555555514e <+5>: mov rbp,rsp
0x0000555555555151 <+8>: sub rsp,0x20
0x0000555555555155 <+12>: mov rax,QWORD PTR fs:0x28
0x000055555555515e <+21>: mov QWORD PTR [rbp-0x8],rax
0x0000555555555162 <+25>: xor eax,eax
0x0000555555555164 <+27>: mov DWORD PTR [rbp-0x1c],0x64
0x000055555555516b <+34>: movabs rax,0x57202c6f6c6c6548
0x0000555555555175 <+44>: mov QWORD PTR [rbp-0x16],rax
0x0000555555555179 <+48>: mov DWORD PTR [rbp-0xe],0x646c726f
0x0000555555555180 <+55>: mov WORD PTR [rbp-0xa],0x21
0x0000555555555186 <+61>: nop
0x0000555555555187 <+62>: mov rax,QWORD PTR [rbp-0x8]
0x000055555555518b <+66>: sub rax,QWORD PTR fs:0x28
0x0000555555555194 <+75>: je 0x55555555519b <func+82>
0x0000555555555196 <+77>: call 0x555555555050 <__stack_chk_fail@plt>
0x000055555555519b <+82>: leave
0x000055555555519c <+83>: ret
End of assembler dump.
特に、func関数のアセンブリの中にある
0x000055555555514d <+4>: push rbp
0x000055555555514e <+5>: mov rbp,rsp
0x0000555555555151 <+8>: sub rsp,0x20
この部分こそが関数のプロローグと呼ばれる処理である。
なにをしているのか?
最初に述べたように、関数のプロローグは、関数の実行と呼び出し元に戻るための情報をスタックに積み上げる。関数のプロローグを理解するためには、rspレジスタとrbpレジスタの理解から始めよう。rspとrbpは適当な話、スタックに積まれた一つのフレーム(関数実行時に必要となる情報の集まり)の上下のアドレスを保持している。つまりrspとrbpが保持しているアドレスの間に局所変数などか格納されているということ。
func関数を呼び出す前と、呼び出されてfuncの処理が行われているときのrspとrbpが持つアドレスを見てみよう。まずはbreakコマンドにより、7行目(func関数内)と11行目(func関数呼び出し前)にブレイクポイントを設定し、runコマンドで最初のブレイクポイント、func関数呼び出し前の11行目で処理を停止させてみる。
(gdb) break 7
(gdb) break 11
(gdb) run
次にfunc関数を呼び出す前のrspとrbpが保持するアドレスについて見ていく。以下のコマンドで、レジスタが保持している値を確認できる。
(gdb) i r $rsp $rbp
rsp 0x7fffffffe060 0x7fffffffe060
rbp 0x7fffffffe060 0x7fffffffe060
先程、rspとrbpが保持しているアドレスの間に局所変数などか格納されていると書いたが、main関数内では局所変数を宣言していないため、スタック上には領域が確保されていないのです。つまりrspとrbpの値は同じアドレスを指しているということになる。
funcを呼び出す前のスタックの状態が確認ができたら、次はfunc実行中のの状態を確認してみる。contコマンドで、次のブレイクポイントである7行目(func関数内)へと処理を進め、先ほどと同じinfo registerコマンドを使用して、レジスタを確認。
(gdb) cont
(gdb) i r $rsp $rbp
rsp 0x7fffffffe030 0x7fffffffe030
rbp 0x7fffffffe050 0x7fffffffe050
アドレスが先程よりも小さくなっていることが分かると思う。これはmain関数のスタックフレームが存在している0x7fffffffe060の上にfunc関数のスタックフレーム(関数実行に必要な情報)が積み上げられたためである。またrspとrbpには0x20の差がありますが、この差の中に局所変数などが格納されている。
もう少し詳しく
今度は命令を追って、具体的に値が変更される流れを見ていこう。今のスタックのイメージは先程のrsp・rbpのアドレスを参考に、このような状態からスタートする。

まずはmain関数内にあるcall命令から。
0x00005555555551a5 <+8>: call 0x555555555149 <func>
call命令は戻りアドレスをスタックへpush(積み上げ)し、ripを呼び出し先の関数の先頭アドレスに変更する。 また、戻りアドレスとはmain関数内のcall命令の次の命令のアドレスを意味する。
つまり以下の命令のアドレスが、戻りアドレスとして0x7fffffffe060の上にpushされるということになる。
0x00005555555551aa <+13>: mov eax,0x0
そしてpushを行った場合、rspは減算される。
つまりrspは、保持している0x7fffffffe060から0x8が減算されて0x7fffffffe058という値を格納する。

また、ripに格納される呼び出し先の関数(func)の先頭アドレスはこれ。
0x0000555555555149 <+0>: endbr64
ripとは次に実行する命令のアドレスを保持するレジスタのことで、プロセッサはこれを使って次の命令の場所を把握して実行する。call命令を使用することで、呼び出し先の関数の命令が格納されているアドレスへとripを書き換え、関数の実行が実現する。
では次の命令、つまり関数のプロローグ最初の命令を見ていこう。
0x000055555555514d <+4>: push rbp
これは、rbpをスタックへとpushする命令。main関数へ制御を返す際にrbpの値を復元するために後々用いられる。0x7fffffffe058の上にrbpの現在の値がpushされるため、rspのアドレスは先ほどと同じように0x8だけ小さくなるので0x7fffffffe050となる。

次の命令はrbpへrspのアドレスをmov命令によってコピーする。これによって作成するfunc関数のスタックフレーム、そのボトムをrbpが表すようになる。
0x000055555555514e <+5>: mov rbp,rsp

これで、関数から呼び出し元へと帰る際の準備が完了。あとは局所変数などを格納するためにrspの値を減算して領域を確保するだけ。
0x0000555555555151 <+8>: sub rsp,0x20
sub命令はrspから0x20を引き算するよう命令している。

これにて関数プロローグを全て追うことができた。
確認
本当に一連の動作が行われているのかを確認していこう。examineコマンドを使用してrspのアドレスからmain関数のフレームである0x7fffffffe060のあたりまでを確認。
(gdb) x/8xg $rsp
0x7fffffffe030: 0x0000006400000002 0x2c6f6c6c6548fbff
0x7fffffffe040: 0x0021646c726f5720 0xfea8e158512f7600
0x7fffffffe050: 0x00007fffffffe060 0x00005555555551aa
0x7fffffffe060: 0x0000000000000001 0x00007ffff7db8d90
下から2行目には右に戻りアドレス、左にrbpが格納されていることが確認できる。 また、上から1行目の4バイトまでを数えると0x00000064が格納されており、これは10進数の100であることもわかる。さらには上から2行目の右側と、3行目の左側の値をASCIIコード表と比べてみると
0x 2c 6f 6c 6c 65 48 fb ff
, o l l e H
0x 00 21 64 6c 72 6f 57 20
\0 ! d l r o W Space
上記のように、Hello, World! が確認できる。