色々…色々…いや、なんか、いいのかな…
■
続き。
さて。まず、unwind-dwarf2な環境でg++の例外に立ち向かって、最初にぶつかる壁は、「そもそもどうやってハンドラのアドレスを調べてるのかわからない…」というところだ。
void bar(); int foo() { try { bar(); } catch ( const char *p ) { return 0xffff; } return 0; }
こんなコードをコンパイルしてみる。
_Z3foov: .LFB3: pushl %ebp .LCFI0: movl %esp, %ebp .LCFI1: .LEHB0: call _Z3barv .LEHE0: jmp .L2 ;; 無条件ジャンプ…? ;; こっから下が例外ハンドラ? .L7: decl %edx je .L3 pushl %eax .LEHB1: .LCFI2: call _Unwind_Resume .LEHE1: .L3: pushl %eax call __cxa_begin_catch .LCFI3: call __cxa_end_catch movl $65535, %eax ; 0xffff jmp .L1 .L4: .L2: xorl %eax, %eax .L1: leave ret .LFE3:
と、こうなる。ここで不思議なことは、例外ハンドラの先頭であるL7は参照されていないかのように見える、ということだ。(実際には、下のほうのなんかよくわからないところで、L7が使われてるんだけど)
はたして、これで本当に例外処理は正しく動くのか?
unwind-SjLjな環境では、例外ハンドラが必要になる度に、jmp_bufをチェーンしていた。これは、わかる。Windowsで使われる、SEHと呼ばれるものだって、ただのチェーンだ。それも同じだろう。
だがしかし、何も準備しないで、例外ハンドラを見つけられるというのは、理解に苦しむ。
と、いうあたりが、前回までの僕で辿り付けた限界だった。しかし、まあ、なんだかんだあって、不可能ではないことはわかってきたので多分大丈夫。
まず、例外ハンドラを見つける方法でよく使われるのが、例外テーブルを使う方法だ。これを読めばよいだろうか。(←あんまりちゃんと読んでない奴)
簡単に説明すると、その時のプログラムカウンタの値から、例外ハンドラの対応が書いてあるテーブル、といった感じ。
見やすい例外テーブルが見てみたいなら、Javaが良いかもしれない。
class HogehogeException extends Exception { } class C { static void m() throws HogehogeException { } } class Nanika { int x; void m() { try { try { C.m(); } catch ( HogehogeException e ) { } x = 4; } catch ( Exception e ) { } } }
void m(); Code: 0: invokestatic #2; //Method C.m:()V + from 0 3: goto 7 + to 3 6: astore_1 7: aload_0 8: iconst_4 9: putfield #4; // Field x:I 12: goto 16 + to 12 15: astore_1 16: return Exception table: from to target type 0 3 6 Class HogehogeException // 0-3 のどっかで例外が発生して、それが // HogehogeExceptionだったら 6 へジャンプ 0 12 15 Class java/lang/Exception // 0-12 のどっかで例外が発生して、それが // Exception だったら 15 へジャンプ
なーんとなく、わかっていただけるだろうか。
実に単純な話、「プログラムカウンタがxxの時に例外が発生したら、このハンドラで対応する」っていうのが、まさにそのまま書いてあるっていうわけだ。
この方法の利点は、「tryに入っても例外が発生しないかぎり、実行時のコストはほぼ0」という、効率の良さにある。実際は、命令キャッシュやら、例外テーブルの分のキャッシュやらで、0にはならないだろうけど、毎回何かをチェーンにつないだりはずしたりしていくのに比べれば、そのコストは非常に小さいものになるはずだ。
と、いうわけで、上のg++のコンパイル結果を見る限り、g++の例外処理も例外テーブルを用いた実装になっているような気がする、というか、それ以外には考えられない。
さきほど見つからなかったL7も、きっと、例外テーブルに含められているのだろう。
.size _Z3foov, .-_Z3foov .section .gcc_except_table,"a",@progbits ; いかにもそれっぽいセクション名 .align 4 .LLSDA3: .byte 0xff .byte 0x0 .uleb128 .LLSDATT3-.LLSDATTD3 .LLSDATTD3: .byte 0x1 .uleb128 .LLSDACSE3-.LLSDACSB3 .LLSDACSB3: .uleb128 .LEHB0-.LFB3 .uleb128 .LEHE0-.LEHB0 .uleb128 .L7-.LFB3 ; L7 はここに。 .uleb128 0x1 .uleb128 .LEHB1-.LFB3 .uleb128 .LEHE1-.LEHB1 .uleb128 0x0 .uleb128 0x0 .LLSDACSE3: .byte 0x1 .byte 0x0 .align 4 .long _ZTIPKc
いかにもそんな感じがする。テーブルの読み方は全然わからないけど…
と、いうわけで、ハンドラを見つけるのは、不可能ではないような気がしてきた、というわけだ。ただし、今の理解では、ふたつの問題がある。
- スタックとプログラムカウンタはどうやって巻き戻すの?
- 広大なメモリ空間からどうやって例外テーブルを見つけるの?
Javaの場合では、VM作る人がフレーム構造勝手に決めれるので、巻き戻しはどうにでもできるし、例外テーブルはメソッドごとにくっついてくるので、例外テーブルを見つけるのは難しくはない。(JITの場合は知らないが)
だがしかし、C++の場合では、フレーム構造は大体決まってるとはいえ、保証することはできないし、例外テーブルはプログラム全体でまるごといっこのセクションになってるので、そう簡単に見つかりそうな気がしない。Javaと同じ、というわけにはいかないのである。
フレーム構造のほうは、-fomit-frame-pointerオプションを付けても、無理矢理フレームを作ってるんじゃないか、と、予想したんだけど、そうではないようだ。-fomit-frame-pointerオプションは正しく働いている。
と、まあ、ここで悩むんだけど、少し考えれば、プログラムカウンタからなんかテーブルを調べられるんだったら、そのテーブルに、「プログラムカウンタ←→スタック巻き戻し情報」っていうのを載せてしまえばいいんじゃないか、というのに気が付くだろう。
つまり、重要な問題は、ただひとつに、絞られるわけだ。
「プログラムカウンタから、対応したテーブルを拾ってくるには、どうすればよいのか。」
これさえわかれば、あとは、なんか、いもづる式に問題が解決していくような気がしないでもない。しないかもしれない。
と、いうのが、今日まで考えた分。
続きます。
無駄にひっぱるのは、今日はここから先は調べてないから。ギリギリの状態で書いてますよ。
■
ちょっと、なんか、dl_iterate_phdrして探してるんですけど…