Unwind 栈回溯详解

1. 历史背景

1.1 frame pointers

在调试的时候经常需要进行堆栈回溯。最简单的方式是使用一个独立的寄存器(ebp)来保存每层函数调用的堆栈栈顶(frame pointer):

pushl%ebp 
movl%esp,%ebp 
... 
popl%ebp 
ret
  • x86_64的frame point模式

在这里插入图片描述

  • arm64的frame point模式

在这里插入图片描述

这种方式在堆栈回溯时非常方便快捷。但是这种方法也有自己的不足:

  • 需要一个专门寄存器ebp来保存frame poniter。
  • 保存ebp寄存器即保存回溯信息(unwind info)的动作会被编译成代码,有指令开销。
  • 在回溯堆栈时,除了恢复sp,不知道怎么恢复其他的寄存器。(例如gdb中的 frame n, info reg)
  • 没有源语言信息。

1.2 .debug_frame (DWARF)

调试信息标准DWARF(Debugging With Attributed Record Formats)定义了一个.debug_frame section用来解决上述的难题。

  • 可以把ebp当成常规寄存器使用。
  • 但是当保存esp时,它必须在.debug_frame节中产生一条注释,告知调试器它将其保存在什么位置以及存放在何处。
  • 这种机制还有的好处是它不仅仅是用来恢复ebp,还可以用来恢复其他寄存器。
  • 而且是带外的,不消耗任何指令周期,没有任何性能开销。

这种机制也有其不足:

  • 没有源语言信息。
  • 不支持在程序加载时同时加载调试信息。

1.3 .eh_frame (LSB)

现代Linux操作系统在LSB(Linux Standard Base)标准中定义了一个.eh_frame section来解决上述的难题。这个section和.debug_frame非常类似,但是它解决了上述难题:

  • 拥有源语言信息。
  • 编码紧凑,并随程序一起加载。

但是.debug_frame.eh_frame同时面临一个难题:怎么样生成堆栈信息表?

1.4 CFI directives

为了解决上述难题,GAS(GCC Assembler)汇编编译器定义了一组伪指令来协助生成调用栈信息CFI(Call Frame Information)

CFI directives伪指令是一组生成CFI调试信息的高级语言,它的形式类似于:

f:
.cfi_startproc
pushl%ebp
.cfi_def_cfa_offset 8
.cfi_offset ebp,-8

关于汇编器利用这些伪指令来生成.debug_frame还是.debug_frame,在.cfi_sections指令中定义。如果只是调试需求可以生成.debug_frame,如果需要在运行时调用需要生成.eh_frame

2. .debug_frame (DWARF) 详解

DWARF6.4 Call Frame Information一节详细的描述了调用栈帧的定义。

2.1 Call Frame Table

抽象的说,整个栈帧计算机制的核心是一张大表:

在这里插入图片描述

可以使用readelf -wF xxx命令来查看通过elf文件中的.eh_frame解析出来的这张表:

$ readelf -wF a.out 

...

   LOC           CFA      rbx   rbp   r12   r13   r14   r15   ra    
00000000000006b0 rsp+8    u     u     u     u     u     u     c-8   
00000000000006b2 rsp+16   u     u     u     u     u     c-16  c-8   
00000000000006b4 rsp+24   u     u     u     u     c-24  c-16  c-8   
00000000000006b9 rsp+32   u     u     u     c-32  c-24  c-16  c-8   
00000000000006bb rsp+40   u     u     c-40  c-32  c-24  c-16  c-8   
00000000000006c3 rsp+48   u     c-48  c-40  c-32  c-24  c-16  c-8   
00000000000006cb rsp+56   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
00000000000006d8 rsp+64   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
000000000000070a rsp+56   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
000000000000070b rsp+48   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
000000000000070c rsp+40   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
000000000000070e rsp+32   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
0000000000000710 rsp+24   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
0000000000000712 rsp+16   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
0000000000000714 rsp+8    c-56  c-48  c-40  c-32  c-24  c-16  c-8   

这张表的结构设计如下:

  • 1、第一列。表示程序中包含代码的每个位置的地址。(在共享对象中,这是一个相对于对象的偏移量。)其余的列包含与指示的位置关联的虚拟展开规则。
  • 2、CFA列。定义了计算规范帧地址值(Canonical Frame Address)的规则;它可以是寄存器(register)和带符号的偏移量(signed offset)加在一起,也可以是DWARF表达式(DWARF expression)的求值。
  • 3、其余列。由寄存器编号标记。 其中包括一些在某些架构上具有特殊名称的寄存器,例如PC和堆栈指针寄存器。 (用于特定体系结构的寄存器的实际映射由扩充器定义。)寄存器列包含规则用来描述是否已保存给定寄存器以及在上一帧中查找寄存器值的规则。

如果按上述说明实际构建,此表将非常大。 该表中任何位置的大多数条目与它们上方的条目相同。通过仅记录从程序中每个子例程的起始地址开始的差异,可以非常紧凑地表示整个表。

虚拟展开信息被编码在称为.debug_frame的独立部分中。 .debug_frame节中的条目相对于该节的开头按地址大小的倍数对齐,并以两种形式出现:公共信息条目(CIE)和帧描述条目(FDE)。

如果某个函数的代码地址范围不连续,则可能有多个CIE和FDE与该功能的各个部分相对应。

在这里插入图片描述

CIE和FDE的定义在.eh_frame一节会详细描述,这里就不展开。本节重点关注CFA指令的解析。

2.2 Call Frame Instructions

上一节说明栈帧回溯的核心是一张大表,而这个表的行(Row)列(Column)最后都是靠CIE和FDE中的指令填充起来的。

这些指令主要分为以下几大类:

  • 1、Row Creation Instructions

本类的指令会确定一个新的Location(PC)值,相当于在表中创建新的一行(Row)

InstructionsDescriptDescript
DW_CFA_set_loc Location = Address

DW_CFA_set_loc指令采用单个操作数代表目标地址。
所需的操作是使用指定位置的地址来创建新的表行。 新行中的所有其他值最初都与当前行相同。 新位置值始终大于当前位置值。
如果此FDE的CIE的segment_size字段不为零,则在初始位置之前是给定长度的段选择器。
The DW_CFA_set_loc instruction takes a single operand that represents a target address. The required action is to create a new table row using the specified address as the location. All other values in the new row are initially identical to the current row. The new location value is always greater than the current one. If the segment_size field of this FDE's CIE is non-zero, the initial location is preceded by a segment selector of the given length.
DW_CFA_advance_loc Location += (delta * code_alignment_factor)

DW_CFA_advance指令采用单个操作数(和操作码一起编码),该操作数表示常数增量。
所需的操作是使用位置值创建一个新表行,该位置值是通过获取当前条目的位置值并加上“ delta * code_alignment_factor”的值来计算的。 新行中的所有其他值最初都与当前行相同。
The DW_CFA_advance 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.
DW_CFA_advance_loc1 Location += (delta * code_alignment_factor)

DW_CFA_advance_loc1指令采用一个表示常量增量的单个`ubyte`操作数。
除了增量操作数的编码和大小外,该指令与DW_CFA_advance_loc相同。
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_loc2 Location += (delta * code_alignment_factor)

DW_CFA_advance_loc2指令采用单个`uhalf`操作数表示常数增量。
除了增量操作数的编码和大小外,该指令与DW_CFA_advance_loc相同。
The DW_CFA_advance_loc2 instruction takes a single uhalf 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_loc4 Location += (delta * code_alignment_factor)

DW_CFA_advance_loc4指令采用单个`uword`操作数来表示恒定增量。
除了增量操作数的编码和大小外,该指令与DW_CFA_advance_loc相同。
The DW_CFA_advance_loc4 instruction takes a single uword 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.
  • 2、CFA Definition Instructions

在Location相关指令创建一个新行(Row)以后,本节相关指令来定义这一行的CFA计算规则。

InstructionsDescriptDescript
DW_CFA_def_cfa register = new register num
offset = new offset
CFA = register + offset

DW_CFA_def_cfa指令采用两个无符号的LEB128操作数,它们代表`寄存器号`和(非因数)`偏移量`。
所需的操作是定义当前的CFA规则以使用提供的寄存器和偏移量。
The DW_CFA_def_cfa instruction takes two unsigned LEB128 operands representing a register number and a (non-factored) offset. The required action is to define the current CFA rule to use the provided register and offset.
DW_CFA_def_cfa_sf register = new register num
offset = (factored_offset * data_alignment_factor)
CFA = register + offset

DW_CFA_def_cfa_sf指令采用两个操作数:代表寄存器号的无符号LEB128值和有符号LEB128因数偏移量。
该指令与DW_CFA_def_cfa相同,不同之处在于第二个操作数是有符号并且乘以因数。 结果偏移量为factored_offset * data_alignment_factor。
The DW_CFA_def_cfa_sf instruction takes two operands: an unsigned LEB128 value representing a register number and a signed LEB128 factored offset. This instruction is identical to DW_CFA_def_cfa except that the second operand is signed and factored. The resulting offset is factored_offset * data_alignment_factor.
DW_CFA_def_cfa_register register = new register number
CFA = register + old offset

DW_CFA_def_cfa_register指令采用单个无符号的LEB128操作数表示寄存器号。
所需的操作是定义当前的CFA规则以使用提供的寄存器(但保留旧的偏移量)。
仅当当前CFA规则定义为使用寄存器和偏移量时,此操作才有效。
The DW_CFA_def_cfa_register instruction takes a single unsigned LEB128 operand representing a register number. The required action is to define the current CFA rule to use the provided register (but to keep the old offset). This operation is valid only if the current CFA rule is defined to use a register and offset.
DW_CFA_def_cfa_offset offset = new offset
CFA = old register + offset

DW_CFA_def_cfa_offset指令采用单个无符号LEB128操作数表示一个(未分解的)偏移量。
所需的操作是定义当前的CFA规则以使用提供的偏移量(但保留旧寄存器)。
仅当当前CFA规则定义为使用寄存器和偏移量时,此操作才有效。
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_def_cfa_offset_sf offset = (factored_offset * data_alignment_factor)
CFA = old register + offset

DW_CFA_def_cfa_offset_sf指令采用一个带符号的LEB128操作数,代表一个因数偏移量。
该指令与DW_CFA_def_cfa_offset相同,除了操作数是有符号并且乘以因数。 结果偏移量为factored_offset * data_alignment_factor。 仅当当前CFA规则定义为使用寄存器和偏移量时,此操作才有效。
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_expression CFA = DWARF expression

DW_CFA_def_cfa_expression指令采用单个操作数编码为代表DWARF表达式的DW_FORM_exprloc值。
所需的操作是建立该表达式作为计算当前CFA的方式。
The DW_CFA_def_cfa_expression instruction takes a single operand encoded as a DW_FORM_exprloc value representing a DWARF expression. The required action is to establish that expression as the means by which the current CFA is computed.
  • 3、Register Rule Instructions

本节指令确定一行(Row)寄存器的恢复规则。

寄存器规则包含以下:

