有時你需要知道的最重要的信息是什么,你當前的程序狀態(tài)是如何到達那里的。有一個 backtrace 命令,它給你提供了程序當前的函數(shù)調(diào)用鏈。這篇文章將向你展示如何在 x86_64 上實現(xiàn)堆棧展開以生成這樣的回溯。
系列索引
這些鏈接將會隨著其他帖子的發(fā)布而上線。
準備環(huán)境
斷點
寄存器和內(nèi)存
ELF 和 DWARF
源碼和信號
源碼級逐步執(zhí)行
源碼級斷點
堆棧展開
讀取變量
之后步驟
用下面的程序作為例子:
void a() {//stopped here}void b() {a();}void c() {a();}int main() {b();c();}
如果調(diào)試器停在 //stopped here' 這行,那么有兩種方法可以達到:main->b->a或main->c->a`。如果我們用 LLDB 設置一個斷點,繼續(xù)執(zhí)行并請求一個回溯,那么我們將得到以下內(nèi)容:
* frame #0: 0x00000000004004da a.out`a() + 4 at bt.cpp:3frame #1: 0x00000000004004e6 a.out`b() + 9 at bt.cpp:6frame #2: 0x00000000004004fe a.out`main + 9 at bt.cpp:14frame #3: 0x00007ffff7a2e830 libc.so.6`__libc_start_main + 240 at libc-start.c:291frame #4: 0x0000000000400409 a.out`_start + 41
這說明我們目前在函數(shù) a 中,a 從函數(shù) b 中跳轉(zhuǎn),b 從 main 中跳轉(zhuǎn)等等。最后兩個幀是編譯器如何引導 main 函數(shù)的。
現(xiàn)在的問題是我們?nèi)绾卧?x86_64 上實現(xiàn)。最穩(wěn)健的方法是解析 ELF 文件的 .eh_frame 部分,并解決如何從那里展開堆棧,但這會很痛苦。你可以使用 libunwind 或類似的來做,但這很無聊。相反,我們假設編譯器以某種方式設置了堆棧,我們將手動遍歷它。為了做到這一點,我們首先需要了解堆棧的布局。
High| ... |+---------++24| Arg 1 |+---------++16| Arg 2 |+---------++ 8| Return |+---------+EBP+--> |Saved EBP|+---------+- 8| Var 1 |+---------+ESP+--> | Var 2 |+---------+| ... |Low
如你所見,最后一個堆棧幀的幀指針存儲在當前堆棧幀的開始處,創(chuàng)建一個鏈接的指針列表。堆棧依據(jù)這個鏈表解開。我們可以通過查找 DWARF 信息中的返回地址來找出列表中下一幀的函數(shù)。一些編譯器將忽略跟蹤 EBP 的幀基址,因為這可以表示為 ESP 的偏移量,并可以釋放一個額外的寄存器。即使啟用了優(yōu)化,傳遞 -fno-omit-frame-pointer 到 GCC 或 Clang 會強制它遵循我們依賴的約定。
我們將在 print_backtrace 函數(shù)中完成所有的工作:
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;};
打印輸出的第一幀是當前正在執(zhí)行的幀。我們可以通過查找 DWARF 中的當前程序計數(shù)器來獲取此幀的信息:
auto current_func = get_function_from_pc(get_pc());output_frame(current_func);
接下來我們需要獲取當前函數(shù)的幀指針和返回地址。幀指針存儲在 rbp 寄存器中,返回地址是從幀指針堆棧起的 8 字節(jié)。
auto frame_pointer = get_register_value(m_pid, reg::rbp);auto return_address = read_memory(frame_pointer+8);
現(xiàn)在我們擁有了展開堆棧所需的所有信息。我只需要繼續(xù)展開,直到調(diào)試器命中 main,但是當幀指針為 0x0 時,你也可以選擇停止,這些是你在調(diào)用 main 函數(shù)之前調(diào)用的函數(shù)。我們將從每幀抓取幀指針和返回地址,并打印出信息。
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);}}
就是這樣!以下是整個函數(shù):
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();}
測試
測試此功能的一個方法是通過編寫一個測試程序與一堆互相調(diào)用的小函數(shù)。設置幾個斷點,跳到代碼附近,并確保你的回溯是準確的。
我們已經(jīng)從一個只能產(chǎn)生并附加到其他程序的程序走了很長的路。本系列的倒數(shù)第二篇文章將通過支持讀寫變量來完成調(diào)試器的實現(xiàn)。在此之前,你可以在這里找到這個帖子的代碼。
via: https://blog.tartanllama.xyz/c++/2017/06/24/writing-a-linux-debugger-unwinding/
作者:Simon Brand 譯者:geekpi 校對:wxy
?
評論