おしながき

ELFファイルフォーマット

  • .eh_frameセクションの構造と読み方

DWARFファイルフォーマット

NCURSESライブラリ

  • NCURSES Programing HOWTO ワタクシ的ほんやく
    1. Tools and Widget Libraries
    2. Just For Fun !!!
    3. References
  • その他、自分メモ
  • NCURSES雑多な自分メモ01


最近の更新 (Recent Changes)

2019-09-24
2013-10-10
2013-10-03
2013-10-01
2013-09-29
目次に戻る:DWARFファイルフォーマット

.debug_frameセクションの構造:INTEL64(amd64)での例の解析(その1)

さて、.debug_frameセクションですが、困ったことに日本語の解析文書はWeb上では見付からないどころか、英語でも例を出しているものがほとんどありません。。。。 しかも、DWARF3のサンプル(appendix D.6)は、motorola 88000だったります。。。(もはや、motorolaの石は秋葉ですら見掛けません。。。)

ということで、INTEL64(amd64)での例が欲しかったんで、やってみました。

ネタとしたソース

今回の解析のためネタにしたソースは、以下です。(ネタなので、ヘタとか汚いとか言わないよーに!)

/* dwarftest2.c */
#include<stdio.h>

int             a;

""void func2( int *b )""    {
        *b = 3;

        return;
}

int func1( int a, int *b)       {
        int             c;

        func2( b );
        c = a + *b;

        return c;       
}

int main( int argc, char *argv[])       {
        int             b;
        int             c;

        a = 1;
        c = func1( a, &b );
        printf("a=%d, b=%d, c=%dn", a, b, c);

        return;
}

なお、解析に使った環境はこれです。

prompt# uname -a
FreeBSD xxxxx.koinec.jp 9.1-RELEASE FreeBSD 9.1-RELEASE #0:

prompt# gcc -v
Using built-in specs.
Target: amd64-undermydesk-freebsd
Configured with: FreeBSD/amd64 system compiler
Thread model: posix
gcc version 4.2.1 20070831 patched [FreeBSD]

.debug_frameの内容とソースの逆アセンブル結果の突き合わせ

まず、上記のソースの逆アセンブル結果と、.debug_frameの内容をアセンブラ命令単位で突合して、動きを見て行きます。
で、これをやるために使ったコマンドは以下です。

  • コンパイル
    • "gcc -o dwarftest2 -g dwarftest2.c"
    • そのまま余計なことしてませんです。
  • 逆アセンブル
    • "objdump -d -S dwarftest2"
    • "-d"で逆アセンブル+"-S"でCソース中に逆アセンブル埋め込んでね。です。
  • .debug_frame情報出力
    • "readelf -wF dwarftest2"
    • なぜか、頼んでもいないので、".eh_frame"も出してくれちゃいます。。。

そして、逆アセンブルのリスト内に、.debug_frameの情報をマージして、コメント入れる形としてみました。
以下の見方ですが、行頭が!の行は、逆アセンブルの結果、#の行は.debug_frame、何もない行はCソースで、それぞれ逆アセンブルした時のコードアドレス単位に並んでいます。では見てみましょう。

# <CIE> ############################################################
#
# 00000000 00000014 ffffffff CIE "" cf=1 df=-8 ra=16
#  * 00000014: length? = 20Byteって意味でしょうか?
#  * cf=1 : code_alignment_factor
#  * df=-8: data_alignment_factor なぜマイナス?
#  * ra=16: return_address_register
# 
#    LOC   CFA      ra   
# 00000000 r7+8     c-8  
#  * なぜ、r6がないのか分かりません。。。


dwarftest2:     file format elf64-x86-64-freebsd

----------------------------------------------------------------------------------
00000000004005b0 <func2>:
#include<stdio.h>

int             a;

