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