RuleDescriptDescript
undefined 具有此规则的寄存器在前一帧中没有可恢复的值。(按照惯例,被调用方不会保留它。) A register that has this rule has no recoverable value in the previous frame. (By convention, it is not preserved by a callee.)
same value 没有从前一帧修改此寄存器。(按照惯例,它是由被调用方保留的,但是被调用方尚未对其进行修改。) This register has not been modified from the previous frame. (By convention, it is preserved by the callee, but the callee has not modified it.)
offset(N) 该寄存器的先前值保存在地址CFA + N中,其中CFA是当前CFA值,N是有符号偏移量。 The previous value of this register is saved at the address CFA+N where CFA is the current CFA value and N is a signed offset.
val_offset(N) 该寄存器的前一个值为CFA + N,其中CFA为当前CFA值,N为有符号偏移量。 The previous value of this register is the value CFA+N where CFA is the current CFA value and N is a signed offset.
register(R) 该寄存器的先前值存储在另一个编号为R的寄存器中。 The previous value of this register is stored in another register numbered R.
expression(E) 该寄存器的先前值位于通过执行DWARF表达式E产生的地址。 The previous value of this register is located at the address produced by executing the DWARF expression E.
val_expression(E) 该寄存器的先前值是通过执行DWARF表达式E产生的值。 The previous value of this register is the value produced by executing the DWARF expression E.
architectural 该规则由增强器在此规范的外部定义。 The rule is defined externally to this specification by the augmenter.

具体包含以下指令:

InstructionsRuleDescriptDescript
DW_CFA_undefinedundefined reg num = undefined

DW_CFA_undefined指令采用单个无符号的LEB128操作数来表示寄存器号。
所需的操作是将指定寄存器的规则设置为“未定义”。
The DW_CFA_undefined instruction takes a single unsigned LEB128 operand that represents a register number. The required action is to set the rule for the specified register to “undefined.”
DW_CFA_same_valuesame value reg num = same value

DW_CFA_same_value指令采用单个无符号LEB128操作数来表示寄存器号。
所需的操作是将指定寄存器的规则设置为“相同值”。
The DW_CFA_same_value instruction takes a single unsigned LEB128 operand that represents a register number. The required action is to set the rule for the specified register to “same value.”
DW_CFA_offsetoffset(N) offset = (factored offset * data_alignment_factor)
reg num = *(CFA + offset)

DW_CFA_offset指令采用两个操作数:一个寄存器号(用操作码编码)和一个无符号的LEB128常量,表示因数偏移量。
所需的操作是将由寄存器编号指示的寄存器的规则更改为offset(N)规则,其中N的值是`factored offset* data_alignment_factor`。
The DW_CFA_offset instruction takes two operands: a register number (encoded with the opcode) and an unsigned LEB128 constant representing a factored offset. The required action is to change the rule for the register indicated by the register number to be an offset(N) rule where the value of N is factored offset * data_alignment_factor.
DW_CFA_offset_extendedoffset(N) offset = (factored offset * data_alignment_factor)
reg num = *(CFA + offset)

DW_CFA_offset_extended指令采用两个无符号的LEB128操作数,它们代表寄存器号和因数偏移量。
该指令与DW_CFA_offset相同,不同之处在于寄存器操作数的编码和大小。
The DW_CFA_offset_extended instruction takes two unsigned LEB128 operands representing a register number and a factored offset. This instruction is identical to DW_CFA_offset except for the encoding and size of the register operand.
DW_CFA_offset_extended_sfoffset(N) offset = (factored offset * data_alignment_factor)
reg num = *(CFA + offset)

DW_CFA_offset_extended_sf指令采用两个操作数:代表寄存器号的无符号LEB128值和有符号LEB128因数偏移量。
该指令与DW_CFA_offset_extended相同,不同之处在于第二个操作数有符号且乘以因数。 结果偏移量为factored_offset * data_alignment_factor。
The DW_CFA_offset_extended_sf instruction takes two operands: an unsigned LEB128 value representing a register number and a signed LEB128 factored offset. This instruction is identical to DW_CFA_offset_extended except that the second operand is signed and factored. The resulting offset is factored_offset * data_alignment_factor.
DW_CFA_val_offsetval_offset(N) offset = (factored offset * data_alignment_factor)
reg num = CFA + offset

DW_CFA_val_offset指令采用两个无符号的LEB128操作数,它们代表寄存器号和因数偏移量。
所需的操作是将寄存器编号指示的寄存器规则更改为val_offset(N)规则,其中N的值是factored_offset * data_alignment_factor。
The DW_CFA_val_offset instruction takes two unsigned LEB128 operands representing a register number and a factored offset. The required action is to change the rule for the register indicated by the register number to be a val_offset(N) rule where the value of N is factored_offset * data_alignment_factor.
DW_CFA_val_offset_sfval_offset(N) offset = (factored offset * data_alignment_factor)
reg num = CFA + offset

DW_CFA_val_offset_sf指令采用两个操作数:代表寄存器号的无符号LEB128值和有符号LEB128因数偏移量。
该指令与DW_CFA_val_offset相同,不同之处在于第二个操作数有符号且乘以因数。 结果偏移量为factored_offset * data_alignment_factor。
The DW_CFA_val_offset_sf instruction takes two operands: an unsigned LEB128 value representing a register number and a signed LEB128 factored offset. This instruction is identical to DW_CFA_val_offset except that the second operand is signed and factored. The resulting offset is factored_offset * data_alignment_factor.
DW_CFA_registerregister(R) reg num = R (second operands)

DW_CFA_register指令采用两个无符号的LEB128操作数表示寄存器编号。
所需的操作是将第一个寄存器的规则设置为register(R),其中R是第二个寄存器。
The DW_CFA_register instruction takes two unsigned LEB128 operands representing register numbers. The required action is to set the rule for the first register to be register(R) where R is the second register.
DW_CFA_expressionexpression(E) reg num = *(DWARF expression)

DW_CFA_expression指令采用两个操作数:代表寄存器号的无符号LEB128值和代表DWARF表达式的DW_FORM_block值。
所需的操作是将由寄存器编号指示的寄存器的规则更改为expression(E)规则,其中E是DWARF表达式。 即,DWARF表达式计算地址。
在执行DWARF表达式之前,将CFA的值压入DWARF评估堆栈。
The DW_CFA_expression instruction takes two operands: an unsigned LEB128 value representing a register number, and a DW_FORM_block value representing a DWARF expression. The required action is to change the rule for the register indicated by the register number to be an expression(E) rule where E is the DWARF expression. That is, the DWARF expression computes the address. The value of the CFA is pushed on the DWARF evaluation stack prior to execution of the DWARF expression.
DW_CFA_val_expressionval_expression(E) reg num = DWARF expression

DW_CFA_val_expression指令采用两个操作数:代表寄存器号的无符号LEB128值和代表DWARF表达式的DW_FORM_block值。
所需的操作是将由寄存器号指示的寄存器的规则更改为val_expression(E)规则,其中E是DWARF表达式。 也就是说,DWARF表达式计算给定寄存器的值。
在执行DWARF表达式之前,将CFA的值压入DWARF评估堆栈。
The DW_CFA_val_expression instruction takes two operands: an unsigned LEB128 value representing a register number, and a DW_FORM_block value representing a DWARF expression. The required action is to change the rule for the register indicated by the register number to be a val_expression(E) rule where E is the DWARF expression. That is, the DWARF expression computes the value of the given register. The value of the CFA is pushed on the DWARF evaluation stack prior to execution of the DWARF expression.
DW_CFA_restoreinitial_instructions in the CIE reg num = initial_instructions in the CIE

DW_CFA_restore指令采用单个操作数(用操作码编码),该操作数代表寄存器号。
所需的操作是将指示寄存器的规则更改为CIE中initial_instructions为其分配的规则。
The DW_CFA_restore instruction takes a single operand (encoded with the opcode) that represents a register number. The required action is to change the rule for the indicated register to the rule assigned it by the initial_instructions in the CIE.
DW_CFA_restore_extendedinitial_instructions in the CIE reg num = initial_instructions in the CIE

DW_CFA_restore_extended指令采用单个无符号LEB128操作数来表示寄存器号。
该指令与DW_CFA_restore相同,不同之处在于寄存器操作数的编码和大小。
The DW_CFA_restore_extended instruction takes a single unsigned LEB128 operand that represents a register number. This instruction is identical to DW_CFA_restore except for the encoding and size of the register operand.
  • 4、Row State Instructions

接下来的两条指令提供了备份(stack)和恢复(retrieve)完整寄存器状态的能力。

InstructionsDescriptDescript
DW_CFA_remember_state DW_CFA_remember_state指令不接受任何操作数。
所需的操作是将每个寄存器的规则集压入隐式堆栈。
The DW_CFA_remember_state instruction takes no operands. The required action is to push the set of rules for every register onto an implicit stack.
DW_CFA_restore_state DW_CFA_restore_state指令不接受任何操作数。
所需的操作是将规则集从隐式堆栈中弹出,并将其放置在当前行(row)中。
The DW_CFA_restore_state instruction takes no operands. The required action is to pop the set of rules off the implicit stack and place them in the current row.
  • 5、Padding Instruction
InstructionsDescriptDescript
DW_CFA_nop DW_CFA_nop指令没有操作数,也没有必需的操作。 它用作填充以使CIE或FDE大小合适。 The DW_CFA_nop instruction has no operands and no required actions. It is used as padding to make a CIE or FDE an appropriate size.
  • 6、CFI Extensions
InstructionsDescriptDescript
DW_CFA_GNU_args_size DW_CFA_GNU_args_size指令采用表示参数大小的无符号LEB128操作数。 该指令指定已推入堆栈的参数的总大小。 The DW_CFA_GNU_args_size instruction takes an unsigned LEB128 operand representing an argument size. This instruction specifies the total of the size of the arguments which have been pushed onto the stack.
DW_CFA_GNU_negative_offset_extended offset = (factored offset * data_alignment_factor)
reg num = *(CFA + offset)

DW_CFA_def_cfa_sf指令采用两个操作数:代表寄存器号的无符号LEB128值和代表偏移量的无符号LEB128。
该指令与DW_CFA_offset_extended_sf相同,除了减去操作数以产生偏移量。 该指令已被DW_CFA_offset_extended_sf废弃。
The DW_CFA_def_cfa_sf instruction takes two operands: an unsigned LEB128 value representing a register number and an unsigned LEB128 which represents the magnitude of the offset. This instruction is identical to DW_CFA_offset_extended_sf except that the operand is subtracted to produce the offset. This instructions is obsoleted by DW_CFA_offset_extended_sf.

2.3 Instructions Opcode

各个指令具体的指令码编码如下:

