Skip to content

DWARFフレームの読み方

Arch Linuxなら elfutils を入れておく。

Terminal window
run0 pacman -S elfutils

対象のバイナリからDWARF情報を読むが、このときフレームのデバッグ情報を有効にする。

Terminal window
eu-readelf -winfo --debug-dump=frames [file]

以下の形式で各関数のフレームが読める。先頭にCIEがある。Goプログラムだとおそらく1つだけ。

[ 0] CIE length=16
CIE_id: 18446744073709551615
version: 3
augmentation: ""
code_alignment_factor: 1
data_alignment_factor: -4
return_address_register: 16
Program:
def_cfa r7 (rsp) at offset 8
offset_extended r16 (rip) at cfa-8
nop

CIEは複数のFDEを含む。

[ 151d4] FDE length=28 cie=[ 0]
CIE_pointer: 0
initial_location: 0x00000000004932c0 <github.com/lufia/try.waserror.abi0>
address_range: 0x46
Program:
def_cfa_offset_sf 8
advance_loc1 69 to 0x45
nop
nop
nop
nop

最初の16進数はDWARFヘッダ先頭からのオフセットなのであまり読む意味はない。length はCIE(FDE)ユニットの長さ。CIE_pointer はCIEエントリのオフセット値。intial_location は関数の先頭アドレスで、コンパイルユニット以下の low_pc に対応する。address_range は関数のコードサイズで、以下の式が成立する。

initial_location == low_pc
initial_location + address_range == high_pc

Program: 以下の意味は次で説明する。

Program: 以下はCFI(Call Frame Information)テーブルを表現する。

Program:
def_cfa_offset_sf 8
advance_loc1 69 to 0x45
nop
nop
nop
nop

opcodeという概念があって、eu-readelf はopcodeごとに出力する。名前に入っている cfa とはCFA(Canonical Frame Address)の略語で、通常は関数が呼ばれた時点における SP レジスタの値1を意味する。

では下記の意味は何なのか。一言でいえば、プログラムカウンタがどこにあるのかによって、関数が呼ばれた際のスタックポインタ値がどこに格納されているかを表現している。

具体的に以下の例をみていく。

initial_location: 0x00000000004932c0
address_range: 0x46
Program:
def_cfa_offset_sf 8
advance_loc1 69 to 0x45

関数はアドレス 0x4932c0 から始まっていて、その時点のCFAはスタックポインタから8バイト離れた場所にあるという意味になる。次に 0x4932c0 + 69 = 0x493305 を計算するが、このアドレスは 0x4932c0 + 0x46 = 0x493306 に到達するので関数が終わる。

もう少し複雑な例をみる。

def_cfa_offset_sf 8
advance_loc 11 to 0xb
def_cfa_offset_sf 16
advance_loc 7 to 0x12
def_cfa_offset_sf 40

該当するアセンブリコードはこの通り。

TEXT github.com/lufia/try.Handle(SB) /home/lufia/src/try/try.go
try.go:20 0x4998a0 CMPQ SP, 0x10(R14)
try.go:20 0x4998a4 JBE 0x499945
try.go:20 0x4998aa PUSHQ BP
try.go:20 0x4998ab MOVQ SP, BP
try.go:20 0x4998ae SUBQ $0x18, SP
try.go:21 0x4998b2 LEAQ 0x1acc7(IP), AX
try.go:21 0x4998b9 CALL runtime.newobject(SB)
try.go:21 0x4998be MOVQ AX, 0x10(SP)
try.go:22 0x4998c3 MOVQ AX, 0(SP)
try.go:22 0x4998c7 CALL .waserror.abi0(SB)

まずは CALLSP が-8されるので、CFAの位置は 8(SP) を示す。次に、プログラムが10バイト進行すると PUSHQ 命令があって SP が動くので、11バイト目、具体的には 0x4998ab からCFAは 16(SP) と変わる。同じように SUBQ 命令でも SP が動いて 40(SP) となる。

def_cfa_offset_sfadvance_loc1 の意味が分からなかったのでreadelf.cソースコードを読む。

#define get_sleb128(var, addr, end) ((var) = __libdw_get_sleb128(&(addr), end))
while(readp < endp){
unsigned int opcode = *readp++;
switch(opcode){
case DW_CFA_advance_loc1:
printf(" advance_loc1 %u to %#" PRIx64 "\n",
*readp, pc += *readp * code_align);
++readp;
break;
case DW_CFA_def_cfa_offset_sf:
get_sleb128(sop1, readp, endp);
printf(" def_cfa_offset_sf %" PRId64 "\n", sop1 * data_align);
break;
}
}

DW_CFA_def_cfa_offset_sf はDWARF 5の 6.4.2 Call Frame Instructions に説明があって、

The DW_CFA_def_cfa_offset_sf instruction takes a signed LEB128 operand representing a factored offset. This instruction is identical to DW_CFA_def_cfa_offset except that the operand is signed and factored. The resulting offset is factored_offset * data_alignment_factor. This operation is valid only if the current CFA rule is defined to use a register and offset.

そして DW_CFA_def_cfa_offset

The DW_CFA_def_cfa_offset instruction takes a single unsigned LEB128 operand representing a (non-factored) offset. The required action is to define the current CFA rule to use the provided offset (but to keep the old register). This operation is valid only if the current CFA rule is defined to use a register and offset.

雑な理解では、オフセット値(何の?)を置いているということだろうか。次、DW_CFA_advance_loc1 をみるとこれも別のコードを参照していて、

The DW_CFA_advance_loc1 instruction takes a single ubyte operand that represents a constant delta. This instruction is identical to DW_CFA_advance_loc except for the encoding and size of the delta operand.

で、DW_CFA_advance_loc の説明は

The DW_CFA_advance_loc instruction takes a single operand (encoded with the opcode) that represents a constant delta. The required action is to create a new table row with a location value that is computed by taking the current entry’s location value and adding the value of delta * code_alignment_factor. All other values in the new row are initially identical to the current row

よく分からないけど、以下の出力は「69バイト加算して0x45」という意味になる。

advance_loc1 69 to 0x45
  1. おそらくCALL直前の SP なので、実質スタックフレームの先頭(下位)アドレスを意味する