文章目錄
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_expression
、DW_CFA_expression
、 DW_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_location
和address_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
總體架構就是由CIE
和FDE
組成的。其中最核心的就是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偽指令匯總而成。
其中CIE
、FDE
的格式在第一節中已經介紹,不好理解的是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_hdr
section。這兩個段運行的時候被一起加載到內存,運行的時候需要有方法能找到它們。
內核在鏈接腳本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_hdr
section當成一張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 != ¬_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 == ¬_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的-g
和rdynamic
選項:
- 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
參考文檔:
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