InstructionHigh 2BitsLow 6 BitsOperand 1Operand 2
DW_CFA_advance_loc0x1delta
DW_CFA_offset0x2registerULEB128 offset
DW_CFA_restore0x3register
DW_CFA_nop00
DW_CFA_set_loc00x01address
DW_CFA_advance_loc100x021-byte delta
DW_CFA_advance_loc200x032-byte delta
DW_CFA_advance_loc400x044-byte delta
DW_CFA_offset_extended00x05ULEB128 registerULEB128 offset
DW_CFA_restore_extended00x06ULEB128 register
DW_CFA_undefined00x07ULEB128 register
DW_CFA_same_value00x08ULEB128 register
DW_CFA_register00x09ULEB128 registerULEB128 register
DW_CFA_remember_state00x0a
DW_CFA_restore_state00x0b
DW_CFA_def_cfa00x0cULEB128 registerULEB128 offset
DW_CFA_def_cfa_register00x0dULEB128 register
DW_CFA_def_cfa_offset00x0eULEB128 offset
DW_CFA_def_cfa_expression00x0fBLOCK
DW_CFA_expression00x10ULEB128 registerBLOCK
DW_CFA_offset_extended_sf00x11ULEB128 registerSLEB128 offset
DW_CFA_def_cfa_sf00x12ULEB128 registerSLEB128 offset
DW_CFA_def_cfa_offset_sf00x13SLEB128 offset
DW_CFA_val_offset00x14ULEB128ULEB128
DW_CFA_val_offset_sf00x15ULEB128SLEB128
DW_CFA_val_expression00x16ULEB128BLOCK
DW_CFA_lo_user00x1c
DW_CFA_GNU_args_size00x2eULEB128
DW_CFA_GNU_negative_offset_extended00x2fULEB128ULEB128
DW_CFA_hi_user00x3f

2.4 DWARF expression

DWARF表达式描述了在程序调试期间如何计算值或命名位置。 它们以对一堆值进行操作的DWARF操作表示。

所有DWARF操作都被编码为流,每个操作码后跟零个或多个文字操作数。 操作数的数量由操作码决定。

具体DWARF表达式的运算指令在这里就不展开,感兴趣可以参考DWARF2.5 DWARF Expressions一节。

DW_CFA_def_cfa_expressionDW_CFA_expressionDW_CFA_val_expression这几条指令在计算时用到了DWARF表达式。同时它也有限制,以下DWARF运算符不能在此类操作数中使用:

  • DW_OP_call2,DW_OP_call4和DW_OP_call_ref运算符在这些指令的操作数中没有意义,因为没有从调用帧信息到任何对应的调试编译单元信息的映射,因此无法解释调用偏移。
  • DW_OP_push_object_address在这些指令的操作数中没有意义,因为没有对象上下文可提供要推送的值。
  • DW_OP_call_frame_cfa在这些指令的操作数中没有意义,因为它的使用是循环的。

因为DWARF expression拥有完备的图灵计算的能力,所以eh_frame容易成为攻击的后门。

2.5 Call Frame Instruction Usage

CFA相关指令的解析过程如下。

首先为了给虚拟展开规则集设置一个确定位置(L1),人们在FDE标头中进行搜索,查看initial_locationaddress_range值以查看L1是否包含在FDE中。 如果存在,则:

  • 1.通过读取关联的CIE的initial_instructions字段来初始化寄存器集。
  • 2.阅读并处理FDE的指令序列,直到遇到DW_CFA_advance_loc,DW_CFA_set_loc或指令流的末尾。
  • 3.如果遇到DW_CFA_advance_loc或DW_CFA_set_loc指令,则计算新的位置值(L2)。如果 L1 >= L2,则处理指令并返回步骤2。
  • 4.指令流的末尾可被视为DW_CFA_set_loc(初始位置+地址范围)指令。请注意,如果L2小于L1,则FDE格式不正确。
  • 5.现在,寄存器集中的规则适用于位置L1。

2.6 Call Frame Calling Address

展开帧时,使用者经常希望获得调用子例程的指令的地址。并非总是提供此信息。但是,通常虚拟展开表中的寄存器之一是返回地址。

如果在虚拟展开表中定义了返回地址寄存器,并且未定义其规则(例如,通过DW_CFA_undefined定义),则没有返回地址(return address),也没有调用地址(call address),并且堆栈激活的虚拟展开已完成。

在大多数情况下,返回地址与调用地址在同一上下文中,但不必如此,特别是如果生产者以某种方式知道该调用永不返回。 “返回地址”的上下文可能在不同的行中,在不同的词法块中或在调用子例程的末尾。如果使用者假定它与呼叫地址处于同一上下文中,则展开可能会失败。

对于具有恒定长度指令的体系结构,其中返回地址紧跟在调用指令之后,一种简单的解决方案是从返回地址中减去指令的长度以获得调用指令。对于具有可变长度指令的体系结构(例如x86),这是不可能的。但是,从返回地址减去1尽管不能保证提供准确的调用地址,但通常会在与调用地址相同的上下文中生成一个地址,通常就足够了。

2.7 CFI Example

以下示例使用摩托罗拉88000风格的假想RISC机器。它有以下特性:

•存储器是按字节寻址的。
•指令均为4个字节,字对齐。
•指令操作数通常采用以下形式:<destination.reg>,<source.reg>,<constant>
•加载和存储指令的地址是通过将源寄存器的内容与常量相加得出的。
•有8个4字节寄存器:
	R0 始终为0 
	R1 保存调用时的返回地址
	R2-R3 临时寄存器(在调用时不保护)
	R4-R6 在调用时保护
	R7 堆栈指针
•堆栈向负方向增长。
•体系结构ABI委员会指定堆栈指针(R7)与CFA相同
  • 1、源码:

以下是来自名为foo的子例程的两个代码片段,该子例程使用帧指针(除了堆栈指针之外)。 第一列的值是字节地址。 <fs>表示以字节为单位的堆栈帧大小,即12。

		;; 保存现场,保护 R1/R6/R4
		;; start prologue 
foo 	sub R7, R7, <fs> ; Allocate frame
foo+4 	store R1, R7, (<fs>-4) ; Save the return address
foo+8 	store R6, R7, (<fs>-8) ; Save R6
foo+12 	add R6, R7, 0 ; R6 is now the Frame ptr
foo+16 	store R4, R6, (<fs>-12) ; Save a preserved reg
		;;  R5 在子函数中没有调用,所以没有保护
		;;  This subroutine does not change R5
		...
		;; 恢复现场,恢复 R4/R6/R1
		;; Start epilogue (R7 is returned to entry value)
foo+64 	load R4, R6, (<fs>-12) ; Restore R4
foo+68 	load R6, R7, (<fs>-8) ; Restore R6
foo+72 	load R1, R7, (<fs>-4) ; Restore return address
foo+76 	add R7, R7, <fs> ; Deallocate frame
foo+80 	jump R1 ; Return
foo+84
  • 2、Call Frame Table

以上代码最终生成的CFI Table如下:

在这里插入图片描述

图中的注释如下:

1. R8 is the return address 
2. s = same_value rule 
3. u = undefined rule 
4. rN = register(N) rule 
5. cN = offset(N) rule
6. a = architectural rule
  • 3、CIE

在这里插入图片描述

  • 4、FDE

在这里插入图片描述

图中的注释如下:

1. <fs> = frame size
2. <caf> = code alignment factor
3. <daf> = data alignment factor

3. .eh_frame 详解

使用gcc -g生成的DWARF信息存储在debug_*类型的section,我们可以使用readelf -wi xxx查看debug_info段,或者dwarfdump xxx查看debug信息。

无论是否有-g选项,gcc默认都会生成.eh_frame.eh_frame_hdr section。-fno-asynchronous-unwind-tables选项可以禁止生成.eh_frame.eh_frame_hdr section。

3.1 .eh_frame 格式

LSB(Linux Standard Base)中对.eh_frame格式有详细的描述。

.eh_frame section 包含一个或者多个CFI(Call Frame Information)记录。每个CFI包含一个CIE(Common Information Entry Record)记录,每个CIE包含一个或者多个FDE(Frame Description Entry)记录。

通常情况下,CIE对应一个文件,FDE对应一个函数。

3.1.1 CIE 格式

FieldDescription
LengthRequired
Extended LengthOptional
CIE IDRequired
VersionRequired
Augmentation StringRequired
Code Alignment FactorRequired
Data Alignment FactorRequired
Return Address RegisterRequired
Augmentation Data LengthOptional
Augmentation DataOptional
Initial InstructionsRequired
Padding

具体解析如下:

  • 1、Length。读取4个字节。如果它们不是0xffffffff,则它们是CIE或FDE记录的长度。否则,接下来的64位将保留长度,这是64位DWARF格式。就像.debug_frame。

  • 2、ID。一个4字节无符号值,对于CIE,它是0。对于FDE,它是从该字段到与该FDE关联的CIE开头的字节偏移。字节偏移量到达CIE的长度记录。正值向后退;也就是说,您必须从当前字节位置减去ID字段的值才能获得CIE位置。这与.debug_frame不同,因为偏移是相对的,而不是.debug_frame节中的偏移。

  • 3、Version。1字节的CIE版本。在撰写本文时,该值为1或3。

  • 4、Augmentation String。扩充参数字符串,以NULL结尾。这是一个字符序列。非常老版本的gcc在这里使用字符串“ eh”,但我不会对此进行记录。这将在下面进一步描述。

  • 5、Code Alignment Factor。代码对齐因子,无符号LEB128(LEB128是数字的DWARF编码,在此不再赘述)。对于.eh_frame,该值应始终为1。

  • 6、Data Alignment Factor。数据对齐因子,带符号的LEB128。如.debug_frame中所示,这是偏移指令之外的常数。

  • 7、Return Address Register。返回地址寄存器。在CIE版本1中,这是一个字节。在CIE版本3中,这是未签名的LEB128。这表明框架表中的哪一列代表返回地址。

  • 8、下面的字段含义,取决于Augmentation String的定义:

    • 'z’可以作为字符串的第一个字符出现。如果存在,则应显示Augmentation Data字段。 扩展数据的内容应根据扩展字符串中的其他字符来解释。
      如果扩展字符串以’z’开头,下一个字段是一个无符号的LEB128即扩展数据的长度Augmentation Data Length,将其补齐以使CIE在地址边界处结束。如果看到无法识别的扩充字符,则用于跳到扩充数据augmentation data的末尾。

    • 字符串的第一个字符之后的任何位置都可能存在’L’。 仅当’z’是字符串的第一个字符时,才可以显示该字符。
      如果存在,则表示CIE的扩展数据Augmentation Data中存在一个参数argument,而FDE的扩展数据Augmentation Data中也存在相应的参数argument
      CIE的扩展数据中的参数为1字节,表示用于FDE的扩展数据中的参数的指针编码。这些编码是DW_EH_PE_xxx值(稍后描述),默认值为DW_EH_PE_absptr。
      FDE的扩展数据是特定于语言的数据区(LSDA)的地址,LSDA指针的大小由所使用的指针编码指定。

    • 字符串的第一个字符之后的任何位置都可以出现’R’。 仅当’z’是字符串的第一个字符时,才可以显示该字符。
      如果存在,则CIE的扩展数据应包括一个1字节的参数,该参数表示FDE中使用的地址指针的指针编码。这些是DW_EH_PE_xxx值。默认值为DW_EH_PE_absptr。

    • 扩充字符串中的字符’S’表示此CIE表示用于调用信号处理程序的堆栈帧。展开堆栈时,信号堆栈帧的处理方式略有不同:指令指针被假定为在下一条要执行的指令之前而不是在其之后。

    • 字符串的第一个字符之后的任何位置都可能存在“ P”。 仅当“ z”是字符串的第一个字符时,才可以显示该字符。
      如果存在,则表示在CIE的扩展数据中存在两个参数。
      第一个参数为1字节,代表用于第二个参数的指针编码(DW_EH_PE_xxx)。
      第二个参数是个性例程处理程序的地址。
      个性例程用于处理语言和特定于供应商的任务。 系统展开库接口通过指向个性例程的指针访问特定于语言的异常处理语义。 个性例程没有特定于ABI的名称。 个性例程指针的大小由所使用的指针编码指定。

  • 9、Initial Instructions。其余字节是DW_CFA_xxx操作码数组,它们定义帧表的初始值。然后,根据需要跟在DW_CFA_nop填充字节之后,以匹配CIE的总长度。

