最近そういうのが流行ってるらしいので。調べながら書くのを久し振りにやってみたい感じ。
CでC++の例外を捕まえたり投げたりする話。
いや、この話は理解しようとして、過去に二回挫折してんだけど。そろそろ僕も乗り越えないといけない時期が来たんではないかと。意味がわからんけど。
いや、それはいいか。とにかくC++の例外処理を解明していくのは面白い遊びなんではないかと思う。と、いうわけで、
- unwind-SjLj
- type_info と 例外の実装
- unwind-dwarf2 と 例外の謎と効率とその実装
と、いう順番で話を進めていく。まだ詳しく理解したわけではないので、気分によっては他の話が入ってきたりするかもしれない。そこらへんは、まあ、柔軟な対応ということで。
まず、C++の例外処理は、「大域脱出(unwind)」と、「RTTI」のふたつからなっている。
void func() { try { ... } catch( const Nanika &n ) { ... } }
こういうコードがあった場合、tryの中でthrowすると、なんらかの方法で、catchまで大域脱出。んで、投げられたオブジェクトの型情報を調べて、catchするかどうかの判定。といった感じになる。もちろん、その実装方法については、C++言語仕様では決められてないので、実際はどうなのかはわからない。けど、そんなに大きく変わることはないだろう。
まず、unwindだ、が、g++のunwindの実装には、sjljと呼ばれるものと、dwarf2と呼ばれるものがある。dwarf2のほうが効率がいいけど、実行バイナリのフォーマットに依存するので、使えない環境ではsjljを使う、といった理解でいいと思う。僕の理解が間違っていなければ、だけど。
dwarf2のほうは、最後のネタなのでとっておくとして、sjljによるunwindについて説明する。wineとmingwを用意して。
sjlj、というと、なんか、よくわからんけど、これ、多分、「SetJmp-LongJmp」のことだと思う。いや、そう明記した資料があったわけではないので、なんともいえないんだけど。
あー、なんか文が思い付かないので、結論だけ書いてしまうと、sjljの実装は、ただのjmp_bufのチェーンだ。どっかにjmp_bufをつないどいて、あとはunwindするときに、それを辿っていく、というだけだ。
_Unwind_SjLj_Registerでチェーンをつなぐ。_Unwind_SjLj_Unregisterでチェーンをはずす。_Unwind_SjLj_RaiseExceptionでチェーンをたどってunwindする。
int f(); int func() { try {f();} catch (...) {} }
こういうコードは、
__Z4funcv: pushl %ebp movl %esp, %ebp leal -12(%ebp), %eax pushl %edi pushl %esi pushl %ebx subl $56, %esp movl %eax, -32(%ebp) leal -64(%ebp), %eax movl %esp, -24(%ebp) pushl %eax movl $___gxx_personality_sj0, -40(%ebp) movl $LLSDA2, -36(%ebp) movl $L7, -28(%ebp) call __Unwind_SjLj_Register popl %ecx movl $1, -60(%ebp) call __Z1fv jmp L2 L7: L3: L4: addl $12, %ebp pushl -56(%ebp) call ___cxa_begin_catch popl %edx movl $-1, -60(%ebp) call ___cxa_end_catch L2: L1: leal -64(%ebp), %eax pushl %eax call __Unwind_SjLj_Unregister popl %eax movl -68(%ebp), %eax leal -12(%ebp), %esp popl %ebx popl %esi popl %edi popl %ebp ret
大体こんな感じになる。SjLj_RegisterとSjLj_Unregisterの対があるのがわかるだろう。
ちなみに、SjLj_Registerの実装はGCCのソース中のgcc/unwind-sjlj.cにある。
void _Unwind_SjLj_Register (struct SjLj_Function_Context *fc) { #if __GTHREADS if (use_fc_key < 0) fc_key_init_once (); if (use_fc_key) { fc->prev = __gthread_getspecific (fc_key); __gthread_setspecific (fc_key, fc); } else #endif { fc->prev = fc_static; fc_static = fc; } }
スレッド対応とかでごちゃごちゃなってるけど、fc->prev=fc_static; fc_statick=fc; で何か(SjLj_Function_Context)をチェーンしてるのは大体わかるだろう。
んで、そのなんかっていうのが、同じく、gcc/unwind-sjlj.cにある、
struct SjLj_Function_Context { /* This is the chain through all registered contexts. It is filled in by _Unwind_SjLj_Register. */ struct SjLj_Function_Context *prev; /* This is assigned in by the target function before every call to the index of the call site in the lsda. It is assigned by the personality routine to the landing pad index. */ int call_site; /* This is how data is returned from the personality routine to the target function's handler. */ _Unwind_Word data[4]; /* These are filled in once by the target function before any exceptions are expected to be handled. */ _Unwind_Personality_Fn personality; void *lsda; #ifdef DONT_USE_BUILTIN_SETJMP /* We don't know what sort of alignment requirements the system jmp_buf has. We over estimated in except.c, and now we have to match that here just in case the system *didn't* have more restrictive requirements. */ jmp_buf jbuf __attribute__((aligned)); #else void *jbuf[]; #endif };
これ。中身はちょっとわからん部分があるけど、最後にjmp_bufがあるのがわかるだろう。
あー、なんか書くのが面倒になってきたな。以下はやる気無くして書いてるので注意。
jbufは、setjmpは使わない。__builtin_setjmpを使っている。これは多分、glibcやらのsetjmpだと、全レジスタ保存するけど、GCCは自分で全レジスタ保存しなくていいのがわかってるから、まあ、そこらへんの省スペースのためではないかと思う。
int func( jmp_buf *jb ) { __builtin_setjmp( *jb ); }
こういう、コードを書くと、
func: pushl %ebp movl %esp, %ebp pushl %edi pushl %esi pushl %ebx movl 8(%ebp), %eax leal -12(%ebp), %edx movl %edx, (%eax) movl $.L2, %edx movl %edx, 4(%eax) movl %esp, 8(%eax) .L2: popl %ebx popl %esi popl %edi popl %ebp ret
まーなんとなく、保存しなくていいレジスタ(eaxとか)を保存してない感じがする。これのおかげで、tryした場合でも、スタック消費を56バイトに抑えられてるわけだ。
今確かめたglibcの場合だと、jmp_bufの大きさが156になってたので、これはそれなりのアレのソレ。
上のコードを見るに、__builtin_setjmpで使ってる、jmp_bufは、スタックにdi、si、bxを積んで、
struct jmp_buf{ void *スタックのてっぺん; /* 0 */ void *longjmpしたとき次に実行するアドレス; /* 4 */ void *esp; /* 8 */ }
って感じだろうか。あー、スタックのてっぺんとespを保存してるのはそのうち考える。多分そのうち忘れるけど。
と、いうわけで、まあ、なんとなく、上のtryしてる部分でFunctin_Contextを初期化してるのは理解できそうだ。
struct SjLj_Function_Context { struct SjLj_Function_Context *prev; /* -56 */ int call_site; /* -50 */ _Unwind_Word data[4]; /* -44 */ _Unwind_Personality_Fn personality; /* -40 */ void *lsda; /* -36 */ void *jbuf[]; /* -24, -28, -32 */ };
なんかわからんのは飛ばすとして、これを考えると、
__Z4funcv: pushl %ebp movl %esp, %ebp leal -12(%ebp), %eax pushl %edi pushl %esi pushl %ebx subl $56, %esp movl %eax, -32(%ebp) leal -64(%ebp), %eax movl %esp, -24(%ebp) /* ここまで __builtin_setjmpっぽい */ pushl %eax /* Unwind_SjLj_Registerの引数 */ movl $___gxx_personality_sj0, -40(%ebp) /* なんかわからん */ movl $LLSDA2, -36(%ebp) /* なんかわからん */ movl $L7, -28(%ebp) /* longjmpしたあとに実行するアドレス */ call __Unwind_SjLj_Register
こんな感じだろう。よくわからんのが多いけど、例外がSjLjで動いてるのなら、なんか、うまくできそうな気がしてくる。考えるよりも生むが易し。適当に思いつくままにやってみる。
…と、思ったが、RaiseExceptionはもうちょっとややこしいようだ。
- personalityはいる
- personalityがURC_HANDLER_FOUNDを返すまでは巻き戻し続ける。
- personalityは何回か呼ばれる
- _Unwind_Exceptionて?
- よくわからん
うん。大体わかった。しかし、それを書くには、やる気が少なすぎる。
#include <unwind.h> struct SjLj_Function_Context { struct SjLj_Function_Context *prev; /* -56 */ int call_site; /* -50 */ void* data[4]; /* -44 */ _Unwind_Personality_Fn personality; /* -40 */ void *lsda; /* -36 */ void *jbuf[4] __attribute__((aligned)); /* -24, -28, -32 */ }; struct _Unwind_Exception ue; _Unwind_Reason_Code personality(int code, _Unwind_Action act, _Unwind_Exception_Class cls, struct _Unwind_Exception *e, struct _Unwind_Context *ctxt ) { if ( act & _UA_CLEANUP_PHASE ) return _URC_INSTALL_CONTEXT; else if ( act & _UA_SEARCH_PHASE ) return _URC_HANDLER_FOUND; return 0; } int flag = 0; extern void _Unwind_SjLj_Register( struct SjLj_Function_Context *ctxt ); int func() { struct SjLj_Function_Context nanika; nanika.personality = personality; __builtin_setjmp( nanika.jbuf ); nanika.jbuf[1] = &&label; _Unwind_SjLj_Register( &nanika ); cpp_func(); puts("boo"); label: puts("ok"); return 0; } int main() { return func(); }
これを、catch.c
extern "C" void cpp_func() { throw 4; }
これを、throw.cppとして、$ i386-mingw32msvc-gcc catch.c throw.cpp -lstdc++ とかして、実行してやると、例外が捕まえられる。これぞ、衝撃!素手で例外を捕まえる瞬間だ!!
あんまり、こういうことにばっかり、コンピュータを使うのはよくない。
超グダグダなんだけど、次回、例外を投げる編に続く。無理かもしれない!!