g++の例外を捕まえよう!!(素手で)

最近そういうのが流行ってるらしいので。調べながら書くのを久し振りにやってみたい感じ。
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++ とかして、実行してやると、例外が捕まえられる。これぞ、衝撃!素手で例外を捕まえる瞬間だ!!


あんまり、こういうことにばっかり、コンピュータを使うのはよくない。
超グダグダなんだけど、次回、例外を投げる編に続く。無理かもしれない!!