# .debug_frame for "void func2( int *b )" ###########################
# 00000018 0000001c 00000000 FDE cie=00000000 pc=004005b0..004005c4
#  * 00000018 : これは、このFDEが.debug_frameの0x18=24Byte目からってこと
#  * 0000001c : FDE:length 28Byteなんですね。
#  * 00000000 : FDE:CIE_pointer このFDEが見るCIEは、.debug_frameの0Byte目からだよってことですね。
#               同時に、-1じゃないんでこいつは「FDE」だよってことです。
#  * pc=004005b0..004005c4 :
#        FDE:initial_location = 004005b0
#        FDE:address_range = 20
#      ってことみたいです。
#  * 以下、FDE:instructionsです。
#    LOC   CFA      r6   ra   
# 004005b0 r7+8     u    c-8  
# 004005b1 r7+16    c-16 c-8  
# 004005b4 r6+16    c-16 c-8  
#  * この3行は、以下Cソース、逆アセンブラコード文中にも埋め込んでみます。
#  * あと、なぜCIEにはいない「r6」がいるんでしょうか。。。?どっから来たの?
#  * ちなみに、以下も含め、CIFルール表の各行の値、はその行のアセンブラ命令を実行する前の
#    状態です。004005b0なら、"puch %rbp"を実行する直前の状態です。

void func2( int *b )    {
# 004005b0 r7+8     u    c-8  
#  * r6: undefined
#     この段階ではr6は使われていませんってことですね。
#  * ra: c-8
#     raはレジスタじゃなくって、return addressの場所を指し示しています。
#     で、ra=c-8  cは、Call Frame Addressなので、CFA-8の場所です。
#     ん、ここで気になります。INTEL64/amd64のABIでは、call命令で関数を呼ぶと
#     戻りアドレスが自動的にスタックにpushされちゃう(=スタックポインタSPは-8される)
#     わけですが、ここでのraはc-8ですね。ということは、逆算すると、
#        「CFA は call命令を呼び出す前のスタック = 戻り先格納スタックアドレス+8」
#     になりますね。
#    (INTEL64の場合は、てっきりReturn AddressのスタックアドレスがCFAかなって思い込んでました)
#  * CFA: r7+8
#     で、CFAのr7+8を、上のraの内容から逆算すると、r7=CFA-8です。
#     これは、まさしく、"push %rbp"する前のSPの値です。よって、r7=spですね。
!  4005b0:      55                      push   %rbp
#  * で、"push %rbp"で呼び出し元(親)関数のBPを保存します。

# 004005b1 r7+16    c-16 c-8  
#  * 保存した後の状態が、004005b1の行の状態です。
#  * ポイントは、r7の値は、004005b0の行から-8されているってことです。(pushしたので)
#  *  CFA: r7 + 16
#      はい。rbpのpushで、スタックポインタsp=r7は-8されますね。
#      スタックには、rbpの値の前に、return address 8byteもcall命令によってPushされてます。
#      よって、CFAの場所は、今のスタックポインタSP=r7に16足す、ということでビンゴ!です。
#  *  ra: c-8
#      cはCFAで、これはpushしてもCFAの位置は変わりません。なので、4005b0と同じく、c-8です。
#      よって、c-8のまま変わらず。
#  *  r6: c-16
#      んで、r6です。
#      c-16なんで、これはちょうど、400b50でrbpをpushしたアドレスですね。
#      r6の値は、CFA-16の場所に今あるよってことを示していますから、r6=bpと考えるのが妥当ですね。
!  4005b1:      48 89 e5                mov    %rsp,%rbp
#  * で、現在のrsp → rbp にコピーします。

!  4005b4:      48 89 7d f8             mov    %rdi,-0x8(%rbp)
# 004005b4 r6+16    c-16 c-8  
#  * この行の状態を見て行く前に、実行した"mov %rsp, %rbp"ですが、
#    rsp=rbpにしたため、r7=r6 になります。まず、これが前提です。
#  *  ra: c-8
#      これは、先程とおんなじですね。CFAはあくまで固定、ですから、
#      return addressはCFA-8の場所、も固定ですね。
#  *  r6: c-16
#      これは、注意ですね。まず、4005b1のmovで、rbp、すなわちr6の値を書き換えてます。
#      にもかかわらず、r6の値はc-16です。あれぇ?書き換えたよね?
#      ということで、これから
#        「CFIルール表のレジスタ列=関数開始時点のそのレジスタの値の今の格納先」
#      を示している、ということが分かります。今のr6の値ではない、と言うことに注意です。
#  *  CFA: r6+16
#      最後にCFAですが、なぜか"r7+16"から"r6+16"に変わっていますね。
#      これは、この行の時点でこそ"r7+16"="r6+16"=CFAなのですが、r7はSPです。
#      この後の関数の処理、で自動変数使ったりすると、値がバンバン変わります。
#      でも、その度にCFAのinstruction書いていると、データ容量的にたまらんですね。
#      ということで、rbpにコピった、スタックポインタ値をベースにCFAも表現しているってことです。
#      あと、「CFA列のレジスタ表記は、現在のレジスタの値」ということみたいです。
#      おなじr6でも、CFA列とr6列では、意味が違うので注意ですね。
#        (こんなん、英語の規約書読んだだけで分かるかいぃー) 

        *b = 3;
!  4005b8:      48 8b 45 f8             mov    -0x8(%rbp),%rax
!  4005bc:      c7 00 03 00 00 00       movl   $0x3,(%rax)

        return;
 }
