開發一個 Linux 偵錯程式(八):堆疊展開
有時你需要知道的最重要的資訊是什麼,你當前的程式狀態是如何到達那裡的。有一個 backtrace
命令,它給你提供了程式當前的函式呼叫鏈。這篇文章將向你展示如何在 x86_64 上實現堆疊展開以生成這樣的回溯。
系列索引
這些連結將會隨著其他帖子的釋出而上線。
用下面的程式作為例子:
void a() {
//stopped here
}
void b() {
a();
}
void c() {
a();
}
int main() {
b();
c();
}
如果偵錯程式停在 //stopped here' 這行,那麼有兩種方法可以達到:
main->b->a或
main->c->a`。如果我們用 LLDB 設定一個斷點,繼續執行並請求一個回溯,那麼我們將得到以下內容:
* frame #0: 0x00000000004004da a.out`a() + 4 at bt.cpp:3
frame #1: 0x00000000004004e6 a.out`b() + 9 at bt.cpp:6
frame #2: 0x00000000004004fe a.out`main + 9 at bt.cpp:14
frame #3: 0x00007ffff7a2e830 libc.so.6`__libc_start_main + 240 at libc-start.c:291
frame #4: 0x0000000000400409 a.out`_start + 41
這說明我們目前在函式 a
中,a
從函式 b
中跳轉,b
從 main
中跳轉等等。最後兩個幀是編譯器如何引導 main
函式的。
現在的問題是我們如何在 x86_64 上實現。最穩健的方法是解析 ELF 檔案的 .eh_frame
部分,並解決如何從那裡展開堆疊,但這會很痛苦。你可以使用 libunwind
或類似的來做,但這很無聊。相反,我們假設編譯器以某種方式設定了堆疊,我們將手動遍歷它。為了做到這一點,我們首先需要了解堆疊的佈局。
High
| ... |
+---------+
+24| Arg 1 |
+---------+
+16| Arg 2 |
+---------+
+ 8| Return |
+---------+
EBP+--> |Saved EBP|
+---------+
- 8| Var 1 |
+---------+
ESP+--> | Var 2 |
+---------+
| ... |
Low
如你所見,最後一個堆疊幀的幀指標儲存在當前堆疊幀的開始處,建立一個連結的指標列表。堆疊依據這個連結串列解開。我們可以通過查詢 DWARF 資訊中的返回地址來找出列表中下一幀的函式。一些編譯器將忽略跟蹤 EBP
的幀基址,因為這可以表示為 ESP
的偏移量,並可以釋放一個額外的暫存器。即使啟用了優化,傳遞 -fno-omit-frame-pointer
到 GCC 或 Clang 會強制它遵循我們依賴的約定。
我們將在 print_backtrace
函式中完成所有的工作:
void debugger::print_backtrace() {
首先要決定的是使用什麼格式列印出幀資訊。我用了一個 lambda 來推出這個方法:
auto output_frame = [frame_number = 0] (auto&& func) mutable {
std::cout << "frame #" << frame_number++ << ": 0x" << dwarf::at_low_pc(func)
<< ' ' << dwarf::at_name(func) << std::endl;
};
列印輸出的第一幀是當前正在執行的幀。我們可以通過查詢 DWARF 中的當前程式計數器來獲取此幀的資訊:
auto current_func = get_function_from_pc(get_pc());
output_frame(current_func);
接下來我們需要獲取當前函式的幀指標和返回地址。幀指標儲存在 rbp
暫存器中,返回地址是從幀指標堆疊起的 8 位元組。
auto frame_pointer = get_register_value(m_pid, reg::rbp);
auto return_address = read_memory(frame_pointer+8);
現在我們擁有了展開堆疊所需的所有資訊。我只需要繼續展開,直到偵錯程式命中 main
,但是當幀指標為 0x0
時,你也可以選擇停止,這些是你在呼叫 main
函式之前呼叫的函式。我們將從每幀抓取幀指標和返回地址,並列印出資訊。
while (dwarf::at_name(current_func) != "main") {
current_func = get_function_from_pc(return_address);
output_frame(current_func);
frame_pointer = read_memory(frame_pointer);
return_address = read_memory(frame_pointer+8);
}
}
就是這樣!以下是整個函式:
void debugger::print_backtrace() {
auto output_frame = [frame_number = 0] (auto&& func) mutable {
std::cout << "frame #" << frame_number++ << ": 0x" << dwarf::at_low_pc(func)
<< ' ' << dwarf::at_name(func) << std::endl;
};
auto current_func = get_function_from_pc(get_pc());
output_frame(current_func);
auto frame_pointer = get_register_value(m_pid, reg::rbp);
auto return_address = read_memory(frame_pointer+8);
while (dwarf::at_name(current_func) != "main") {
current_func = get_function_from_pc(return_address);
output_frame(current_func);
frame_pointer = read_memory(frame_pointer);
return_address = read_memory(frame_pointer+8);
}
}
新增命令
當然,我們必須向使用者公開這個命令。
else if(is_prefix(command, "backtrace")) {
print_backtrace();
}
測試
測試此功能的一個方法是通過編寫一個測試程式與一堆互相呼叫的小函式。設定幾個斷點,跳到程式碼附近,並確保你的回溯是準確的。
我們已經從一個只能產生並附加到其他程式的程式走了很長的路。本系列的倒數第二篇文章將通過支援讀寫變數來完成偵錯程式的實現。在此之前,你可以在這裡找到這個帖子的程式碼。
via: https://blog.tartanllama.xyz/c++/2017/06/24/writing-a-linux-debugger-unwinding/
作者:Simon Brand 譯者:geekpi 校對:wxy
相關文章
- 開發一個 Linux 偵錯程式(九):處理變數Linux變數
- Rails開發中使用byebug偵錯程式AI
- 另一個Swoole偵錯程式 - Yasd
- HarmonyOS NEXT應用開發實戰—元件堆疊元件
- vscode摺疊展開程式碼VSCode
- 堆疊溢位報錯引發的思考
- Dojo開發之佈局容器和堆疊容器使用
- StackOverflowError堆疊溢位錯誤Error
- 開發一個好專案:八、建立viewView
- 偵錯程式是個大騙子!
- Linux gdb偵錯程式用法全面解析Linux
- Linux 核心偵錯程式內幕(轉)Linux
- 微信偵錯程式
- 從零開始的堆疊卡片控制元件控制元件
- [Win32]一個偵錯程式的實現(五)除錯符號Win32除錯符號
- JS 堆疊JS
- java堆疊Java
- 平衡堆疊
- 堆疊圖
- windows程式設計師開發linux程式的頭一個月Windows程式設計師Linux
- 深入Linux網路核心堆疊(轉)Linux
- 揭秘Linux核心偵錯程式之內幕(轉)Linux
- linux下的c/c++偵錯程式gdbLinuxC++
- Go 錯誤堆疊資訊之 CockroachDB errors 庫GoError
- 【原創】mysql 錯誤緩衝堆疊薦MySql
- Linux與開源發展迅猛Linux
- 幽默:恭喜,您將單堆疊的單體變成了n個微服務,然後您發現自己的微服務緊密耦合,現在已經有43個不同的堆疊,每個堆疊都有自己的故障模式,您玩得開心!- Ian Miell微服務模式
- 微信小程式實現多摺疊展開酷炫選單微信小程式
- Java經典例項:實現一個簡單堆疊Java
- Xcode偵錯程式LLDBXCodeLLDB
- go語言偵錯程式Go
- Linux下彙編偵錯程式GDB的使用薦Linux
- 如何優雅地檢視 JS 錯誤堆疊?JS
- 利用Decorator和SourceMap優化JavaScript錯誤堆疊優化JavaScript
- 深入理解 JavaScript 錯誤和堆疊追蹤JavaScript
- 使用Error Stack跟蹤Oracle錯誤堆疊資訊ErrorOracle
- 第一個linux驅動開發包Linux
- el-tree全部展開全部摺疊方法