天天看點

軟體開發 | GDB 調試器如何通過調用幀資訊來确定函數調用關系

作者:硬核老王
軟體開發 | GDB 調試器如何通過調用幀資訊來确定函數調用關系
從調試器中擷取函數調用關系。

在我的 上一篇文章中,我展示了如何使用

debuginfo

在目前指令指針(IP)和包含它的函數或行之間進行映射。該資訊對于顯示 CPU 目前正在執行的代碼很有幫助。不過,如果能顯示更多的有關目前函數調用棧及其正在執行語句的上下文對我們定位問題來說也是十分有助的。

例如,将空指針作為參數傳遞到函數中而導緻非法記憶體通路的問題,隻需檢視目前執行函數行,即可發現該錯誤是由嘗試通過空指針進行通路而觸發的。但是,你真正想知道的是導緻空指針通路的函數調用的完整上下文,以便确定該空指針最初是如何傳遞到該函數中的。此上下文資訊由回溯提供,可以讓你确定哪些函數可能對空指針參數負責。

有一點是肯定的:确定目前活動的函數調用棧不是一項簡單的操作。

函數激活記錄

現代程式設計語言具有局部變量,并允許函數可以調用自身的遞歸。此外,并發程式具有多個線程,這些線程可能同時運作相同的函數。在這些情況下,局部變量不能存儲在全局位置。對于函數的每次調用,局部變量的位置必須是唯一的。它的工作原理如下:

  • 每次調用函數時,編譯器都會生成函數激活記錄,以将局部變量存儲在唯一位置。
  • 為了提高效率,處理器堆棧用于存儲函數激活記錄。
  • 當函數被調用時,會在處理器堆棧的頂部為該函數建立一條新的函數激活記錄。
  • 如果該函數調用另一個函數,則新的函數激活記錄将放置在現有函數激活記錄之上。
  • 每次函數傳回時,其函數激活記錄都會從堆棧中删除。

函數激活記錄的建立是由函數中稱為“序言prologue”的代碼建立的。函數激活記錄的删除由函數“尾聲epilogue”處理。函數體可以利用堆棧上為其預留的記憶體來存儲臨時值和局部變量。

函數激活記錄的大小可以是可變的。對于某些函數,不需要空間來存儲局部變量。理想情況下,函數激活記錄隻需要存儲調用 該函數的函數的傳回位址。對于其他函數,除了傳回位址之外,可能還需要大量空間來存儲函數的本地資料結構。幀大小的可變導緻編譯器使用幀指針來跟蹤函數激活幀的開始。函數序言代碼具有在為目前函數建立新幀指針之前存儲舊幀指針的額外任務,并且函數尾聲必須恢複舊幀指針值。

函數激活記錄的布局方式、調用函數的傳回位址和舊幀指針是相對于目前幀指針的恒定偏移量。通過舊的幀指針,可以定位堆棧上下一個函數的激活幀。重複此過程,直到檢查完所有函數激活記錄為止。

優化複雜性

在代碼中使用顯式幀指針有幾個缺點。在某些處理器上,可用的寄存器相對較少。具有顯式幀指針會導緻使用更多記憶體操作。生成的代碼速度較慢,因為幀指針必須位于寄存器中。具有顯式幀指針可能會限制編譯器可以生成的代碼,因為編譯器可能不會将函數序言和尾聲代碼與函數體混合。

編譯器的目标是盡可能生成快速代碼,是以編譯器通常會從生成的代碼中省略幀指針。正如 Phoronix 的基準測試所示,保留幀指針會顯着降低性能。不過省略幀指針也有缺點,查找前一個調用函數的激活幀和傳回位址不再是相對于幀指針的簡單偏移。

調用幀資訊

為了幫助生成函數回溯,編譯器包含 DWARF 調用幀資訊(CFI)來重建幀指針并查找傳回位址。此補充資訊存儲在執行的

.eh_frame

部分中。與傳統的函數和行位置資訊的

debuginfo

不同,即使生成的可執行檔案沒有調試資訊,或者調試資訊已從檔案中删除,

.eh_frame

部分也位于可執行檔案中。 調用幀資訊對于 C++ 中的

throw-catch

等語言結構的操作至關重要。

CFI 的每個功能都有一個幀描述條目(FDE)。作為其步驟之一,回溯生成過程為目前正在檢查的激活幀找到适當的 FDE。将 FDE 視為一張表,每一行代表一個或多個指令,并具有以下列:

  • 規範幀位址(CFA),幀指針指向的位置
  • 傳回位址
  • 有關其他寄存器的資訊