!  4005c2:      c9                      leaveq 
!  4005c3:      c3                      retq   
!  4005c4:      66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
!  4005ca:      66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
#  * DWARF3の規約書では、関数の終り部分で、レジスタを元に戻して行く様の例があったのですが、
#    gccのINTEL64/amd64では、省略されてるっぽいです。つか、使わんって見たのでしょう。


----------------------------------------------------------------------------------
00000000004005d0 <func1>:

# .debug_frame for "int func1( int a, int *b )" ###########################
# 00000038 0000001c 00000000 FDE cie=00000000 pc=004005d0..004005f9
#    LOC   CFA      r6   ra   
# 004005d0 r7+8     u    c-8  
# 004005d1 r7+16    c-16 c-8  
# 004005d4 r6+16    c-16 c-8  
#
#  * ちなみに、func1(この関数)も、func2と同じっぽいです。よって説明は上記参照ってことで。

int func1( int a, int *b)       {
# 004005d0 r7+8     u    c-8  
!  4005d0:      55                      push   %rbp

# 004005d1 r7+16    c-16 c-8  
!  4005d1:      48 89 e5                mov    %rsp,%rbp

# 004005d4 r6+16    c-16 c-8  
!  4005d4:      48 83 ec 20             sub    $0x20,%rsp
!  4005d8:      89 7d ec                mov    %edi,-0x14(%rbp)
!  4005db:      48 89 75 e0             mov    %rsi,-0x20(%rbp)
        int             c;

        func2( b );
!  4005df:      48 8b 7d e0             mov    -0x20(%rbp),%rdi
!  4005e3:      e8 c8 ff ff ff          callq  4005b0 <func2>
        c = a + *b;
!  4005e8:      48 8b 45 e0             mov    -0x20(%rbp),%rax
!  4005ec:      8b 00                   mov    (%rax),%eax
!  4005ee:      03 45 ec                add    -0x14(%rbp),%eax
!  4005f1:      89 45 fc                mov    %eax,-0x4(%rbp)

        return c;       
!  4005f4:      8b 45 fc                mov    -0x4(%rbp),%eax
 }
!  4005f7:      c9                      leaveq 
!  4005f8:      c3                      retq   
!  4005f9:      0f 1f 80 00 00 00 00    nopl   0x0(%rax)


