gccの生成するトランポリンコードについて
gccでは、次のように、関数の中で関数を定義することができます(内部に書かれた関数をnested functionなどと呼びます)。
void func(void) { int x; void inner_func(void) { printf("%d\n", x); // a. } inner_func(); // b. }
ここで、nested function では、外側の関数で宣言されているローカル変数を参照することができます(a.の部分)。どのように外側の関数が呼び出されたかによって、外側の関数のローカル変数のアドレスは変化しますから、b. でinner_func()を呼ぶとき、何らかの方法でそのアドレス(以下Xとします)を渡してやる必要があります。さもないとa.で正しい値をprintfできません。
Linux/x86 + gcc-3.3.3 では、ECXレジスタを用いてアドレスXを渡すことになっているようです。
func: (略) leal -8(%ebp), %ecx call inner_func.0
さて、C言語には関数ポインタというものがあります。関数ポインタを用いて、次のようなことをしたらどうなるでしょう?
void caller(void (*fp)(void)) { fp(); } void func(void) { int x; void inner_func(void) { printf("%d\n", x); // a. } caller(inner_func); // b. }
caller関数にアドレスXを渡し、そしてcaller関数がinner_func関数にアドレスXを渡せばよいと思われるかもしれませんが、caller関数はlibcなど外部の、コンパイル済みの関数かもしれませんので*1、caller関数を通常と異なる方法でコンパイルすることはできません。
ここで必要になるのが「トランポリン」です。func関数は、次のようにコンパイルされます。
- func関数のスタック上に、動的に実行コードを書く
- caller関数には、inner_funcのアドレスを渡すのではなく、いま書いた実行コードの先頭アドレスを渡すようにする
動的に生成される実行コードは、次の2命令です。
- アドレスXをECXレジスタに書き込む命令 (mov $0xXXXXXXXX,%ecx)
- inner_func関数に無条件にジャンプする命令 (jmp $0xYYYYYYYY)
関数ポインタを経由した関数fの呼び出しが、一旦自分が動的に生成したコードを経由することから、「トランポリン」と呼ばれるわけですね。
func: (略) leal -40(%ebp), %eax movl $inner_func.0, %ecx (略) movl %ecx, %edx movb $-71, (%eax) # -71 (=0xB9) は "ECXへのMOV" leal -8(%ebp), %ecx movl %ecx, 1(%eax) # アドレスX movb $-23, 5(%eax) # -23 (=0xE9) は "JMP" movl %edx, 6(%eax) # inner_funcのアドレス (略) leal -40(%ebp), %eax pushl %eax # callerに渡すのは動的に生成 # したコードの先頭アドレス call caller # あ、-O2 でコンパイルしたほうがむしろ読みやすい鴨... orz
以上。なお、g++ではnested functionは許可されていません。
参考:
http://www.uwsg.iu.edu/hypermail/linux/kernel/9912.3/0437.html
*1:qsort(3)など