続き。
全体像はつかめてきたんだけど、なんか予想をはるかに越える量になりそうな気が。こんなボリュームになる予定ではなかったんだけど…


と、いうわけで、まずは、テーブル検索が最初だろう、という話だった。
unwind-dwarf2環境下では、unwindは、_Unwind_RaiseExceptionで行われる。これの実装はgcc/unwind.incにある。(これ、実は、_Unwinid_SjLj_RaiseExceptionと同じ実装なんだけど、uw_init_contextとuw_update_context、uw_install_contextを置き換えて、動作が変わるようにしてある。)


と、いうわけで、_Unwind_RaiseException追っていけば、テーブル検索してる部分も見つかるだろう。


で、まず、RaiseExceptionでは最初にuw_init_contextしてるので、テーブル検索してるのなら、ここらへんでまずしてそうな感じだ。実装は、gcc/unwind-dw2にある。

#define uw_init_context(CONTEXT)					   \
  do									   \
    {									   \
      /* Do any necessary initialization to access arbitrary stack frames. \
	 On the SPARC, this means flushing the register windows.  */	   \
      __builtin_unwind_init ();						   \
      uw_init_context_1 (CONTEXT, __builtin_dwarf_cfa (),		   \
			 __builtin_return_address (0));			   \
    }									   \
  while (0)

builtin関数を使い過ぎ、のような気がするけど、unwind-dwarf2は、builtin関数使いまくってるので、この程度でへこんではいけない。いや、わからなくてへこむよ。
__builtin_unwind_init…はよくわからん。フレームを作って、レジスタを保存するのだろうか、SPARCレジスタウィンドウ保存とか書いてるので、まあ、なんかCPU依存のなんかなのかも。__builtin_dwarf_cfa…さっぱりわからん。__builtin_return_addressは、わかるよ。リターンアドレスを調べる。


ここらへんはあんまりテーブルとは関係無さそう。続いて、uw_init_context_1。

