Linux 調試器之堆疊展開!
導讀 | 有時你需要知道的最重要的資訊是什麼,你目前的程式狀態是如何到達那裡的。有一個 backtrace 指令,它給你提供了程式目前的函數呼叫鏈。這篇文章將向你展示如何在 x86_64 上實現堆疊展開以產生這樣的回溯。 |
這些連結將會隨著其他貼文的發布而上線。
- 準備環境
- 斷點
- 暫存器和記憶體
- ELF 和 DWARF
- 原始碼和訊號
- 源碼級逐步執行
- 原始碼級斷點
- 堆疊展開
- 讀取變數
- 之後步驟
用下面的程式當範例:
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(); }
測試此功能的一個方法是透過編寫一個測試程式與一堆互相呼叫的小函數。設定幾個斷點,跳到程式碼附近,並確保你的回溯是準確的。
我們已經從一個只能產生並附加到其他程式的程式走了很長的路。本系列的倒數第二篇文章將透過支援讀寫變數來完成偵錯器的實作。在此之前,你可以在這裡找到這個帖子的程式碼。
以上是Linux 調試器之堆疊展開!的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

熱門話題

無法以 root 身份登錄 MySQL 的原因主要在於權限問題、配置文件錯誤、密碼不符、socket 文件問題或防火牆攔截。解決方法包括:檢查配置文件中 bind-address 參數是否正確配置。查看 root 用戶權限是否被修改或刪除,並進行重置。驗證密碼是否準確無誤,包括大小寫和特殊字符。檢查 socket 文件權限設置和路徑。檢查防火牆是否阻止了 MySQL 服務器的連接。

C語言條件編譯是一種根據編譯時條件選擇性編譯代碼塊的機制,入門方法有:使用#if和#else指令根據條件選擇代碼塊。常用條件表達式包括STDC、_WIN32和linux。實戰案例:根據操作系統打印不同消息。根據系統位數使用不同的數據類型。根據編譯器支持不同的頭文件。條件編譯增強了代碼的可移植性和靈活性,使其適應編譯器、操作系統和CPU架構變化。

1.0.1前言這個項目(包括代碼和註釋)是在我自學Rust的過程中記錄的。可能有不准確或表述不清的地方,還請大家諒解。如果您從中受益,那就更好了。 1.0.2為什麼使用RustRust可靠且高效。 Rust可以取代C和C,性能相似但安全性更高,並且不需要像C和C那樣頻繁重新編譯來檢查錯誤。主要優點包括:內存安全(防止空指針取消引用、懸空指針和數據爭用)。線程安全(確保多線程代碼在執行前是安全的)。避免未定義的行為(例如,數組越界、未初始化的變量或訪問已釋放的內存)。 Rust提供現代語言功能(例如泛型

Linux的五個基本組件是:1.內核,管理硬件資源;2.系統庫,提供函數和服務;3.Shell,用戶與系統交互的接口;4.文件系統,存儲和組織數據;5.應用程序,利用系統資源實現功能。

MySQL啟動失敗的原因有多種,可以通過檢查錯誤日誌進行診斷。常見原因包括端口衝突(檢查端口占用情況並修改配置)、權限問題(檢查服務運行用戶權限)、配置文件錯誤(檢查參數設置)、數據目錄損壞(恢復數據或重建表空間)、InnoDB表空間問題(檢查ibdata1文件)、插件加載失敗(檢查錯誤日誌)。解決問題時應根據錯誤日誌進行分析,找到問題的根源,並養成定期備份數據的習慣,以預防和解決問題。

MySQL安裝報錯的解決方法是:1.仔細檢查系統環境,確保滿足MySQL的依賴庫要求,不同操作系統和版本需求不同;2.認真閱讀報錯信息,根據提示(例如缺少庫文件或權限不足)採取對應措施,例如安裝依賴或使用sudo命令;3.必要時,可嘗試源碼安裝並仔細檢查編譯日誌,但這需要一定的Linux知識和經驗。最終解決問題的關鍵在於仔細檢查系統環境和報錯信息,並參考官方文檔。

MySQL無法直接在Android上運行,但可以通過以下方法間接實現:使用輕量級數據庫SQLite,由Android系統自帶,無需單獨服務器,資源佔用小,非常適合移動設備應用。遠程連接MySQL服務器,通過網絡連接到遠程服務器上的MySQL數據庫進行數據讀寫,但存在網絡依賴性強、安全性問題和服務器成本等缺點。

C語言函數庫是一個包含各種函數的工具箱,這些函數被組織在不同的庫文件中。添加函數庫需要通過編譯器的命令行選項來指定,例如 GCC 編譯器使用 -l 選項,後跟庫名的縮寫。如果庫文件不在默認搜索路徑下,則需要使用 -L 選項指定庫文件路徑。庫有靜態庫和動態庫之分,靜態庫在編譯時直接鏈接到程序中,而動態庫在運行時被加載。