FDE 的編碼旨在最大限度地減少所需的空間量。FDE 描述了行之間的變化,而不是完全指定每一行。為了進一步壓縮資料,多個 FDE 共有的起始資訊被分解出來并放置在通用資訊條目(CIE)中。 這使得 FDE 更加緊湊,但也需要更多的工作來計算實際的 CFA 并找到傳回位址位置。該工具必須從未初始化狀态啟動。它逐漸周遊 CIE 中的條目以擷取函數條目的初始狀态,然後從 FDE 的第一個條目開始繼續處理 FDE,并處理操作,直到到達覆寫目前正在分析的指令指針的行。

調用幀資訊使用執行個體

從一個簡單的示例開始,其中包含将華氏溫度轉換為攝氏度的函數。 内聯函數在 CFI 中沒有條目,是以

f2c

函數的

__attribute__((noinline))

確定編譯器将

f2c

保留為真實函數。

#include            

編譯代碼:

$ gcc -O2 -g -o f2c f2c.c
           

.eh_frame

部分展示如下:

$ eu-readelf -S f2c |grep eh_frame
[17] .eh_frame_hdr  PROGBITS   0000000000402058 00002058 00000034  0 A  0   0  4
[18] .eh_frame      PROGBITS   0000000000402090 00002090 000000a0  0 A  0   0  8
           

我們可以通過以下方式擷取 CFI 資訊:

$ readelf --debug-dump=frames  f2c > f2c.cfi
           

生成

f2c

可執行檔案的反彙編代碼,這樣你可以查找

f2c

main

函數:

$ objdump -d f2c > f2c.dis
           

f2c.dis

中找到以下資訊來看看

f2c

main

函數的執行位置:

0000000000401060            

在許多情況下,二進制檔案中的所有函數在執行函數的第一條指令之前都使用相同的 CIE 來定義初始條件。 在此示例中,

f2c

main

都使用以下 CIE:

00000000 0000000000000014 00000000 CIE
  Version:                   1
  Augmentation:              "zR"
  Code alignment factor: 1
  Data alignment factor: -8
  Return address column: 16
  Augmentation data:         1b
  DW_CFA_def_cfa: r7 (rsp) ofs 8
  DW_CFA_offset: r16 (rip) at cfa-8
  DW_CFA_nop
  DW_CFA_nop
           

本示例中,不必擔心增強或增強資料條目。由于 x86_64 處理器具有 1 到 15 位元組大小的可變長度指令,是以 “代碼對齊因子” 設定為 1。在隻有 32 位(4 位元組指令)的處理器上,“代碼對齊因子” 設定為 4,并且允許對一行狀态資訊适用的位元組數進行更緊湊的編碼。類似地,還有 “資料對齊因子” 來使 CFA 所在位置的調整更加緊湊。在 x86_64 上,堆棧槽的大小為 8 個位元組。

虛拟表中儲存傳回位址的列是 16。這在 CIE 尾部的指令中使用。 有四個

DW_CFA

指令。第一條指令

DW_CFA_def_cfa

描述了如果代碼具有幀指針,如何計算幀指針将指向的規範幀位址(CFA)。 在這種情況下,CFA 是根據

r7 (rsp)

CFA=rsp+8

計算的。

第二條指令

DW_CFA_offset

定義從哪裡擷取傳回位址

CFA-8

。在這種情況下,傳回位址目前由堆棧指針

(rsp+8)-8

指向。CFA 從堆棧傳回位址的正上方開始。

CIE 末尾的

DW_CFA_nop

進行填充以保持 DWARF 資訊的對齊。 FDE 還可以在末尾添加填充以進行對齊。

f2c.cfi

中找到

main

的 FDE,它涵蓋了從

0x40160

到(但不包括)

0x401097

main

函數:

00000084 0000000000000014 00000088 FDE cie=00000000 pc=0000000000401060..0000000000401097
  DW_CFA_advance_loc: 4 to 0000000000401064
  DW_CFA_def_cfa_offset: 32
  DW_CFA_advance_loc: 50 to 0000000000401096
  DW_CFA_def_cfa_offset: 8
  DW_CFA_nop
           

在執行函數中的第一條指令之前,CIE 描述調用幀狀态。然而,當處理器執行函數中的指令時,細節将會改變。 首先,指令

