続き。結構飽きてきた感じが。大体見当が付いてしまうと、どうでもよくなってくるんだよな…
あと、僕があまり理解してないとかで、今日のは読んで理解できるようなレベルではないです。適当に流して。
PCから、FDEが拾ってこれる、という話だった。
このFDEというのはなんなのだろうか。と、いうのはDWARF2仕様の62ページあたりに書いてある。
gcc に -dA オプションを付けるとコメントが付くので、色々見てみるとよいかもしれない。
.LSFDE1: .long .LEFDE1-.LASFDE1 # FDE Length .LASFDE1: .long .LASFDE1-.Lframe1 # FDE CIE offset .long .LFB3 # FDE initial location .long .LFE3-.LFB3 # FDE address range .uleb128 0x4 # Augmentation size .long .LLSDA3 # Language Specific Data Area .byte 0x4 # DW_CFA_advance_loc4 .long .LCFI0-.LFB3 .byte 0xe # DW_CFA_def_cfa_offset .uleb128 0x8 .byte 0x85 # DW_CFA_offset, column 0x5 .uleb128 0x2 .byte 0x4 # DW_CFA_advance_loc4 .long .LCFI1-.LCFI0 .byte 0xd # DW_CFA_def_cfa_register .uleb128 0x5 .align 4
あと、これも参考になる。っていうか、微妙にこれに書いてあるからもういいんじゃないかな…
いや、ええと、ここで、まあ、こんなことは今更書くまでもない常識だとは思うけど、DWARF2はチューリング完全。
と、いうのは、アレです。アレ。「チューリング完全」って言ってみたかっただけです。
ここらへんで、「プログラミング言語DWARF2」というネタをやろうやろうと思って、三日ぐらい考えてたんだけど、話の持って行きかたが思い付かなかったとか、まあ、そんな話があって、うまくやれば非常にくだらないネタになりそうな気がするので誰か書かないですか。まあ、そこらへんの話は、また気が向いた時に書くことにします。書かないかもしれません。
まあいいか。なんかよくわからんけど、DWARF2はスタックマシンなプログラムを実行する程度の能力を持ってるような気がする、という話。
それはどうでもよくて、CIEやFDEがフレームの位置を巻き戻したりするのには、なんかプログラム実行することで行っている。
このなんかプログラムと、そのスタックマシンなプログラムとの関係はよくわからないんだけど、そのことは忘れておく。
とりあえず、
int bar( char * ); int foo() { char a[256]; return bar(a); }
こんなコードをコンパイル。説明の都合上-fomit-frame-pointerする。
.file "frame.cpp" .text .align 2 .globl _Z3foov .type _Z3foov, @function _Z3foov: .LFB3: # basic block 0 subl $284, %esp .LCFI0: leal 16(%esp), %eax movl %eax, (%esp) call _Z3barPc addl $284, %esp ret .LFE3: .size _Z3foov, .-_Z3foov #APP .section .eh_frame,"a",@progbits .Lframe1: .long .LECIE1-.LSCIE1 # Length of Common Information Entry .LSCIE1: .long 0x0 # CIE Identifier Tag .byte 0x1 # CIE Version .ascii "zP\0" # CIE Augmentation .uleb128 0x1 # CIE Code Alignment Factor .sleb128 -4 # CIE Data Alignment Factor .byte 0x8 # CIE RA Column .uleb128 0x5 # Augmentation size .byte 0x0 # Personality (absolute) .long __gxx_personality_v0 .byte 0xc # DW_CFA_def_cfa // こっからCIEのプログラム。 .uleb128 0x4 .uleb128 0x4 .byte 0x88 # DW_CFA_offset, column 0x8 .uleb128 0x1 .align 4 .LECIE1: .LSFDE1: .long .LEFDE1-.LASFDE1 # FDE Length .LASFDE1: .long .LASFDE1-.Lframe1 # FDE CIE offset .long .LFB3 # FDE initial location .long .LFE3-.LFB3 # FDE address range .uleb128 0x0 # Augmentation size .byte 0x4 # DW_CFA_advance_loc4 // こっから FDEの。 .long .LCFI0-.LFB3 .byte 0xe # DW_CFA_def_cfa_offset .uleb128 0x120 .align 4 .LEFDE1: #NO_APP .section .note.GNU-stack,"",@progbits .ident "GCC: (GNU) 3.3.6 (Gentoo 3.3.6, ssp-3.3.6-1.0, pie-8.7.8)"
こんな感じ。
unwind-dw2.cより、まず、CIEのプログラムが実行されるので、DW_CFA_def_cfa。これは、cfa_regの設定と、cfa_regのオフセット設定。これにどんな意味があるのかはわからない。次に、DW_CFA_offset。指定したレジスタにオフセットを設定しておく。この場合、8番目のレジスタのオフセットを1に設定。けど、なんか、これ、Data Alignment Factorをかけてるようなので、-4。これもどんな意味を持つのかはわからない。これでCIEが終了。実はよくわからない。
続いて、FDEのプログラム実行。DW_CFA_advance_loc4、PCを進める?よくわからん。DW_CFA_def_cfa_offset。CFAレジスタ(これはさっき4番目にしたはず)のオフセットを0x120に設定。
そんで、なんだかんだあって、uw_update_context_1へ。最初のEH_RETURN_STACKADJ_RTX…はよくわからん。
続いて、cfa_howはCFA_REG_OFFSETにしてあるはず。なので、cfa_reg(にしたはず)にオフセット加算。ここでポイントなのは、DWARFのレジスタ4番目はespであること。さっきオフセットは0x120に設定したはず。4番目のレジスタを0x120足すっていうことは、すなわち、スタックを一個戻すっていうこと。ここで巻き戻しが行われてるわけ。
/* Compute the addresses of all registers saved in this frame. */ for (i = 0; i < DWARF_FRAME_REGISTERS + 1; ++i)
んで、このループ。全レジスタのオフセット加算とか。ここで、さきほど、8番目のレジスタにオフセットを設定したのがここで反映される。CFA-4が、8番目のレジスタの値。
それで、uw_update_context_1は抜けて、uw_update_context。CIEで、RA Columnが8に設定してあるのがポイント。fs->retaddr_columnが8。で、8番目のレジスタは、CFA-4。これによって、戻りアドレスが掘り起こせるわけだ。
もう、もはや、何を言ってるのかわかんないな。さすがにこの文で理解できた人はちょっと僕のことを汲み取りすぎだと思う。いや、僕も書きながらようやく今になって理解できたので、最後の一文以外は何もわからずに書いてたりしたので、なんだそれ。
大体理解できたところで、もう一回説明しておく。
CFAはおそらくCurrent Frame Addressのこと(Call Frame Addressかも)。こいつが、この関数内のフレームのアドレスを指してる。
まず、CIEで、eip等がフレームからどの位置に埋められているかを保存しておく。フレーム先頭からeipが埋まってる位置のアドレスまでのオフセットは、よほどのことが無いかぎり変化しないだろう、また、大抵の関数で、この値は変わらないはずだ。こういうように、オフセットが変化しないであろうものはCIEに入れておく。おそらく、これは、スペース削減のため。
int baz(); int bar( char * ) { return baz(); } int foo() { return baz(); }
こうなっていた場合でも、CIEはbarとfooで共有されているはず。
んで、FDEにはesp、もしくはebpの位置を埋めておく。こいつらは、実行中の関数やアドレスによって、変化する可能性がある。そのため、こいつらのオフセットは実行アドレスと関連付けられたFDEに埋めておく。
つまり、CIEのプログラムには共有できる部分を、FDEのプログラムには、専用の部分を入れておく、ということだ。
これで、実行アドレスから、FDE、FDEからCIE、CIEから「フレームとEIPの間のオフセット」、んで、FDEからもういっこ「現在のESPとフレームの先頭のオフセット」。が、拾える。つまり、PCから、スタックの巻き戻しを行えるようになる、というわけだ。
http://d.hatena.ne.jp/w_o/20051106#p1で書いたthrow.cがunwind-dwarf2な環境で動かなかった理由は、ずばり、FDEとCIEを持ってなかったから、ここから先は巻き戻しできなかったから、だとか、そんな理由なのだろう。多分。
あと、これで巻き戻しはできるんだろうけど、最初のCFAとPCはどうやって拾ってくるのか、っていう問題がある。んだけど、これは、多分、前回、謎だとしていた、
#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_dwarf_cfaがCFA。__builtin_return_addressがPC。最初のフレームアドレスだけは、コンパイル時に埋めこんでしまうとか、そういう感じで。
と、いうわけで、巻き戻しの実装も大体理解できたはず。(上の文を読んで理解できる人が僕以外に存在するのかは疑問だけど)
次回で終わる予定。