static void
uw_init_context_1 (struct _Unwind_Context *context,
		   void *outer_cfa, void *outer_ra)
{
  void *ra = __builtin_extract_return_addr (__builtin_return_address (0));
  _Unwind_FrameState fs;
  _Unwind_SpTmp sp_slot;

  memset (context, 0, sizeof (struct _Unwind_Context));
  context->ra = ra;

  if (uw_frame_state_for (context, &fs) != _URC_NO_REASON)
    abort ();

__builtin_extract_return_addr…よくわからん。expand_builtin_extract_return_addrの実装を見る限り、なんかマシンによっては、リターンアドレスを細工しないといけないようなものがあるのかもしれない。僕はあんまりそういうのは知らないのでわからん。RETURN_ADDR_OFFSETがSPARCで定義してあったり、ARMでMASK_RETURN_ADDRがあったり。


めんどうになってきた、というか、このペースだと終わらないので、答えを書いてしまうと、

static _Unwind_Reason_Code
uw_frame_state_for (struct _Unwind_Context *context, _Unwind_FrameState *fs)
{
  const struct dwarf_fde *fde;
  const struct dwarf_cie *cie;
  const unsigned char *aug, *insn, *end;

  memset (fs, 0, sizeof (*fs));
  context->args_size = 0;
  context->lsda = 0;

  if (context->ra == 0)
    return _URC_END_OF_STACK;

  fde = _Unwind_Find_FDE (context->ra - 1, &context->bases);

この、_Unwind_Find_FDEが、それ。glibc環境下での実装は、unwind-dw2-fde-glibcにある。プログラムカウンタから対応するFDEを探してくる。
FDEと、いうのは、DWARF2用語で、Frame Descriptor Entryのことらしい。と、いうわけで、ここから、DWARF2の話が入ってきたりするので、適宜DWARF2仕様を開いておくとよいかもしれない。見なくてもよいかもしれない。


で、前回のソースをコンパイルした結果の中に、

.LSFDE1:
	.long	.LEFDE1-.LASFDE1
.LASFDE1:
	.long	.LASFDE1-.Lframe1
	.long	.LFB3
	.long	.LFE3-.LFB3
	.uleb128 0x4
	.long	.LLSDA3
	.byte	0x4
	.long	.LCFI0-.LFB3
	.byte	0xe
	.uleb128 0x8
	.byte	0x85
	.uleb128 0x2
	.byte	0x4
	.long	.LCFI1-.LCFI0
	.byte	0xd
	.uleb128 0x5
	.align 4
.LEFDE1:

こんなのがあったはずだ。名前からしてFDEっぽいように見える。そんで、その中に、LLSDA3というシンボルが見える。前回は、「LLSDA3は、例外テーブルっぽいよね」という話だった。と、いうわけで、正解のような感じだ。「PCからFDE。FDEから例外テーブル」と、いう流れが見えたわけだ。


が、しかし、また、この、_Unwind_Find_FDEがよくわからんのであった。終わり。「dl_iterate_phdrしてるよ」っていう前回の嘆きがここらへん。


いや、そんなことでめげないよ。その前に、多分関連してるであろう問題を片付けておく。
と、いうのは、gccコンパイルすると、リンカと手を組んで反則技を使ってるのではないか、という話だ。リンカの話になるので、細かい話が必要無い人は続きを読まないこと。

$ gcc -v frame.cpp 
/usr/lib/gcc-lib/i686-pc-linux-gnu/3.3.6/specs から spec を読み込み中

...

 /usr/lib/gcc-lib/i686-pc-linux-gnu/3.3.6/../../../../i686-pc-linux-gnu/bin/as -V -Qy -o /tmp/cc5IhnvT.o /tmp/ccaoadW6.s
GNU assembler version 2.15.92.0.2 (i686-pc-linux-gnu) using BFD version 2.15.92.0.2 20040927
 /usr/lib/gcc-lib/i686-pc-linux-gnu/3.3.6/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 /usr/lib/gcc-lib/i686-pc-linux-gnu/3.3.6/../../../crt1.o /usr/lib/gcc-lib/i686-pc-linux-gnu/3.3.6/../../../crti.o /usr/lib/gcc-lib/i686-pc-linux-gnu/3.3.6/crtbegin.o -L/usr/lib/gcc-lib/i686-pc-linux-gnu/3.3.6 -L/usr/lib/gcc-lib/i686-pc-linux-gnu/3.3.6/../../../../i686-pc-linux-gnu/lib -L/usr/lib/gcc-lib/i686-pc-linux-gnu/3.3.6/../../.. /tmp/cc5IhnvT.o -lgcc -lgcc_eh -lc -lgcc -lgcc_eh /usr/lib/gcc-lib/i686-pc-linux-gnu/3.3.6/crtend.o /usr/lib/gcc-lib/i686-pc-linux-gnu/3.3.6/../../../crtn.o

ここで、非常にわかりにくいんだけど、"--eh-frame-hdr"という引数が見えないこともない。--eh-frame-hdrは、「ExceptionHandling Frame Header」っぽいような。

$ ld --help
Usage: /usr/bin/ld [options] file...
...
  --eh-frame-hdr	Create .eh_frame_hdr section
...

…と、いう感じで、ldは無関係というわけではなさそうだ。もう行き詰まってる感があるので、リンカにやつあたりをしよう。


と、いうわけで、binutilsのソースを調べはじめる。ここらへんで、「自分は何やってんだ…」って気分になってくる。…いや、ほんとに。(あー、けど、こういうのは無駄に楽しいので、あんまりよろしくない。)
面倒なので適当に。ld/emultempl/elf32.emに、

    case OPTION_EH_FRAME_HDR:
      link_info.eh_frame_hdr = TRUE;
      break;

こういうのがある。bfd/elflink.cに

  if (info->eh_frame_hdr)
    {
      if (! _bfd_elf_write_section_eh_frame_hdr (abfd, info))
	goto error_return;
    }

こういうのがある。んで、こいつの実装は、bfd/elf-eh-frame.cだ。
ヤケクソ気味に動作を追っていこう。もちろんここで、動作の予想をしておくのが重要だ。これまでの情報

  • eh_frameは検索に使う
  • 検索はプログラムカウンタで行う
  • もちろん検索は高速でなくてはいけない
  • FDEというのがある。
  • eh_frame_hdrをつくる

から、その動作は、「FDEを二分探索するために、PCでソートしたような感じのeh_frame_hdrをつくる」ではないか、と思われる。
ソース中に、qsortの文字が見えるので、その確率は高い。


とかかっこいいフリをしつつ、よくわからんので、だーっと飛ばしていって…
いや、嘘です!!全然わかりません!!
以下は勘です。


多分、リンクするときに、elf_link_input_bfdがどっかから呼ばれる。このとき、ELF_INFO_TYPE_EH_FRAMEだと、bfd_elf_write_section_eh_frameにいって、ここで、なんか。あー中でも.eh_frame探してるのが謎。

 for (ent = sec_info->entry; ent < sec_info->entry + sec_info->count; ++ent)
    {

.eh_frameにあるエントリごとのループ。

      else
	{
	  /* FDE */
	  bfd_vma value, address;
	  unsigned int width;

このへんの部分がFDEエントリの処理。GNUなコードはたまに関数が糞長いので嫌になる。などと愚痴をこぼしてライブ感を漂わせつつ、

	  if (hdr_info)
	    {
	      hdr_info->array[hdr_info->array_count].initial_loc = address;
	      hdr_info->array[hdr_info->array_count++].fde
		= sec->output_section->vma + ent->new_offset;
	    }

これが、多分、重要。arrayに、FDEのアドレスとそのFDEが指し示すPCのアドレスを埋めてるのがポイント。
eh_frame中のFDEのアドレスと、そのFDEが示すPCのペアをを全部arrayに詰めこんでるわけだ。


んで、_bfd_elf_write_section_eh_frame_hdrを見ていく。ここで、qsort。話は繋がったような。

      qsort (hdr_info->array, hdr_info->fde_count, sizeof (*hdr_info->array),
	     vma_compare);

vma_compareが、

static int
vma_compare (const void *a, const void *b)
{
  const struct eh_frame_array_ent *p = a;
  const struct eh_frame_array_ent *q = b;
  if (p->initial_loc > q->initial_loc)
    return 1;
  if (p->initial_loc < q->initial_loc)
    return -1;
  return 0;
}

当たり。だと思う。

      for (i = 0; i < hdr_info->fde_count; i++)
	{
          /* eh_frame_hdrの先頭とFDEが対象とするアドレスのオフセット */
	  bfd_put_32 (abfd,
		      hdr_info->array[i].initial_loc
		      - sec->output_section->vma,
		      contents + EH_FRAME_HDR_SIZE + i * 8 + 4);

          /* eh_frame_hdrの先頭とFDEのオフセット */
	  bfd_put_32 (abfd,
		      hdr_info->array[i].fde - sec->output_section->vma,
 		      contents + EH_FRAME_HDR_SIZE + i * 8 + 8);
	}

というわけで、結論。

.eh_frame_hdrセクションには、

  • アドレスとFDEを関連付けるデータが載ってて、
  • それがアドレスでソートされている

これによって、プログラムカウンタからeh_frame_hdrを二分探索することでFDEを探すのが可能になる。というわけだ。


void bar(){ throw 4; }
int foo()
{
  try {
    bar();
  } catch ( const char *msg ) {
    return 0x8888;
  }
  return 0;
}
int main() {}

こんなコードを、

$ g++ nanika.cpp
$ objdump --section=.eh_frame_hdr -s a.out
$ nm a.out

こんな感じにして、

 8048784 011b033b 18000000 02000000 70feffff  ...;........p...
 8048794 38000000 a0feffff 58000000           8.......X...    

多分こんな感じのが得られるはず。最初の3wordは多分ヘッダ。リトルエンディアンで、

pc-offset  fde-offset
fffffe70   00000038
fffffea0   00000058

さらに、eh_frame_hdrの先頭が、0x8048784より。

0x8048784 + ffffe70 = 80485f4
0x8048784 + ffffea0 = 8048624

nmの結果より。

080485f4 T _Z3barv
08048624 T _Z3foov
0804866c T main

こんな感じで。


明日に引っぱるのもなんなので、
unwind-dw2-fde-glibc.cより、それっぽいところ、

	    {
	      lo = 0;
	      hi = mid;

	      while (lo < hi)
		{
		  mid = (lo + hi) / 2;
		  if (data->pc < table[mid].initial_loc + data_base)
		    hi = mid;
		  else if (data->pc >= table[mid + 1].initial_loc + data_base)
		    lo = mid + 1;
		  else
		    break;
		}

	      if (lo >= hi)
		__gxx_abort ();
	    }

が、見つかった、ということで、_Unwind_Find_FDE は、解決したものとしておく。いや、もうちょっとちゃんとやるべきなんだろうけど…


で、テーブルの探索はできたので、あとは…いもづる式に解決していく…といいな。