----------------------------------------------------------------------------------
0000000000400600 <main>:

# .debug_frame for "int main( int argc, char *argv[] )" ###########################
# 00000058 0000001c 00000000 FDE cie=00000000 pc=00400600..00400648
#    LOC   CFA      r6   ra   
# 00400600 r7+8     u    c-8  
# 00400601 r7+16    c-16 c-8  
# 00400604 r6+16    c-16 c-8  
#
#  * main関数も、func2, fucn1と同じですね。

int main( int argc, char *argv[])       {
# 00400600 r7+8     u    c-8  
!  400600:      55                      push   %rbp

# 00400601 r7+16    c-16 c-8  
!  400601:      48 89 e5                mov    %rsp,%rbp

# 00400604 r6+16    c-16 c-8  
!  400604:      48 83 ec 20             sub    $0x20,%rsp
!  400608:      89 7d ec                mov    %edi,-0x14(%rbp)
!  40060b:      48 89 75 e0             mov    %rsi,-0x20(%rbp)
        
        int             b;
        int             c;

        a = 1;
!  40060f:      c7 05 4f 03 20 00 01    movl   $0x1,0x20034f(%rip)        # 600968 <a>
!  400616:      00 00 00 

        c = func1( a, &b );
!  400619:      8b 3d 49 03 20 00       mov    0x200349(%rip),%edi        # 600968 <a>
!  40061f:      48 8d 75 f8             lea    -0x8(%rbp),%rsi
!  400623:      e8 a8 ff ff ff          callq  4005d0 <func1>
!  400628:      89 45 fc                mov    %eax,-0x4(%rbp)

        printf("a=%d, b=%d, c=%dn", a, b, c);
!  40062b:      8b 55 f8                mov    -0x8(%rbp),%edx
!  40062e:      8b 35 34 03 20 00       mov    0x200334(%rip),%esi        # 600968 <a>
!  400634:      8b 4d fc                mov    -0x4(%rbp),%ecx
!  400637:      bf 93 06 40 00          mov    $0x400693,%edi
!  40063c:      b8 00 00 00 00          mov    $0x0,%eax
!  400641:      e8 0e fe ff ff          callq  400454 <printf@plt>

        return;
 }
!  400646:      c9                      leaveq 
!  400647:      c3                      retq   
!  400648:      90                      nop    
!  400649:      90                      nop    
!  40064a:      90                      nop    
!  40064b:      90                      nop    
!  40064c:      90                      nop    
!  40064d:      90                      nop    
!  40064e:      90                      nop    
!  40064f:      90                      nop    

ここで分かったこと。(ポイント)

この例を見て、本人的にはまぁすっきりしました。
ということで、一応、ポイントだけ並べてみます。

  • FDEは、関数単位にレコードを持つ。
  • CIEは、呼び出しの形式単位に1つ。
    • INTEL64(amd64)の場合は、事実上1つ?(C言語の場合、C++は知らん)
  • CFA(Call Frame Address)は、スタックにあるreturn addressやrbpの保管アドレスではなく、「return addressをスタックに積む前のスタックポインタ」
    • もちろん、INTEL64(amd64)の場合+gcc 4.2の場合です。
  • CIEルール表の各行の値は、そのコードアドレスの「命令を実行する前の値」
  • CIEルール表の「レジスタ列は、その関数実行前の各レジスタの値が今どこにあるか?を示すもの」
    • そのコードアドレス時点の各レジスタの値を示すものではない。
    • 他方、CFA列内のレジスタだけは、そのコードアドレス時点の各レジスタの値。

次の調査点

この例で、実際のソース/アセンブラコードとCFA命令、およびCFA命令によって作られたCFIルール表の関係ははっきりしましたが、

「.debug_frameセクション内でのCIE,FDEのバイナリ表現」

がまだ、不明です。これは、次に見て行きたいとおもいます。

目次に戻る:DWARFファイルフォーマット