3.1.2 FDE 格式

FieldDescription
LengthRequired
Extended LengthOptional
CIE PointerRequired
PC BeginRequired
PC RangeRequired
Augmentation Data LengthOptional
Augmentation DataOptional
Call Frame InstructionsRequired
Padding

具体解析如下:

  • 1、Length。读取4个字节。如果它们不是0xffffffff,则它们是CIE或FDE记录的长度。否则,接下来的64位将保留长度,这是64位DWARF格式。就像.debug_frame。
  • 2、CIE Pointer。一个4字节无符号值,当从当前FDE中的CIE指针的偏移量中减去该值时,将得出关联的CIE起点的偏移量。此值绝不能为0。
  • 3、PC Begin。此FDE适用的PC起始地址。使用关联的CIE指定的FDE编码对它进行编码(FDE encoding)。
  • 4、PC Range。此FDE适用的PC起始地址之后的字节数。这是使用FDE编码进行编码的(FDE encoding)。
  • 5、Augmentation Data Length。该字段仅在CIE扩展字符串以“ z”开头的情况下出现。对应一个无符号LEB128,这是FDE扩展数据的总大小。这可用于跳过与无法识别的扩展字符关联的数据。
  • 6、Augmentation Data。如果CIE未将LSDA编码设置为DW_EH_PE_omit,则Augmentation Data中包含指向LSDA的指针,该指针由CIE指定编码(LSDA encoding)。
  • 7、Call Frame Instructions。FDE中的其余字节是DW_CFA_xxx操作码的数组,这些操作码在帧表中设置值以退回到调用方。

3.1.3 .eh_frame_hdr 格式

EncodingField
unsigned byteversion
unsigned byteeh_frame_ptr_enc
unsigned bytefde_count_enc
unsigned bytetable_enc
encodedeh_frame_ptr
encodedfde_count
binary search table

.eh_frame_hdr section包含.eh_frame的额外信息。

具体解析如下:

  • 1、eh_frame_ptr_enc。eh_frame_ptr字段的编码格式。
  • 2、fde_count_enc。fde_count字段的编码格式。DW_EH_PE_omit的值指示不存在二进制搜索表。
  • 3、table_enc。二进制搜索表中条目的编码格式。DW_EH_PE_omit的值指示不存在二进制搜索表。
  • 4、eh_frame_ptr。指向.eh_frame section开始指针。
  • 5、fde_count。二进制搜索表中条目的计数值。
  • 6、binary search table。二进制搜索表,包含fde_count个条目。每个条目包含两个值,初始位置和地址。这些条目按初始位置值的升序排列。

3.1.4 DWARF Exception Header Encoding

DWARF异常标头编码用于描述.eh_frame和.eh_frame_hdr部分中使用的数据类型。 高4位指示如何应用该值, 低4位表示数据格式。

  • 低4位:
NameValueMeaningDescript
DW_EH_PE_absptr0x00Pointer (long)The Value is a literal pointer whose size is determined by the architecture.
DW_EH_PE_uleb1280x01Unsigned LEB128Unsigned value is encoded using the Little Endian Base 128 (LEB128) as defined by DWARF Debugging Information Format, Revision 2.0.0.
DW_EH_PE_udata20x02A 2 bytes unsigned value.
DW_EH_PE_udata40x03A 4 bytes unsigned value.
DW_EH_PE_udata80x04An 8 bytes unsigned value.
DW_EH_PE_sleb1280x09Signed LEB128Signed value is encoded using the Little Endian Base 128 (LEB128) as defined by DWARF Debugging Information Format, Revision 2.0.0.
DW_EH_PE_sdata20x0AA 2 bytes signed value.
DW_EH_PE_sdata40x0BA 4 bytes signed value.
DW_EH_PE_sdata80x0CAn 8 bytes signed value.
  • 高4位:
NameValueMeaningDescript
DW_EH_PE_pcrel0x10Value is relative to the current program counter.值是相对于当前程序计数器的。
DW_EH_PE_textrel0x20Value is relative to the beginning of the .text section.值是相对于.text section的。
DW_EH_PE_datarel0x30Value is relative to the beginning of the .got or .eh_frame_hdr section.值是相对于.got或者.eh_frame_hdr section的。
DW_EH_PE_funcrel0x40Value is relative to the beginning of the function.值是相对于当前函数的。
DW_EH_PE_aligned0x50Value is aligned to an address unit sized boundary.值与地址单元大小的边界对齐。
  • 特殊值
NameValueMeaningDescript
DW_EH_PE_omit0xFFindicate that no value is present

3.2 .eh_frame 实例

一段简单的c语言代码:

$ cat test.c 
#include <stdio.h>

int test(int x)
{
        int c =10;

        return x*c;
}

void main()
{
        int a,b;

        a = 10;
        b = 11;

        printf("hello test~, %d\n", a+b);

        a = test(a+b);
}
$ gcc test.c

3.2.1 解析信息

可以使用readelf -wF xxx命令来查看elf文件中的.eh_frame解析信息:

$ readelf -wF a.out 
Contents of the .eh_frame section:

00000000 0000000000000014 00000000 CIE "zR" cf=1 df=-8 ra=16
   LOC           CFA      ra    
0000000000000000 rsp+8    u     

...

000000c8 0000000000000044 0000009c FDE cie=00000030 pc=00000000000006b0..0000000000000715
   LOC           CFA      rbx   rbp   r12   r13   r14   r15   ra    
00000000000006b0 rsp+8    u     u     u     u     u     u     c-8   
00000000000006b2 rsp+16   u     u     u     u     u     c-16  c-8   
00000000000006b4 rsp+24   u     u     u     u     c-24  c-16  c-8   
00000000000006b9 rsp+32   u     u     u     c-32  c-24  c-16  c-8   
00000000000006bb rsp+40   u     u     c-40  c-32  c-24  c-16  c-8   
00000000000006c3 rsp+48   u     c-48  c-40  c-32  c-24  c-16  c-8   
00000000000006cb rsp+56   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
00000000000006d8 rsp+64   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
000000000000070a rsp+56   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
000000000000070b rsp+48   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
000000000000070c rsp+40   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
000000000000070e rsp+32   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
0000000000000710 rsp+24   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
0000000000000712 rsp+16   c-56  c-48  c-40  c-32  c-24  c-16  c-8   
0000000000000714 rsp+8    c-56  c-48  c-40  c-32  c-24  c-16  c-8   

可以看到.eh_frame总体架构就是由CIEFDE组成的。其中最核心的就是FDE的组织,读懂它条目的所有字段基本就理解了unwind的含义:

在这里插入图片描述

CFA (Canonical Frame Address, which is the address of %rsp in the caller frame),CFA就是上一级调用者的堆栈指针。

上图详细说明了怎么样利用.eh_frame来进行栈回溯:

  • 1、根据当前的PC在.eh_frame中找到对应的条目,根据条目提供的各种偏移计算其他信息。
  • 2、首先根据CFA = rsp+4,把当前rsp+4得到CFA的值。再根据CFA的值计算出通用寄存器和返回地址在堆栈中的位置。
  • 3、通用寄存器栈位置计算。例如:rbx = CFA-56。
  • 4、返回地址ra的栈位置计算。ra = CFA-8。
  • 5、根据ra的值,重复步骤1到4,就形成了完整的栈回溯。

3.2.2 原始信息

也可以使用readelf -wf xxx命令来查看elf文件中的.eh_frame原始信息:

$ readelf -wf a.out 
...

000000c8 0000000000000044 0000009c FDE cie=00000030 pc=00000000000006b0..0000000000000715
  DW_CFA_advance_loc: 2 to 00000000000006b2
  DW_CFA_def_cfa_offset: 16
  DW_CFA_offset: r15 (r15) at cfa-16
  DW_CFA_advance_loc: 2 to 00000000000006b4
  DW_CFA_def_cfa_offset: 24
  DW_CFA_offset: r14 (r14) at cfa-24
  DW_CFA_advance_loc: 5 to 00000000000006b9
  DW_CFA_def_cfa_offset: 32
  DW_CFA_offset: r13 (r13) at cfa-32
  DW_CFA_advance_loc: 2 to 00000000000006bb
  DW_CFA_def_cfa_offset: 40
  DW_CFA_offset: r12 (r12) at cfa-40
  DW_CFA_advance_loc: 8 to 00000000000006c3
  DW_CFA_def_cfa_offset: 48
  DW_CFA_offset: r6 (rbp) at cfa-48
  DW_CFA_advance_loc: 8 to 00000000000006cb
  DW_CFA_def_cfa_offset: 56
  DW_CFA_offset: r3 (rbx) at cfa-56
  DW_CFA_advance_loc: 13 to 00000000000006d8
  DW_CFA_def_cfa_offset: 64
  DW_CFA_advance_loc: 50 to 000000000000070a
  DW_CFA_def_cfa_offset: 56
  DW_CFA_advance_loc: 1 to 000000000000070b
  DW_CFA_def_cfa_offset: 48
  DW_CFA_advance_loc: 1 to 000000000000070c
  DW_CFA_def_cfa_offset: 40
  DW_CFA_advance_loc: 2 to 000000000000070e
  DW_CFA_def_cfa_offset: 32
  DW_CFA_advance_loc: 2 to 0000000000000710
  DW_CFA_def_cfa_offset: 24
  DW_CFA_advance_loc: 2 to 0000000000000712
  DW_CFA_def_cfa_offset: 16
  DW_CFA_advance_loc: 2 to 0000000000000714
  DW_CFA_def_cfa_offset: 8
  DW_CFA_nop

这些信息就是.eh_frame的原始格式,是GAS(GCC Assembler)汇编编译器搜集汇编代码中所有的CFI伪指令汇总而成。

其中CIEFDE的格式在第一节中已经介绍,不好理解的是DW_CFA_*开头的这些指令,这些指令的具体含义可以查看DWARF6.4.2 Call Frame Instructions一节或者CFI(Call Frame Information)/ARM CFI的定义。

而上一节使用readelf -wF xxx命令解析了这些信息,我们初步看看对应关系:

在这里插入图片描述

4. CFI directives 详解

4.1 CFI 详解

在GAS(GCC Assembler)汇编编译器CFI(Call Frame Information)/ARM CFI文档中,对所有CFI伪指令的含义有详细描述。或者查看查看DWARF6.4.2 Call Frame Instructions一节。

我们介绍其中的一些重点指令:

  • .cfi_sections section_list

.cfi_sections用来描述产生的目标是.eh_frame section and/or .debug_frame section
如果section_list为.eh_frame,.eh_frame则产生,如果section_list为.debug_frame,.debug_frame则产生。两者同时产生.eh_frame, .debug_frame。如果不使用此指令,则默认值为.cfi_sections .eh_frame。

  • .cfi_startproc [simple]

.cfi_startproc用在每个函数的入口处。

  • .cfi_endproc

.cfi_endproc用在函数的结束处,和.cfi_startproc对应。

  • .cfi_def_cfa register, offset

