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)

Instructions Descript Descript
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計算規則。

Instructions Descript Descript
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)寄存器的恢復規則。

寄存器規則包含以下:

Rule Descript Descript
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.

具體包含以下指令:

Instructions Rule Descript Descript
DW_CFA_undefined undefined 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_value same 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_offset offset(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_extended offset(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_sf offset(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_offset val_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_sf val_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_register register(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_expression expression(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_expression val_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_restore initial_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_extended initial_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)完整寄存器狀態的能力。

Instructions Descript Descript
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
Instructions Descript Descript
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
Instructions Descript Descript
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

各個指令具體的指令碼編碼如下:

Instruction High 2Bits Low 6 Bits Operand 1 Operand 2
DW_CFA_advance_loc 0x1 delta
DW_CFA_offset 0x2 register ULEB128 offset
DW_CFA_restore 0x3 register
DW_CFA_nop 0 0
DW_CFA_set_loc 0 0x01 address
DW_CFA_advance_loc1 0 0x02 1-byte delta
DW_CFA_advance_loc2 0 0x03 2-byte delta
DW_CFA_advance_loc4 0 0x04 4-byte delta
DW_CFA_offset_extended 0 0x05 ULEB128 register ULEB128 offset
DW_CFA_restore_extended 0 0x06 ULEB128 register
DW_CFA_undefined 0 0x07 ULEB128 register
DW_CFA_same_value 0 0x08 ULEB128 register
DW_CFA_register 0 0x09 ULEB128 register ULEB128 register
DW_CFA_remember_state 0 0x0a
DW_CFA_restore_state 0 0x0b
DW_CFA_def_cfa 0 0x0c ULEB128 register ULEB128 offset
DW_CFA_def_cfa_register 0 0x0d ULEB128 register
DW_CFA_def_cfa_offset 0 0x0e ULEB128 offset
DW_CFA_def_cfa_expression 0 0x0f BLOCK
DW_CFA_expression 0 0x10 ULEB128 register BLOCK
DW_CFA_offset_extended_sf 0 0x11 ULEB128 register SLEB128 offset
DW_CFA_def_cfa_sf 0 0x12 ULEB128 register SLEB128 offset
DW_CFA_def_cfa_offset_sf 0 0x13 SLEB128 offset
DW_CFA_val_offset 0 0x14 ULEB128 ULEB128
DW_CFA_val_offset_sf 0 0x15 ULEB128 SLEB128
DW_CFA_val_expression 0 0x16 ULEB128 BLOCK
DW_CFA_lo_user 0 0x1c
DW_CFA_GNU_args_size 0 0x2e ULEB128
DW_CFA_GNU_negative_offset_extended 0 0x2f ULEB128 ULEB128
DW_CFA_hi_user 0 0x3f

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 格式

Field Description
Length Required
Extended Length Optional
CIE ID Required
Version Required
Augmentation String Required
Code Alignment Factor Required
Data Alignment Factor Required
Return Address Register Required
Augmentation Data Length Optional
Augmentation Data Optional
Initial Instructions Required
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 格式

Field Description
Length Required
Extended Length Optional
CIE Pointer Required
PC Begin Required
PC Range Required
Augmentation Data Length Optional
Augmentation Data Optional
Call Frame Instructions Required
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 格式

Encoding Field
unsigned byte version
unsigned byte eh_frame_ptr_enc
unsigned byte fde_count_enc
unsigned byte table_enc
encoded eh_frame_ptr
encoded fde_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位:
Name Value Meaning Descript
DW_EH_PE_absptr 0x00 Pointer (long) The Value is a literal pointer whose size is determined by the architecture.
DW_EH_PE_uleb128 0x01 Unsigned LEB128 Unsigned value is encoded using the Little Endian Base 128 (LEB128) as defined by DWARF Debugging Information Format, Revision 2.0.0.
DW_EH_PE_udata2 0x02 A 2 bytes unsigned value.
DW_EH_PE_udata4 0x03 A 4 bytes unsigned value.
DW_EH_PE_udata8 0x04 An 8 bytes unsigned value.
DW_EH_PE_sleb128 0x09 Signed LEB128 Signed value is encoded using the Little Endian Base 128 (LEB128) as defined by DWARF Debugging Information Format, Revision 2.0.0.
DW_EH_PE_sdata2 0x0A A 2 bytes signed value.
DW_EH_PE_sdata4 0x0B A 4 bytes signed value.
DW_EH_PE_sdata8 0x0C An 8 bytes signed value.
  • 高4位:
Name Value Meaning Descript
DW_EH_PE_pcrel 0x10 Value is relative to the current program counter. 值是相對於當前程序計數器的。
DW_EH_PE_textrel 0x20 Value is relative to the beginning of the .text section. 值是相對於.text section的。
DW_EH_PE_datarel 0x30 Value is relative to the beginning of the .got or .eh_frame_hdr section. 值是相對於.got或者.eh_frame_hdr section的。
DW_EH_PE_funcrel 0x40 Value is relative to the beginning of the function. 值是相對於當前函數的。
DW_EH_PE_aligned 0x50 Value is aligned to an address unit sized boundary. 值與地址單元大小的邊界對齊。
  • 特殊值
Name Value Meaning Descript
DW_EH_PE_omit 0xFF indicate 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM