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)など