■
昨日の話で、(少なくとも僕は)unwindの方法は理解できたわけだが、C++の例外はunwindするだけでは実現できない
- 例外ハンドラを選ぶのは?
- どうやって例外を投げる?
- 投げたオブジェクトはどこへ?
など、もうちょっと色々調べないと、例外を理解したとはいえないだろう。と、いうわけで、もうちょっと考える。
けど、ここらへんの事情は、実は、http://www.codesourcery.com/cxx-abi/abi-eh.htmlに大体書いてあるので各自読んでおくこと。おわり。
いや!!そんな、実は面倒になってきたとか、そんな、そんな感じ…ですょ?
いや、終わりじゃないですよ!!と、いうのはいいとして、昨日は面倒になったので、書かなかったんだけど、Unwind_SjLj_Registerするときには、jmp_bufだけでなくて、クリーンアップルーチンやら、例外ハンドラ検索ルーチンやらをいっしょにした、personalityという関数も一緒にチェーンにつないでいる。昨日はなんかわからんとか書いてた、__gxx_personality_sj0というやつだ。
_Unwind_SjLj_RaiseException内でチェーンをたどっていくときに、このpersonalityを呼んで、正しいクリーンアップと、例外を捕まえる/捕まえないの判定をしてる、というわけだ。そんで、その、personality関数は、次のような型になっている
_Unwind_Reason_Code (*__personality_routine) (int version, _Unwind_Action actions, uint64 exceptionClass, struct _Unwind_Exception *exceptionObject, struct _Unwind_Context *context)
だーっと説明していく。
まず version。これは今のところ 1 としなければならない。
次にaction。これは、どういう目的でpersonalityを呼び出したか、が渡される。これは、unwind.hの、
#define _UA_SEARCH_PHASE 1 #define _UA_CLEANUP_PHASE 2 #define _UA_HANDLER_FRAME 4 #define _UA_FORCE_UNWIND 8 #define _UA_END_OF_STACK 16
を、なんかbit orしたもの。詳細は、よくわからんが。
次に、exceptionClass。これは、なんかベンダの名前4byteと言語の名前4byteを合わせたもの。g++の場合、"GNUC" "C++\0"となっている。
次に、context。なんかコンテキスト。これはよくわからん。
ここまでわかれば、投げられた型を見て、「catchする/しない」の判定くらいはなんとかなりそうな気がしてきた。いや、無理だった。
次に、g++での、例外について調べる。g++では、ここまで見てきた、unwindのしくみの上でなんだかごにょごにょして、C++の例外を実現してるわけだ。
extern "C" void throw_charptr() { throw "nanika"; }
こーいうコードをコンパイルすると、
.file "throw.cpp" .section .rdata,"dr" LC0: .ascii "nanika\0" .text .align 2 .globl _throw_charptr .def _throw_charptr; .scl 2; .type 32; .endef _throw_charptr: pushl %ebp movl %esp, %ebp subl $24, %esp movl $4, (%esp) call ___cxa_allocate_exception movl $LC0, (%eax) L2: movl $0, 8(%esp) movl $__ZTIPKc, 4(%esp) movl %eax, (%esp) call ___cxa_throw L1: .def ___cxa_throw; .scl 3; .type 32; .endef .def ___cxa_allocate_exception; .scl 3; .type 32; .endef
こーんな感じになる。__cxa_throwといういかにもそれっぽいサブルーチンを呼んでるのがわかる。これの実体は、libstdc++のlibsupc++/eh_throw.ccにある。
extern "C" void __cxxabiv1::__cxa_throw (void *obj, std::type_info *tinfo, void (*dest) (void *)) { __cxa_exception *header = __get_exception_header_from_obj (obj); header->exceptionType = tinfo; header->exceptionDestructor = dest; header->unexpectedHandler = __unexpected_handler; header->terminateHandler = __terminate_handler; header->unwindHeader.exception_class = __gxx_exception_class; header->unwindHeader.exception_cleanup = __gxx_exception_cleanup; #ifdef _GLIBCXX_SJLJ_EXCEPTIONS _Unwind_SjLj_RaiseException (&header->unwindHeader); #else _Unwind_RaiseException (&header->unwindHeader); #endif // Some sort of unwinding error. Note that terminate is a handler. __cxa_begin_catch (&header->unwindHeader); std::terminate (); }
スタック操作から考えて、引数は、obj = "nanika\0", tinfo = &_ZTIPKc, dest = NULL。になるのだろう。
で、_ZTIPKcってなんじゃい、っていうのは、"const char *"のtype_infoだと思っていただければよいかと。
"TI"がtype_info、"c"がchar、"K"がconst修飾子で、Pがポインタ。で、TIPKcで"const char へのポインタ"となるわけだ。ここらへんは、C++名前マングリングルールでも参照。。$ c++filt _ZTIPKc でもよいけど、普通の奴らの下をいくC++プログラマならば、名前マングリングぐらい脳内でできるようになるべきだろう。そんなわけないけど。
話が少しずれてしまった…いや、どうせずれまくってんだし、そのぐらいいいんだけど、そんなことより!ここで重要なことは、例外を投げるときに、一緒に型情報も投げているという事実だ。型情報も投げてるのなら、捕まえるがわでもそれを利用しているに違いない。
int func3() { try { f(); } catch ( const char *ptr ){ } }
こんなコードがあったら、受け取る側もどっかで、_ZTIPKcをなんかしてるはずだ。
__Z5func3v: ... movl $___gxx_personality_sj0, -40(%ebp) movl $LLSDA2, -36(%ebp) movl $L7, -28(%ebp) call __Unwind_SjLj_Register ... LLSDA2: .byte 0xff .byte 0x0 .uleb128 LLSDATT2-LLSDATTD2 LLSDATTD2: .byte 0x1 .uleb128 LLSDACSE2-LLSDACSB2 LLSDACSB2: .uleb128 0x0 .uleb128 0x1 LLSDACSE2: .byte 0x1 .byte 0x0 .align 4 .long __ZTIPKc
長いので省略しているが、間違いなく、_ZTIPKcのシンボルが見える。そんで、それはLLSDA2というのに入ってて、personalityと一緒に、_Unwind_SjLj_Registerするときにごにょごにょしている。(ちなみに、LSDAは"Language Specific Data Area"の略っぽいよ)
さきほどの、personalityの型は …… 文が思い付かんので省略。……
exceptionObjectに、投げられたオブジェクトの型情報が、contextの中に、_Unwind_SjLj_Registerしたときの型情報が入ってて、personalityはそれを見て、「catchする/しない」を判定しているのだとすれば、なんだか、非常にうまくいってくれそうな感じがする。
昨日はとりあえず例外を捕まえることを目的としてたので、
_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; }
personalityは非常に適当にしていた。けど、なんか、これをうまくやれば、投げられたオブジェクトによって判定を変えるなんてことができそうだ。
では、まず、正しいpersonalityがどうなってるのか調べる。これの定義は、libstdc++/libsupc++/eh_personality.ccにある。長いので載せないけど。
その中で、get_adjusted_ptrを呼んでいるところがあるはずだ。多分これが、キャッチできるできないの判定っぽい。
static bool get_adjusted_ptr (const std::type_info *catch_type, const std::type_info *throw_type, void **thrown_ptr_p) { void *thrown_ptr = *thrown_ptr_p; // Pointer types need to adjust the actual pointer, not // the pointer to pointer that is the exception object. // This also has the effect of passing pointer types // "by value" through the __cxa_begin_catch return value. if (throw_type->__is_pointer_p ()) thrown_ptr = *(void **) thrown_ptr; if (catch_type->__do_catch (throw_type, &thrown_ptr, 1)) { *thrown_ptr_p = thrown_ptr; return true; } return false; }
って、あー、__do_catchがC++メソッドっぽいので、C++の力を頼らないと無理じゃないのか?いや、まあ、そんなわけなくて、名前マングリングとvtblを極めた本物のC++プログラマならば、C++メソッドをCから呼ぶくらい日常茶飯事だよ。まあ、僕は、C++プログラマではないので無理だけど。というか、そろそろ今日のやる気が無くなってきたので、personalityを実現するのは諦めよう。
あー、いや、なんか無理。アレをアレするにはLSDAのフォーマットを理解しないといけなくて、もう気分的に無理だ。いい加減、誰も読まないような気がしてきたしねっ!!
というわけで、次回、もはやどう続くのかわからなくなってきたぐらいだけど続きます!!unwind-dwarf2の解明まではやらないとすっきりしないので。