トランポリン

http://d.hatena.ne.jp/hayamiz/20080629/1214745930

これはきっと shiro さんがすごく面白いコメントを書いてくださるんだろう…と思ってたけどそうでもないみたいなので、まとまりのない話を書く。

まずなんか「なんでも継続」にある

http://practical-scheme.net/docs/cont-j.html

一般には、関数func1から関数func2を直接呼べない場合に「func2を呼ぶという継続」をスタックにプッシュして、 func1から一度抜けるテクニックを「トランポリン」と呼ぶ。

というのが一般にトランポリンなのか、っていうと、よくわからんけど私はもちょい一般に、 func1 がなんらかの手段で生成されたコードを通って func2 を呼ぶのがトランポリンなんじゃないかなぁとか思ってました。少なくとも GCC は func1 がトランポリンコードを call してそのトランポリンコードが jmp して func2 に入る、と思う。

GCC のコードなんだけど、 こんなの読めるわけなくて、いや読める人いるのかも知れませんが私は読めなくて、なぜなら一番のキモである

    leal    -28(%ebp), %ebx # スタックのアドレスを ebx に
    ...
    movb    $-71, (%edx)    # mov <imm>, %ecx
    movl    %ebx, 1(%edx)   # mov の <imm>
    movb    $-23, 5(%edx)   # jmp
    movl    %eax, 6(%edx)   # jmp の飛び先

がコード生成してる部分だけど、こんなもん opcode 覚えてないとわからんわけです。凡人は素直に生成されたコードをダンプとかするといいかと思います。

#include <stdio.h>

void other(void (*funcp)()){
  char* p = (char*)funcp;
  int i;
  for (i = 0; i < 10; i++) {
      printf("%c", p[i]);
  }
  //funcp();
}

void outer(void){
  asm("#");
  int a = 12;
  void inner(void){ printf("other a is %d\n", a); }
  asm("#");
  other(inner);
  asm("#");
}

int main() {
    outer();
}

とかしてコードを出力してやって(asm("#"); は このへん参照)中を見ると。

i@u4 ~/test
> ./a.out > hoge
i@u4 ~/test
> objdump -D -b binary -m i386 hoge

hoge:     file format binary


Disassembly of section .data:

00000000 <.data>:
   0:   b9 f0 21 f4 ff          mov    $0xfff421f0,%ecx
   5:   e9 23 62 10 08          jmp    0x810622d

まぁつまり mov して jmp と。 mov 用の即値はこれスタックのアドレスです。実際 inner の方見てやると、

inner.1284:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $8, %esp
    movl    (%ecx), %eax   # ecx の参照先は a なの
    movl    %eax, 4(%esp)
    movl    $.LC0, (%esp)
    call    printf
    leave

と。適当だけどこんなもんでなんとなくなんで動くかと、こういうのをどうやって調べるか(まぁいくらでも方法はあるけど。 gdb 使うとかもいいと思う)とかが伝わればいいなーと思います。

でなんで必要か、だけど、この場合、

  • コンパイル時にはわかってなくて、 outer 実行時にはわかってる情報(今回の場合スタックのアドレス)を伝えたい
  • でも呼び出し側(今回は other)からはあくまで普通の関数ポインタのように使えて欲しい

の二つを両立させたいから。

そもそもなんでこんなものが必要かっていうと、 ABI を強引に変えたい時に緩衝材みたいな感じだと思う。 GCC の例では、 inner がヘンな関数(ecx 経由で環境のスタックアドレスが渡される、という約束が通常の ABI に加えられている)だけど呼び出し側 (other) は普通に書かれてるので間にトランポリンかましてやろう、とか、なんでも継続のシグナルハンドラの例では呼ばれる側は普通の子だけど、呼ぶ側が kernel 内なので前後でごにょごにょ通常の ret で帰ろうとしないようにしないといかん、とかいう。

Yajit でもなんか trampoline 作ってて、 definemethod を実装する時に、

  • 生成した関数を rb_define_method で可変引数関数として登録したい
  • でも JIT した関数は Ruby から直接呼んでもらうんじゃなくて、下準備をする関数 rb_yajit_func_call を通ってから呼んでもらいたい
  • で、下準備をする関数 rb_yajit_func_call は JIT されたコードを受け取る必要があるんだけど、 rb_define_method はそんなの渡してはくれない
  • だから rb_define_method には「JITされたコードを引数にして rb_yajit_func_call を呼ぶトランポリンコード」を登録

て感じで。

あとなんか例のごとく Wikipedia に色々書いてありますね。なんか本当にそれもトランポリンって呼ぶのかっていうのが結構書いてあるような。

http://en.wikipedia.org/wiki/Trampoline_(computers)

TODO: 気がむいたらサンプルとか?

なにかあれば下記メールアドレスへ。
shinichiro.hamaji _at_ gmail.com
shinichiro.h