デバッグ情報を探る 〜.cfi_xx命令の動作〜

次のようなc言語プログラムを例にとって考えていく。

// test.c

int hoge(int x)
{
    return x+1;
}

int main()
{
    int a = 1;
    return hoge(a);
}

gcc -O0 -S test.cを実行してコンパイルすると、

; test.s

_hoge:
    .cfi_startproc

    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16

    movq %rsp, %rbp
    .cfi_def_cfa_register %rbp

    movl %edi, -4(%rbp)
    movl -4(%rbp), %edi
    addl $1, %edi
    movl %edi, %eax
    popq %rbp
    retq
    .cfi_endproc

_main:
    .cfi_startproc

    pushq    %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16

    movq %rsp, %rbp
    .cfi_def_cfa_register %rbp

    subq $16, %rsp
    movl $0, -4(%rbp)
    movl $1, -8(%rbp)
    movl $1, -12(%rbp)
    movl -8(%rbp), %eax
    addl -12(%rbp), %eax
    movl %eax, %edi
    callq    _hoge
    addq $16, %rsp
    popq %rbp
    retq
    .cfi_endproc

このようなアセンブリが生成される。

.cfi_startproc擬似命令では、CFI (Call Frame Infomation)が新たに追加され、CFA (Canonical Frame Address)計算用のレジスタrspに、オフセットが0に初期化される。続くpushqではrbpをスタックに退避させている。そして、.cfi_def_cfa_offsetでCFAのオフセットを16にしている。このときのスタックには、

f:id:babyron64:20180805012254p:plain

のようにデータが格納されていく。

.cfi_offset %rbp, -16では、rbpレジスタの値をCFA-16のアドレスに退避させたことを表している。実際、rbpレジスタの値はスタックの先頭にあるので、(rsp-16)+16 = rspはrbpレジスタの値が格納されているアドレスを表す。僕はここで少しハマったのだが、rbpの値が格納されているアドレスはrsp-8ではない。スタックは成長するにつれてアドレスが減るからだ。(下図参照)

f:id:babyron64:20180805012242p:plain

実際例えば、

mv (%rsp-16+16) %rx

とすることで、rxレジスタにrbpの元の値を格納できる。

その後、CFAのレジスタを.cfi_def_cfa_register %rbpとすることで、このフレームのCFAを固定している。rspを参照レジスタにすると、スタック操作をするたびにCFAが変わってしまう。

最後に、.cfi_endprocで前のフレームのCFAを復元している。これらの命令によって生成された情報は、デフォルトでは.eh_frameセクションに保存される。