用来定义CFA的计算规则:

CFA = register + offset

默认基址寄存器register = rsp。

x86_64的register编号从0-15对应下表。rbp的register编号为6,rsp的register编号为7。

%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15

完整x86_64的DWARF Register Number Mapping可以参考x86_64 ABI 3.7 Stack Unwind Algorithm一节。

  • .cfi_def_cfa_register register

用来修改修改CFA计算规则,基址寄存器从rsp转移到新的register

register = new register
  • .cfi_def_cfa_offset offset

用来修改修改CFA计算规则,基址寄存器不变,offset变化:

CFA = register + offset(new)
  • .cfi_adjust_cfa_offset offset

用来修改修改CFA计算规则,在上一个offset的基础上加上相对的offset:

CFA = register + pre_offset + offset(new)
  • .cfi_offset register, offset

寄存器register上一次值保存在CFA偏移offset的堆栈中:

*(CFA + offset) = register(pre_value)
  • .cfi_restore register

基址寄存器恢复成函数开始时的默认寄存器,默认是rsp

更多的指令请查看手册,或者在实际用例中理解。

4.2 CFI 实例

如果我们直接编写汇编代码,需要自己手工添加CFI伪指令,否者堆栈回溯信息会出错。例如:

kernel\arch\x86\kernel\entry_64.S:

ENTRY(system_call)
	CFI_STARTPROC	simple
	CFI_SIGNAL_FRAME
	CFI_DEF_CFA	rsp,KERNEL_STACK_OFFSET
	CFI_REGISTER	rip,rcx
	/*CFI_REGISTER	rflags,r11*/
	SWAPGS_UNSAFE_STACK

#define CFI_STARTPROC		.cfi_startproc
#define CFI_ENDPROC		.cfi_endproc
#define CFI_DEF_CFA		.cfi_def_cfa
#define CFI_DEF_CFA_REGISTER	.cfi_def_cfa_register
#define CFI_DEF_CFA_OFFSET	.cfi_def_cfa_offset
#define CFI_ADJUST_CFA_OFFSET	.cfi_adjust_cfa_offset
#define CFI_OFFSET		.cfi_offset
#define CFI_REL_OFFSET		.cfi_rel_offset
#define CFI_REGISTER		.cfi_register
#define CFI_RESTORE		.cfi_restore
#define CFI_REMEMBER_STATE	.cfi_remember_state
#define CFI_RESTORE_STATE	.cfi_restore_state
#define CFI_UNDEFINED		.cfi_undefined
#define CFI_SAME_VALUE		.cfi_same_value

我们使用c语言编写时,gcc会自动帮我们产生CFI伪指令:

gcc -S test.c           // c语言生成汇编代码
vim test.s              // 查看汇编代码

我们对汇编指令中的CFI伪指令做一个解析:

在这里插入图片描述

使用readelf -wF xxx读出对应FDE的信息:

000000a8 000000000000001c 0000007c FDE cie=00000030 pc=0000000000000661..00000000000006a7
   LOC           CFA      rbp   ra    
0000000000000661 rsp+8    u     c-8   
0000000000000662 rsp+16   c-16  c-8   
0000000000000665 rbp+16   c-16  c-8   
00000000000006a6 rsp+8    c-16  c-8   

5. kernel unwind 实现

在内核中已经有现成的代码实现了kernel代码的unwind方式栈回溯。

5.1 .eh_frame的加载

内核vmlinux也是一个elf文件,它编译完成后默认也生成了.eh_frame.eh_frame_hdrsection。这两个段运行的时候被一起加载到内存,运行的时候需要有方法能找到它们。

内核在链接脚本vmlinux.lds.h中制定了定义:__start_unwind_hdr__end_unwind_hdr变量用来标识.eh_frame_hdr的位置,__start_unwind__end_unwind变量用来标识.eh_frame的位置。

kernel\include\asm-generic\vmlinux.lds.h

#ifdef CONFIG_STACK_UNWIND
#define EH_FRAME							\
		/* Unwind data binary search table */			\
		. = ALIGN(8);						\
		.eh_frame_hdr : AT(ADDR(.eh_frame_hdr) - LOAD_OFFSET) {	\
			VMLINUX_SYMBOL(__start_unwind_hdr) = .;		\
			*(.eh_frame_hdr)				\
			VMLINUX_SYMBOL(__end_unwind_hdr) = .;		\
		}							\
		/* Unwind data */					\
		. = ALIGN(8);						\
		.eh_frame : AT(ADDR(.eh_frame) - LOAD_OFFSET) {		\
			VMLINUX_SYMBOL(__start_unwind) = .;		\
			*(.eh_frame)					\
			VMLINUX_SYMBOL(__end_unwind) = .;		\
		}
#else
#define EH_FRAME
#endif

在系统启动时,把内核的.eh_frame.eh_frame_hdrsection当成一张table管理起来:

start_kernel()

↓

void __init unwind_init(void)
{
	init_unwind_table(&root_table, "kernel",
	                  _text, _end - _text,
	                  NULL, 0,
	                  __start_unwind, __end_unwind - __start_unwind,
	                  __start_unwind_hdr, __end_unwind_hdr - __start_unwind_hdr);
}

↓

static void init_unwind_table(struct unwind_table *table,
                              const char *name,
                              const void *core_start,
                              unsigned long core_size,
                              const void *init_start,
                              unsigned long init_size,
                              const void *table_start,
                              unsigned long table_size,
                              const u8 *header_start,
                              unsigned long header_size)
{
	const u8 *ptr = header_start + 4;
	const u8 *end = header_start + header_size;

	table->core.pc = (unsigned long)core_start;
	table->core.range = core_size;
	table->init.pc = (unsigned long)init_start;
	table->init.range = init_size;
	table->address = table_start;
	table->size = table_size;
	/* See if the linker provided table looks valid. */
	if (header_size <= 4
	    || header_start[0] != 1
	    || (void *)read_pointer(&ptr, end, header_start[1], 0, 0)
	       != table_start
	    || !read_pointer(&ptr, end, header_start[2], 0, 0)
	    || !read_pointer(&ptr, end, header_start[3], 0,
	                     (unsigned long)header_start)
	    || !read_pointer(&ptr, end, header_start[3], 0,
	                     (unsigned long)header_start))
		header_start = NULL;
	table->hdrsz = header_size;
	smp_wmb();
	table->header = header_start;
	table->link = NULL;
	table->name = name;
}

驱动模块在加载的时候也需要把自己的.eh_frame加进来,接口函数unwind_add_table(),这样就形成了一张unwind信息table链表。

5.2 unwind解析

通常我们调用dump_stack()来打印出内核的当前调用栈,其具体实现如下:

dump_stack()
↓
show_trace() 
↓

void
show_trace_log_lvl(struct task_struct *task, struct pt_regs *regs,
		unsigned long *stack, unsigned long bp, char *log_lvl)
{
	printk("%sCall Trace:\n", log_lvl);
    /* 指定了ops为print_trace_ops */
	dump_trace(task, regs, stack, bp, &print_trace_ops, log_lvl);
}

↓
dump_trace() 
↓
try_stack_unwind()
↓

kernel\arch\x86\kernel\entry_64.S:

#ifdef CONFIG_STACK_UNWIND
/*
	%rdi 参数1:unwind_frame_info
	%rsi 参数2:unwind_callback_fn = dump_trace_unwind()
	%rdx 参数3:stacktrace_ops
	%rcx 参数4:void *data = NULL
 */
ENTRY(arch_unwind_init_running)
	CFI_STARTPROC
	movq	%r15, R15(%rdi)     /* 给参数1赋值:构造本进程的寄存器值 unwind_frame_info->regs */
	movq	%r14, R14(%rdi)
	xchgq	%rsi, %rdx
	movq	%r13, R13(%rdi)
	movq	%r12, R12(%rdi)
	xorl	%eax, %eax
	movq	%rbp, RBP(%rdi)
	movq	%rbx, RBX(%rdi)
	movq	(%rsp), %r9
	xchgq	%rdx, %rcx			/* 交换参数3和参数4 */
	movq	%rax, R11(%rdi)
	movq	%rax, R10(%rdi)
	movq	%rax, R9(%rdi)
	movq	%rax, R8(%rdi)
	movq	%rax, RAX(%rdi)
	movq	%rax, RCX(%rdi)
	movq	%rax, RDX(%rdi)
	movq	%rax, RSI(%rdi)
	movq	%rax, RDI(%rdi)
	movq	%rax, ORIG_RAX(%rdi)
	movq	%r9, RIP(%rdi)
	leaq	8(%rsp), %r9
	movq	$__KERNEL_CS, CS(%rdi)
	movq	%rax, EFLAGS(%rdi)
	movq	%r9, RSP(%rdi)
	movq	$__KERNEL_DS, SS(%rdi)
	jmpq	*%rcx               /* 使用构造好的参数来调用dump_trace_unwind() */
	CFI_ENDPROC
END(arch_unwind_init_running)
#endif

↓

int asmlinkage dump_trace_unwind(struct unwind_frame_info *info,
		      const struct stacktrace_ops *ops, void *data)
{
	int n = 0;
#ifdef CONFIG_STACK_UNWIND
	unsigned long sp = UNW_SP(info);

	if (arch_unw_user_mode(info))
		return -1;

    /* (1) 循环调用unwind()函数
        获取每一层堆栈的寄存器信息 info->regs
     */
	while (unwind(info) == 0 && UNW_PC(info)) {
		n++;

        /* (1.1) 调用ops->address来处理每一层的pc值 */
		ops->address(data, UNW_PC(info), 1);

        /* (1.2) 是否是用户态判断 */
		if (arch_unw_user_mode(info))
			break;
        
        /* (1.3) sp合法性判断 */
		if ((sp & ~(PAGE_SIZE - 1)) == (UNW_SP(info) & ~(PAGE_SIZE - 1))
		    && sp > UNW_SP(info))
			break;
		sp = UNW_SP(info);
	}
#endif
	return n;
}

最核心的函数unwind(),所有unwind的具体算法都在其中:

