Cでクロージャ
int
retfunc( int x )
{
int a = 20;
int hogehoge( ) { return a; }
return hogehoge();
}GCC拡張で、ネストした関数は、外側のスコープの変数を参照できる、というのは有名な話かと思う。
これの実装は結構面白くて、スタックの上に、フレームをロードしたあと、関数を呼び出すプログラムを実行時に生成して、それを関数ポインタとする、というようになっている。
(どっか解説有るかなーっと思ったけど見つからないな…google:gcc trampolineとかを漁ってみるとよいかもしれない)
gcc4.1、x86だと、
mov $フレームのアドレス, %ecx jmp 関数のアドレス
こういうプログラムをスタックの上に生成して、それを関数ポインタとするのである。(exec-shieldがあるともうちょっと色々あるらしいけど)
ちなみに、これは、
typedef int (*func_t)( int );
func_t dump_closure( func_t f)
{
int i;
unsigned char *p = (unsigned char*)f;
for ( i=0; i<10; i++ ) {
putchar( p[i] );
}
return f;
}
func_t
retfunc( int x )
{
int a = 4;
int hogehoge( int b ) { return b + a + x; }
dump_closure( hogehoge );
}こういう感じのプログラムを書いて、
$ ./a.out > out $ objdump -m i386 -b binary -D out out: ファイル形式 binary セクション .data の逆アセンブル: 00000000 <.data>: 0: b9 08 f6 93 bf mov $0xbf93f608,%ecx ; これがフレームのアドレスね 5: e9 f0 8d 70 48 jmp 0x48708dfa ; これが関数へのジャンプ
こうやれば見られる。
で、ここまでが前提知識。続いて、クロージャを実装する。
GCC3.4とかでは無理だったのだけど、GCC4.xだと、このトランポリンを作るときのスタック構造が、
| | +--------+ | local | <- mov xxx, %ecx で %ecxに入れられる値 +--------+ | local | +--------+ | .... | . . +--------+ |mov | <- mov $0xbf93f608, %ecx + + | 0x08 | + + | 0xf6 | + + | 0x93 | + + | 0xbf | +--------+ |jmp | <- jmp 0x48708dfa + + | 0xf0 | + + | 0x8d | + + | 0x70 | + + | 0x48 | +--------+ . ..... . | ..... | <- %ebp +--------+
こんな感じになってるのである。(必ずこうなるのかどうかは調べてない)
これがあれば、ネストされた関数が使うフレームの大きさがわかる。
ここまでくれば、答えは見えたも同じだ。
struct trampoline_code /* トランポリンのコード */
{
char mov_ecx;
unsigned char *frame_start; /* フレームのアドレスはここに */
char jmp_func;
unsigned char *func_offset; /* ジャンプ先のアドレスはここに */
} __attribute__((packed)) ;
func_t
make_closure( func_t f ) /* f がトランポリンの先頭アドレス */
{
struct trampoline_code *src = (struct trampoline_code*)f;
unsigned char *tramp_start = (unsigned char*)src;
unsigned char *frame_start = src->frame_start;
/* フレームは、フレームの先頭とトランポリンの先頭の間 */
unsigned int frame_size = tramp_start-frame_start;
unsigned char *off;
unsigned char *frame = malloc( frame_size );
struct trampoline_code *dst = malloc( sizeof(struct trampoline_code) );
/* フレームの値をコピー */
memcpy( frame, frame_start, frame_size );
/* トランポリンのコピー */
dst->mov_ecx = -71;
dst->jmp_func = -23;
dst->frame_start = frame;
off = (unsigned int)src->func_offset + (unsigned char*)src;
dst->func_offset = off - (unsigned int)dst;
return (func_t)dst;
}これで、クロージャができる!!
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef int (*func_t)( );
struct trampoline_code
{
char mov_ecx;
unsigned char *frame_start;
char jmp_func;
unsigned char *func_offset;
} __attribute__((packed)) ;
func_t
make_closure( func_t f )
{
struct trampoline_code *src = (struct trampoline_code*)f;
unsigned char *tramp_start = (unsigned char*)src;
unsigned char *frame_start = src->frame_start;
unsigned int frame_size = tramp_start-frame_start;
unsigned char *off;
unsigned char *frame = malloc( frame_size );
struct trampoline_code *dst = malloc( sizeof(struct trampoline_code) );
memcpy( frame, frame_start, frame_size );
dst->mov_ecx = -71;
dst->jmp_func = -23;
dst->frame_start = frame;
off = (unsigned int)src->func_offset + (unsigned char*)src;
dst->func_offset = off - (unsigned int)dst;
return (func_t)dst;
}
void
dump_result(void)
{
func_t func[10];
int i;
for ( i=0; i<10; i++ ) {
int n = i*i;
int hogehoge( ) { return n + 30; }
func[i] = make_closure( hogehoge );
}
for ( i=0; i<10; i++ ) {
printf("%d\n",func[i]());
}
}
int main()
{
dump_result();
return 0;
}だからどうした!!
TODO: 引数に対応する。(しません)