DW_CFA_advance_loc

DW_CFA_def_cfa_offset

main

401060

處的第一條指令比對。 這會将堆棧指針向下調整

0x18

(24 個位元組)。 CFA 沒有改變位置,但堆棧指針改變了,是以 CFA 在

401064

處的正确計算是

rsp+32

。 這就是這段代碼中序言指令的範圍。 以下是

main

中的前幾條指令:

0000000000401060            

DW_CFA_advance_loc

使目前行應用于函數中接下來的 50 個位元組的代碼,直到

401096

。CFA 位于

rsp+32

,直到

401092

處的堆棧調整指令完成執行。

DW_CFA_def_cfa_offset

将 CFA 的計算更新為與函數入口相同。這是預期之中的,因為

401096

處的下一條指令是傳回指令

ret

,并将傳回值從堆棧中彈出。

401090:    31 c0        xor        %eax,%eax
  401092:    48 83 c4 18  add        $0x18,%rsp
  401096:    c3           ret
           

f2c

函數的 FDE 使用與

main

函數相同的 CIE,并覆寫

0x41190

0x4011c3

的範圍:

00000068 0000000000000018 0000006c FDE cie=00000000 pc=0000000000401190..00000000004011c3
  DW_CFA_advance_loc: 1 to 0000000000401191
  DW_CFA_def_cfa_offset: 16
  DW_CFA_offset: r3 (rbx) at cfa-16
  DW_CFA_advance_loc: 29 to 00000000004011ae
  DW_CFA_def_cfa_offset: 8
  DW_CFA_nop
  DW_CFA_nop
  DW_CFA_nop
           

可執行檔案中

f2c

函數的

objdump

輸出:

0000000000401190            

f2c

的 FDE 中,函數開頭有一個帶有

DW_CFA_advance_loc

的單位元組指令。在進階操作之後,還有兩個附加操作。

DW_CFA_def_cfa_offset

将 CFA 更改為

%rsp+16

DW_CFA_offset

表示

%rbx

中的初始值現在位于

CFA-16

(堆棧頂部)。

檢視這個

fc2

反彙編代碼,可以看到

push

用于将

%rbx

儲存到堆棧中。 在代碼生成中省略幀指針的優點之一是可以使用

push

pop

等緊湊指令在堆棧中存儲和檢索值。 在這種情況下,儲存

%rbx

是因為

%rbx

用于向

printf

函數傳遞參數(實際上轉換為

puts

調用),但需要儲存傳遞到函數中的

f

初始值以供後面的計算使用。

4011ae

DW_CFA_advance_loc

29位元組顯示了

pop %rbx

之後的下一個狀态變化,它恢複了

%rbx

的原始值。

DW_CFA_def_cfa_offset

指出 pop 将 CFA 更改為

%rsp+8

GDB 使用調用幀資訊

有了 CFI 資訊,GNU 調試器(GDB)和其他工具就可以生成準确的回溯。如果沒有 CFI 資訊,GDB 将很難找到傳回位址。如果在

f2c.c

的第 7 行設定斷點,可以看到 GDB 使用此資訊。GDB在

f2c

函數中的

pop %rbx

完成且傳回值不在棧頂之前放置了斷點。

GDB 能夠展開堆棧,并且作為額外收獲還能夠擷取目前儲存在堆棧上的參數

f

$ gdb f2c
[...]
(gdb) break f2c.c:7
Breakpoint 1 at 0x40119d: file f2c.c, line 7.
(gdb) run
Starting program: /home/wcohen/present/202207youarehere/f2c
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
98
converting

Breakpoint 1, f2c (f=98) at f2c.c:8
8            return c;
(gdb) where
#0  f2c (f=98) at f2c.c:8
#1  0x000000000040107e in main (argc=           

調用幀資訊

DWARF 調用幀資訊為編譯器提供了一種靈活的方式來包含用于準确展開堆棧的資訊。這使得可以确定目前活動的函數調用。我在本文中提供了簡要介紹,但有關 DWARF 如何實作此機制的更多詳細資訊,請參閱 DWARF 規範。

(題圖:MJ/4004d7c7-8407-40bd-8aa8-92404601dba0)

via: https://opensource.com/article/23/3/gdb-debugger-call-frame-active-function-calls

作者:Will Cohen選題:lkxed譯者:jrglinux校對:wxy

本文由 LCTT原創編譯,Linux中國榮譽推出

繼續閱讀