int unwind(struct unwind_frame_info *frame)
{
#define FRAME_REG(r, t) (((t *)frame)[reg_info[r].offs])
	const u32 *fde = NULL, *cie = NULL;
	const u8 *ptr = NULL, *end = NULL;
	unsigned long pc = UNW_PC(frame) - frame->call_frame, sp;
	unsigned long startLoc = 0, endLoc = 0, cfa;
	unsigned i;
	signed ptrType = -1;
	uleb128_t retAddrReg = 0;
	const struct unwind_table *table;
	struct unwind_state state;

	if (UNW_PC(frame) == 0)
		return -EINVAL;

    /* (1) 根据pc找到对应的unwind table
         查找PC对应的FDE
     */
	if ((table = find_table(pc)) != NULL
	    && !(table->size & (sizeof(*fde) - 1))) {
		const u8 *hdr = table->header;
		unsigned long tableSize;

		smp_rmb();

        /* (1.1) 根据`.eh_frame_hdr`查找到PC对应的FDE
            `.eh_frame_hdr`的格式
            --------------------------------------------------------------------
            Field               | expression
            --------------------------------------------------------------------
            version             | hdr[0]
            eh_frame_ptr_enc    | hdr[1]
            fde_count_enc       | hdr[2]
            table_enc           | hdr[3]
            eh_frame_ptr        | read_pointer(&ptr, end, hdr[1], 0, 0)
            fde_count           | read_pointer(&ptr, end, hdr[2], 0, 0))
            binary search table | (2 * tableSize)*fde_count ,(location, address)
            ---------------------------------------------------------------------
            
            `binary search table`是一张FDE的查询表:
                每张表有fde_count个条目
                每个条目有两个元素(location, address),每个元素的大小为tableSize
                表以降序来排序
         */
		if (hdr && hdr[0] == 1) {

            /* 根据`table_enc`计算出tableSize大小 */
			switch (hdr[3] & DW_EH_PE_FORM) {
			case DW_EH_PE_native: tableSize = sizeof(unsigned long); break;
			case DW_EH_PE_data2: tableSize = 2; break;
			case DW_EH_PE_data4: tableSize = 4; break;
			case DW_EH_PE_data8: tableSize = 8; break;
			default: tableSize = 0; break;
			}
			ptr = hdr + 4;
			end = hdr + table->hdrsz;
			if (tableSize
                /* 确认`eh_frame_ptr`指向的是`.eh_frame` */
			    && read_pointer(&ptr, end, hdr[1], 0, 0)
			       == (unsigned long)table->address

                /* 计算`fde_count`存储到i变量,并且进行合法性判断 */
			    && (i = read_pointer(&ptr, end, hdr[2], 0, 0)) > 0
			    && i == (end - ptr) / (2 * tableSize)
			    && !((end - ptr) % (2 * tableSize))) {

                /* 使用二分法在`binary search table`入口表中查询,
                    根据PC找到对应的FDE地址
                 */
				do {
					const u8 *cur = ptr + (i / 2) * (2 * tableSize);

					startLoc = read_pointer(&cur,
					                        cur + tableSize,
					                        hdr[3], 0,
					                        (unsigned long)hdr);
					if (pc < startLoc)
						i /= 2;
					else {
						ptr = cur - tableSize;
						i = (i + 1) / 2;
					}
				} while (startLoc && i > 1);
				if (i == 1
				    && (startLoc = read_pointer(&ptr,
				                                ptr + tableSize,
				                                hdr[3], 0,
				                                (unsigned long)hdr)) != 0
				    && pc >= startLoc)
					fde = (void *)read_pointer(&ptr,
					                           ptr + tableSize,
					                           hdr[3], 0,
					                           (unsigned long)hdr);
			}
		}
		if (hdr && !fde)
			dprintk(3, "Binary lookup for %lx failed.", pc);

        /* (1.2) 解析`FDE`,确认PC值是否在FDE的范围中
            --------------------------------------------------------------------
            Field                     | expression
            --------------------------|------------------------------------------
            Length	                  | fde
            CIE Pointer	              | (fde + 1)
            PC Begin	              | ptr = (const u8 *)(fde + 2);
                                      | read_pointer(&ptr, (const u8 *)(fde + 1) + *fde, ptrType, 0, 0)
            PC Range	              | read_pointer(&ptr, (const u8 *)(fde + 1) + *fde, ptrType, 0, 0)
            Augmentation Data Length  |
            Augmentation Data	      |
            Call Frame Instructions	  |
            Padding	                  |
            ---------------------------------------------------------------------
         */
		if (fde != NULL) {
            /* 根据fde中的指针找到对应cie */
			cie = cie_for_fde(fde, table);
			ptr = (const u8 *)(fde + 2);
			if (cie != NULL
			    && cie != &bad_cie
			    && cie != &not_fde
                /* 得到cie中的指针类型 */
			    && (ptrType = fde_pointer_type(cie)) >= 0
                /* 读出`PC Begin`,确认`.eh_frame_hdr`中的PC基址和FDE中的PC基址相等 */
			    && read_pointer(&ptr,
			                    (const u8 *)(fde + 1) + *fde,
			                    ptrType, 0, 0) == startLoc) {
				if (!(ptrType & DW_EH_PE_indirect))
					ptrType &= DW_EH_PE_FORM|DW_EH_PE_signed;

                /* 读出`PC Range`,得到FDE的结束PC值 */
				endLoc = startLoc
				         + read_pointer(&ptr,
				                        (const u8 *)(fde + 1) + *fde,
				                        ptrType, 0, 0);
				if (pc >= endLoc)
					fde = NULL;
			} else
				fde = NULL;
			if (!fde)
				dprintk(1, "Binary lookup result for %lx discarded.", pc);
		}

        /* (1.3) 如果从`.eh_frame_hdr`中查找PC对应的FDE失败 (快路径)
                尝试直接从`.eh_frame`中查找PC对应的FDE (慢路径)
         */
		if (fde == NULL) {
			for (fde = table->address, tableSize = table->size;
			     cie = NULL, tableSize > sizeof(*fde)
			     && tableSize - sizeof(*fde) >= *fde;
			     tableSize -= sizeof(*fde) + *fde,
			     fde += 1 + *fde / sizeof(*fde)) {
				cie = cie_for_fde(fde, table);
				if (cie == &bad_cie) {
					cie = NULL;
					break;
				}
				if (cie == NULL
				    || cie == &not_fde
				    || (ptrType = fde_pointer_type(cie)) < 0)
					continue;
				ptr = (const u8 *)(fde + 2);
				startLoc = read_pointer(&ptr,
				                        (const u8 *)(fde + 1) + *fde,
				                        ptrType, 0, 0);
				if (!startLoc)
					continue;
				if (!(ptrType & DW_EH_PE_indirect))
					ptrType &= DW_EH_PE_FORM|DW_EH_PE_signed;
				endLoc = startLoc
				         + read_pointer(&ptr,
				                        (const u8 *)(fde + 1) + *fde,
				                        ptrType, 0, 0);
				if (pc >= startLoc && pc < endLoc)
					break;
			}
			if (!fde)
				dprintk(3, "Linear lookup for %lx failed.", pc);
		}
	}

    /* (2) 解析出`CIE`各字段
        --------------------------------------------------------------------
        Field                     | expression
        --------------------------|------------------------------------------
        Length	                  | cie
        CIE ID	                  | (cie + 1)
        Version	                  | ptr = (const u8 *)(cie + 2)
        Augmentation String	      | 
        Code Alignment Factor	  |
        Data Alignment Factor	  |
        Return Address Register	  |
        Augmentation Data Length  |
        Augmentation Data	      |
        Initial Instructions	  |
        Padding	                  |
        ---------------------------------------------------------------------
        (2.1) 解析出CIE中的`Length`、`CIE ID`、`Version`、`Augmentation String`这几个字段
     */
	if (cie != NULL) {
		memset(&state, 0, sizeof(state));
		state.cieEnd = ptr; /* keep here temporarily */
		ptr = (const u8 *)(cie + 2);
		end = (const u8 *)(cie + 1) + *cie;
		frame->call_frame = 1;
        /* 解析出`Version` */
		if ((state.version = *ptr) != 1)
			cie = NULL; /* unsupported version */
        /* 解析出`Augmentation String` */
		else if (*++ptr) {
			/* check if augmentation size is first (and thus present) */
			if (*ptr == 'z') {
				while (++ptr < end && *ptr) {
					switch (*ptr) {
					/* check for ignorable (or already handled)
					 * nul-terminated augmentation string */
					case 'L':
					case 'P':
					case 'R':
						continue;
					case 'S':
						frame->call_frame = 0;
						continue;
					default:
						break;
					}
					break;
				}
			}
			if (ptr >= end || *ptr)
				cie = NULL;
		}
		if (!cie)
			dprintk(1, "CIE unusable (%p,%p).", ptr, end);
		++ptr;
	}

    /* (2.2) 解析出CIE中的`Code Alignment Factor`、`Data Alignment Factor`、
        `Return Address Register`、`Augmentation Data Length`、`Augmentation Data`这几个字段
     */
	if (cie != NULL) {
		/* get code aligment factor */
        /* 解析出`Code Alignment Factor` */
		state.codeAlign = get_uleb128(&ptr, end);

		/* get data aligment factor */
        /* 解析出`Data Alignment Factor` */
		state.dataAlign = get_sleb128(&ptr, end);
		if (state.codeAlign == 0 || state.dataAlign == 0 || ptr >= end)
			cie = NULL;
		else if (UNW_PC(frame) % state.codeAlign
		         || UNW_SP(frame) % sleb128abs(state.dataAlign)) {
			dprintk(1, "Input pointer(s) misaligned (%lx,%lx).",
			        UNW_PC(frame), UNW_SP(frame));
			return -EPERM;
		} else {
            /* 解析出`Return Address Register` */
			retAddrReg = state.version <= 1 ? *ptr++ : get_uleb128(&ptr, end);

			/* skip augmentation */
            /* 解析出`Augmentation Data Length` */
			if (((const char *)(cie + 2))[1] == 'z') {
				uleb128_t augSize = get_uleb128(&ptr, end);

				ptr += augSize;
			}
			if (ptr > end
			    || retAddrReg >= ARRAY_SIZE(reg_info)
			    || REG_INVALID(retAddrReg)
			    || reg_info[retAddrReg].width != sizeof(unsigned long))
				cie = NULL;
		}
		if (!cie)
			dprintk(1, "CIE validation failed (%p,%p).", ptr, end);
	}

    /* (2.3) 解析出CIE中的`Initial Instructions`位置
			 和FDE中的`Call Frame Instructions`位置
     */
	if (cie != NULL) {
		state.cieStart = ptr;                   /* state.cieStart: CIE的`Initial Instructions`开始 */
		ptr = state.cieEnd;                     
		state.cieEnd = end;                     /* state.cieEnd: CIE的`Initial Instructions`结束 */
		end = (const u8 *)(fde + 1) + *fde;     /* end: FDE的`Call Frame Instructions`结束 */
		/* skip augmentation */
		if (((const char *)(cie + 2))[1] == 'z') {
			uleb128_t augSize = get_uleb128(&ptr, end);

			if ((ptr += augSize) > end)         /* ptr: FDE的`Call Frame Instructions`开始 */
				fde = NULL;
		}
		if (!fde)
			dprintk(1, "FDE validation failed (%p,%p).", ptr, end);
	}

    /* (3) 如果CIE和FDE任一为空,出错返回 */
	if (cie == NULL || fde == NULL) {
#ifdef CONFIG_FRAME_POINTER
		unsigned long top = TSK_STACK_TOP(frame->task);
		unsigned long bottom = STACK_BOTTOM(frame->task);
		unsigned long fp = UNW_FP(frame);
		unsigned long sp = UNW_SP(frame);
		unsigned long link;

		if ((sp | fp) & (sizeof(unsigned long) - 1))
			return -EPERM;

# if FRAME_RETADDR_OFFSET < 0
		if (!(sp < top && fp <= sp && bottom < fp))
# else
		if (!(sp > top && fp >= sp && bottom > fp))
# endif
			return -ENXIO;

		if (probe_kernel_address(fp + FRAME_LINK_OFFSET, link))
			return -ENXIO;

# if FRAME_RETADDR_OFFSET < 0
		if (!(link > bottom && link < fp))
# else
		if (!(link < bottom && link > fp))
# endif
			return -ENXIO;

		if (link & (sizeof(link) - 1))
			return -ENXIO;

		fp += FRAME_RETADDR_OFFSET;
		if (probe_kernel_address(fp, UNW_PC(frame)))
			return -ENXIO;

		/* Ok, we can use it */
# if FRAME_RETADDR_OFFSET < 0
		UNW_SP(frame) = fp - sizeof(UNW_PC(frame));
# else
		UNW_SP(frame) = fp + sizeof(UNW_PC(frame));
# endif
		UNW_FP(frame) = link;
		return 0;
#else
		return -ENXIO;
#endif
	}

    /* (4) 解析CIE的`Initial Instructions`中的cfa指令 和
            FDE的`Call Frame Instructions`中的cfa指令
			解析结果放到state数据结构中
     */
	state.org = startLoc;
	memcpy(&state.cfa, &badCFA, sizeof(state.cfa));
	/* process instructions */
	if (!processCFI(ptr, end, pc, ptrType, &state)
	    || state.loc > endLoc
	    || state.regs[retAddrReg].where == Nowhere
	    || state.cfa.reg >= ARRAY_SIZE(reg_info)
	    || reg_info[state.cfa.reg].width != sizeof(unsigned long)
	    || FRAME_REG(state.cfa.reg, unsigned long) % sizeof(unsigned long)
	    || state.cfa.offs % sizeof(unsigned long)) {
		dprintk(1, "Unusable unwind info (%p,%p).", ptr, end);
		return -EIO;
	}

	/* update frame */
    /* (5) 信号相关的帧处理 */
#ifndef CONFIG_AS_CFI_SIGNAL_FRAME
	if (frame->call_frame
	    && !UNW_DEFAULT_RA(state.regs[retAddrReg], state.dataAlign))
		frame->call_frame = 0;
#endif

    /* (6) 首先根据指令解析结果,计算cfa的值:
         CFA = register + offset
     */
	cfa = FRAME_REG(state.cfa.reg, unsigned long) + state.cfa.offs;
	startLoc = min((unsigned long)UNW_SP(frame), cfa);
	endLoc = max((unsigned long)UNW_SP(frame), cfa);
	if (STACK_LIMIT(startLoc) != STACK_LIMIT(endLoc)) {
		startLoc = min(STACK_LIMIT(cfa), cfa);
		endLoc = max(STACK_LIMIT(cfa), cfa);
	}
#ifndef CONFIG_64BIT
# define CASES CASE(8); CASE(16); CASE(32)
#else
# define CASES CASE(8); CASE(16); CASE(32); CASE(64)
#endif
	pc = UNW_PC(frame);
	sp = UNW_SP(frame);

    /* (7) 再根据指令解析结果更新寄存器集的值
            
       (7.1) 第1遍轮询解析
            Register类型:state.regs[i].value = FRAME_REG(state.regs[i].value)
     */
	for (i = 0; i < ARRAY_SIZE(state.regs); ++i) {
		if (REG_INVALID(i)) {
			if (state.regs[i].where == Nowhere)
				continue;
			dprintk(1, "Cannot restore register %u (%d).",
			        i, state.regs[i].where);
			return -EIO;
		}
		switch (state.regs[i].where) {
		default:
			break;
		case Register:
			if (state.regs[i].value >= ARRAY_SIZE(reg_info)
			    || REG_INVALID(state.regs[i].value)
			    || reg_info[i].width > reg_info[state.regs[i].value].width) {
				dprintk(1, "Cannot restore register %u from register %lu.",
				        i, state.regs[i].value);
				return -EIO;
			}
			switch (reg_info[state.regs[i].value].width) {
#define CASE(n) \
			case sizeof(u##n): \
				state.regs[i].value = FRAME_REG(state.regs[i].value, \
				                                const u##n); \
				break
			CASES;
#undef CASE
			default:
				dprintk(1, "Unsupported register size %u (%lu).",
				        reg_info[state.regs[i].value].width,
				        state.regs[i].value);
				return -EIO;
			}
			break;
		}
	}

    /* (7.2) 第2遍轮询解析
     */
	for (i = 0; i < ARRAY_SIZE(state.regs); ++i) {
		if (REG_INVALID(i))
			continue;
		switch (state.regs[i].where) {

        /* Nowhere类型:UNW_SP(frame) = cfa; */
		case Nowhere:
			if (reg_info[i].width != sizeof(UNW_SP(frame))
			    || &FRAME_REG(i, __typeof__(UNW_SP(frame)))
			       != &UNW_SP(frame))
				continue;
			UNW_SP(frame) = cfa;
			break;

        /* Register类型:把第一轮计算出来的寄存器的值state.regs[i].value,
						更新完到FRAME_REG(i)结构中
		 */
		case Register:
			switch (reg_info[i].width) {
#define CASE(n) case sizeof(u##n): \
				FRAME_REG(i, u##n) = state.regs[i].value; \
				break
			CASES;
#undef CASE
			default:
				dprintk(1, "Unsupported register size %u (%u).",
				        reg_info[i].width, i);
				return -EIO;
			}
			break;
        
        /* Value类型:
			reg =  CFA +  (factored offset * data_alignment_factor)
		 */
		case Value:
			if (reg_info[i].width != sizeof(unsigned long)) {
				dprintk(1, "Unsupported value size %u (%u).",
				        reg_info[i].width, i);
				return -EIO;
			}
			FRAME_REG(i, unsigned long) = cfa + state.regs[i].value
			                                    * state.dataAlign;
			break;

        /* Memory类型:
			reg =  *(CFA +  (factored offset * data_alignment_factor))
		 */
		case Memory: {
				unsigned long addr = cfa + state.regs[i].value
				                           * state.dataAlign;

				if ((state.regs[i].value * state.dataAlign)
				    % sizeof(unsigned long)
				    || addr < startLoc
				    || addr + sizeof(unsigned long) < addr
				    || addr + sizeof(unsigned long) > endLoc) {
					dprintk(1, "Bad memory location %lx (%lx).",
					        addr, state.regs[i].value);
					return -EIO;
				}
				switch (reg_info[i].width) {
#define CASE(n)			case sizeof(u##n): \
					if (probe_kernel_address(addr, \
								 FRAME_REG(i, u##n))) \
						return -EFAULT; \
					break
				CASES;
#undef CASE
				default:
					dprintk(1, "Unsupported memory size %u (%u).",
					        reg_info[i].width, i);
					return -EIO;
				}
			}
			break;
		}
	}

	if (UNW_PC(frame) % state.codeAlign
	    || UNW_SP(frame) % sleb128abs(state.dataAlign)) {
		dprintk(1, "Output pointer(s) misaligned (%lx,%lx).",
		        UNW_PC(frame), UNW_SP(frame));
		return -EIO;
	}
	if (pc == UNW_PC(frame) && sp == UNW_SP(frame)) {
		dprintk(1, "No progress (%lx,%lx).", pc, sp);
		return -EIO;
	}

	return 0;
#undef CASES
#undef FRAME_REG
}

↓

static int processCFI(const u8 *start,
                      const u8 *end,
                      unsigned long targetLoc,
                      signed ptrType,
                      struct unwind_state *state)
{
	union {
		const u8 *p8;
		const u16 *p16;
		const u32 *p32;
	} ptr;
	int result = 1;

    /* (4.1) 先递归的解析CIE中的cfa指令 */
	if (start != state->cieStart) {
		state->loc = state->org;
		result = processCFI(state->cieStart, state->cieEnd, 0, ptrType, state);
		if (targetLoc == 0 && state->label == NULL)
			return result;
	}

    /* (4.2) 再解析FDE中的cfa指令
		这段指令解析请参考本文的`2.2 Call Frame Instructions`一节,就非常好理解了
	 */
	for (ptr.p8 = start; result && ptr.p8 < end; ) {
		switch (*ptr.p8 >> 6) {
			uleb128_t value;

		case 0:
			switch (*ptr.p8++) {

            /* (4.2.1) 更新PC位置到:state->loc */
			case DW_CFA_nop:
				break;
			case DW_CFA_set_loc:
				/* 计算:Location = Address */
				state->loc = read_pointer(&ptr.p8, end, ptrType, 0, 0);
				if (state->loc == 0)
					result = 0;
				break;
			case DW_CFA_advance_loc1:
				/* 计算:Location += (delta * code_alignment_factor) */
				result = ptr.p8 < end && advance_loc(*ptr.p8++, state);
				break;
			case DW_CFA_advance_loc2:
				/* 计算:Location += (delta * code_alignment_factor) */
				result = ptr.p8 <= end + 2
				         && advance_loc(*ptr.p16++, state);
				break;
			case DW_CFA_advance_loc4:
				/* 计算:Location += (delta * code_alignment_factor) */
				result = ptr.p8 <= end + 4
				         && advance_loc(*ptr.p32++, state);
				break;

            /* (4.2.2) 更新通用寄存器的值:state->regs[reg].where/value */
			case DW_CFA_offset_extended:
				/*  计算 rule:offset(N)
					reg num = *(CFA + (factored offset * data_alignment_factor))
				 */
				value = get_uleb128(&ptr.p8, end);
				set_rule(value, Memory, get_uleb128(&ptr.p8, end), state);
				break;
			case DW_CFA_val_offset:
				/*  计算 rule:val_offset(N)
					reg num = CFA + (factored offset * data_alignment_factor)
				 */
				value = get_uleb128(&ptr.p8, end);
				set_rule(value, Value, get_uleb128(&ptr.p8, end), state);
				break;
			case DW_CFA_offset_extended_sf:
				/*  计算 rule:offset(N)
					reg num = *(CFA + (factored offset * data_alignment_factor))
				 */
				value = get_uleb128(&ptr.p8, end);
				set_rule(value, Memory, get_sleb128(&ptr.p8, end), state);
				break;
			case DW_CFA_val_offset_sf:
				/*  计算 rule:val_offset(N)
					reg num = CFA + (factored offset * data_alignment_factor)
				 */
				value = get_uleb128(&ptr.p8, end);
				set_rule(value, Value, get_sleb128(&ptr.p8, end), state);
				break;
			case DW_CFA_restore_extended:
			case DW_CFA_undefined:
			case DW_CFA_same_value:
				set_rule(get_uleb128(&ptr.p8, end), Nowhere, 0, state);
				break;
			case DW_CFA_register:
				/*  计算 rule:register(R)
					reg num = R (second operands)
				 */
				value = get_uleb128(&ptr.p8, end);
				set_rule(value,
				         Register,
				         get_uleb128(&ptr.p8, end), state);
				break;

            /* (4.2.3) 寄存器集的存栈和恢复 */
			case DW_CFA_remember_state:
				/*  本次是寄存器集恢复动作:重新解析指令一直恢复到label处 */
				if (ptr.p8 == state->label) {
					state->label = NULL;
					return 1;
				}
				if (state->stackDepth >= MAX_STACK_DEPTH) {
					dprintk(1, "State stack overflow (%p,%p).", ptr.p8, end);
					return 0;
				}
				/* 本次是寄存器集存栈动作:把当前location存入到堆栈中 */
				state->stack[state->stackDepth++] = ptr.p8;
				break;
			case DW_CFA_restore_state:
				if (state->stackDepth) {
					const uleb128_t loc = state->loc;
					const u8 *label = state->label;

					/* 碰到需要恢复寄存器的情况:
						1、从堆栈中弹出需要恢复到哪一步的label
						2、把cfa和regs寄存器集的值都设置成初始状态
						3、从头重新开始解析,直到label处为止
						4、寄存器集就恢复到了之前`DW_CFA_remember_state`指令保存的状态
					 */
					state->label = state->stack[state->stackDepth - 1];
					memcpy(&state->cfa, &badCFA, sizeof(state->cfa));
					memset(state->regs, 0, sizeof(state->regs));
					state->stackDepth = 0;
					result = processCFI(start, end, 0, ptrType, state);
					state->loc = loc;
					state->label = label;
				} else {
					dprintk(1, "State stack underflow (%p,%p).", ptr.p8, end);
					return 0;
				}
				break;

            /* (4.2.4) cfa值的更新:state->cfa 
				更新cfa计算公式的两个变量register和offset
				通常情况下:CFA = regitser + offset
			 */
			case DW_CFA_def_cfa:
				/* 同时更新register和offset:
					register = new register
					offset = new offset 
				 */
				state->cfa.reg = get_uleb128(&ptr.p8, end);
				/*nobreak*/
			case DW_CFA_def_cfa_offset:
				/* 只更新offset:
					offset = new offset 
				 */
				state->cfa.offs = get_uleb128(&ptr.p8, end);
				break;
			case DW_CFA_def_cfa_sf:
				/* 同时更新register和offset:
					register = new register
					offset = (new offset * data_alignment_factor)
				 */
				state->cfa.reg = get_uleb128(&ptr.p8, end);
				/*nobreak*/
			case DW_CFA_def_cfa_offset_sf:
				/* 只更新offset:
					offset = (new offset * data_alignment_factor)
				 */
				state->cfa.offs = get_sleb128(&ptr.p8, end)
				                  * state->dataAlign;
				break;
			case DW_CFA_def_cfa_register:
				/* 只更新register:
					register = new register
				 */
				state->cfa.reg = get_uleb128(&ptr.p8, end);
				break;

			/* (4.2.5) 对 DWARF expression的处理,没看明白  */
			/*todo case DW_CFA_def_cfa_expression: */
			/*todo case DW_CFA_expression: */
			/*todo case DW_CFA_val_expression: */
			case DW_CFA_GNU_args_size:
				get_uleb128(&ptr.p8, end);
				break;
			case DW_CFA_GNU_negative_offset_extended:
				value = get_uleb128(&ptr.p8, end);
				set_rule(value,
				         Memory,
				         (uleb128_t)0 - get_uleb128(&ptr.p8, end), state);
				break;
			case DW_CFA_GNU_window_save:
			default:
				dprintk(1, "Unrecognized CFI op %02X (%p,%p).", ptr.p8[-1], ptr.p8 - 1, end);
				result = 0;
				break;
			}
			break;
        
        /* DW_CFA_advance_loc */
		case 1:
			/* 计算:Location += (delta * code_alignment_factor) */
			result = advance_loc(*ptr.p8++ & 0x3f, state);
			break;
        /* DW_CFA_offset */
		case 2:
			/*  计算 rule:offset(N)
				reg num = *(CFA + (factored offset * data_alignment_factor))
			 */
			value = *ptr.p8++ & 0x3f;
			set_rule(value, Memory, get_uleb128(&ptr.p8, end), state);
			break;
		/* DW_CFA_restore */
		case 3:
			/*  计算 rule:initial_instructions in the CIE
				reg num = initial_instructions in the CIE
			 */
			set_rule(*ptr.p8++ & 0x3f, Nowhere, 0, state);
			break;
		}
		if (ptr.p8 > end) {
			dprintk(1, "Data overrun (%p,%p).", ptr.p8, end);
			result = 0;
		}

        /* (4.2.6) 读取fde的条目,直到大于PC值的位置 */
		if (result && targetLoc != 0 && targetLoc < state->loc)
			return 1;
	}

	if (result && ptr.p8 < end)
		dprintk(1, "Data underrun (%p,%p).", ptr.p8, end);

	return result
	       && ptr.p8 == end
	       && (targetLoc == 0
	           || (/*todo While in theory this should apply, gcc in practice omits
	                 everything past the function prolog, and hence the location
	                 never reaches the end of the function.
	               targetLoc < state->loc &&*/ state->label == NULL));
}

6. 用户态常见取栈方法

6.1 gcc取栈

gcc提供了__builtin_return_address() 宏来做栈的回溯:

#include <stdio.h>

void do_backtrace()
{
    void *pc0 = __builtin_return_address(0);
    void *pc1 = __builtin_return_address(1);
    //void *pc2 = __builtin_return_address(2);
    //void *pc3 = __builtin_return_address(3);

    printf("Frame 0: PC=%p\n", pc0);
    printf("Frame 1: PC=%p\n", pc1);
    //printf("Frame 2: PC=%p\n", pc2);
    //printf("Frame 3: PC=%p\n", pc3);
}

int main()
{
    do_backtrace();

}

编译并运行:

> gcc gcc_backtrace.c 
> ./a.out 
Frame 0: PC=0x400590
Frame 1: PC=0x7f4052ab8c36

6.2 glibc取栈

glibc提供了一对函数backtrace()和backtrace_symbols()来回溯栈信息:

#include <stdio.h>
#include <execinfo.h>

#define BACKTRACE_SIZ   64
void do_backtrace()
{
    void    *array[BACKTRACE_SIZ];
    size_t   size, i;
    char   **strings;

    size = backtrace(array, BACKTRACE_SIZ);
    strings = backtrace_symbols(array, size);

    for (i = 0; i < size; i++) {
        printf("%p : %s\n", array[i], strings[i]);
    }

    free(strings);  // malloced by backtrace_symbols
}

int  main()
{
    do_backtrace();
    return 0;
}

编译并运行:

> gcc glibc_backtrace.c
> ./a.out 
0x400646 : ./a.out() [0x400646]
0x4006c0 : ./a.out() [0x4006c0]
0x7fd564f97c36 : /lib64/libc.so.6(__libc_start_main+0xe6) [0x7fd564f97c36]
0x400569 : ./a.out() [0x400569]

编译时使用-rdynamic把调试信息链接进文件,运行会打印出更详细的符号信息:

> gcc -g -rdynamic glibc_backtrace.c
> ./a.out 
0x4008d6 : ./a.out(do_backtrace+0x1c) [0x4008d6]
0x400950 : ./a.out(main+0xe) [0x400950]
0x7f9ed1f6bc36 : /lib64/libc.so.6(__libc_start_main+0xe6) [0x7f9ed1f6bc36]
0x4007f9 : ./a.out() [0x4007f9]

gcc的-grdynamic选项:

  • gcc的-g,应该没有人不知道它是一个调试选项,因此在一般需要进行程序调试的场景下,我们都会加上该选项,并且根据调试工具的不同,还能直接选择更有针对性的说明,比如-ggdb。-g是一个编译选项,即在源代码编译的过程中起作用,让gcc把更多调试信息(也就包括符号信息)收集起来并将存放到最终的可执行文件内。
  • -rdynamic却是一个连接选项,它将指示连接器把所有符号(而不仅仅只是程序已使用到的外部符号,但不包括静态符号,比如被static修饰的函数)都添加到动态符号表(即.dynsym表)里,以便那些通过dlopen()或backtrace()(这一系列函数使用.dynsym表内符号)这样的函数使用。

6.3 libunwind取栈

安装libunwind:

// suse11sp4
sudo zypper install libunwind libunwind-devel

// ubuntu18.04
sudo apt-get install libunwind-dev

例程:

#include <stdio.h>
#define UNW_LOCAL_ONLY
#include <libunwind.h>

void do_backtrace()
{
    unw_cursor_t    cursor;
    unw_context_t   context;

    unw_getcontext(&context);
    unw_init_local(&cursor, &context);

    while (unw_step(&cursor) > 0) {
        unw_word_t  offset, pc;
        char        fname[64];

        unw_get_reg(&cursor, UNW_REG_IP, &pc);

        fname[0] = '\0';
        (void) unw_get_proc_name(&cursor, fname, sizeof(fname), &offset);

        printf ("%p : (%s+0x%x) [%p]\n", pc, fname, offset, pc);
    }
}

int main()
{
    do_backtrace();

}

编译并运行:

$ gcc -g libunwind_backtrace.c -lunwind
$ ./a.out 
0x55e6635819cb : (main+0xe) [0x55e6635819cb]
0x7f8f4e88db97 : (__libc_start_main+0xe7) [0x7f8f4e88db97]
0x55e6635817fa : (_start+0x2a) [0x55e6635817fa]

libunwind提供了更多能力来检查每帧的程序状态。 例如,可以打印保存的寄存器值:

void do_backtrace2()
{
    unw_cursor_t    cursor;
    unw_context_t   context;

    unw_getcontext(&context);
    unw_init_local(&cursor, &context);
    while (unw_step(&cursor) > 0) {
        unw_word_t  offset;
        unw_word_t  pc, eax, ebx, ecx, edx;
        char        fname[64];

        unw_get_reg(&cursor, UNW_REG_IP,  &pc);
        unw_get_reg(&cursor, UNW_X86_64_RAX, &eax);
        unw_get_reg(&cursor, UNW_X86_64_RDX, &edx);
        unw_get_reg(&cursor, UNW_X86_64_RCX, &ecx);
        unw_get_reg(&cursor, UNW_X86_64_RBX, &ebx);

        fname[0] = '\0';
        unw_get_proc_name(&cursor, fname, sizeof(fname), &offset);
        printf ("%p : (%s+0x%x) [%p]\n", pc, fname, offset, pc);
        printf("\tEAX=0x%08x EDX=0x%08x ECX=0x%08x EBX=0x%08x\n",
                eax, edx, ecx, ebx);
    }
}

编译并运行:

$ gcc libunwind_bt.c  -lunwind
$ ./a.out 
0x55e24c1f7b50 : (main+0xe) [0x55e24c1f7b50]
        EAX=0xe8a1c4e0 EDX=0x7f6977ca ECX=0x4c1f7b60 EBX=0x00000000
0x7f027f095b97 : (__libc_start_main+0xe7) [0x7f027f095b97]
        EAX=0xe8a1c4e0 EDX=0x7f6977ca ECX=0x4c1f7b60 EBX=0x00000000
0x55e24c1f77fa : (_start+0x2a) [0x55e24c1f77fa]
        EAX=0xe8a1c4e0 EDX=0x7f6977ca ECX=0x4c1f7b60 EBX=0x00000000

相关资源:libunwind projectlibunwind with Android ARM support

参考文档:

1.CFI directives introduce
2..eh_frame section
3.DWARF Version 4
4..eh_frame
5.ARM-Unwinding-Tutorial
6.ARM-Directives
7.CFI-directives
8.linux c 及 c++打印调用者函数caller function的方法,包括arm c平台
9.gcc选项-g与-rdynamic的异同
10.X86系列CPU标准寄存器
11.x86-64 下函数调用及栈帧原理
12.Exploiting the Hard-Working DWARF
13.ELF文件装载链接过程及hook原理
14.Linux ELF文件和VMA间的关系
15.unread
16.DWARF Extensions

posted @ 2020-07-24 21:03  pwl999  阅读(2794)  评论(0编辑  收藏  举报