開發一個 Linux 調試器(十):高級主題
我們終於來到這個系列的最後一篇文章!這一次,我將對調試中的一些更高級的概念進行高層的概述:遠程調試、共享庫支持、表達式計算和多線程支持。這些想法實現起來比較複雜,所以我不會詳細說明如何做,但是如果你有問題的話,我很樂意回答有關這些概念的問題。
系列索引
遠程調試
遠程調試對於嵌入式系統或對不同環境進行調試非常有用。它還在高級調試器操作和與操作系統和硬體的交互之間設置了一個很好的分界線。事實上,像 GDB 和 LLDB 這樣的調試器即使在調試本地程序時也可以作為遠程調試器運行。一般架構是這樣的:
調試器是我們通過命令行交互的組件。也許如果你使用的是 IDE,那麼在其上有另一個層可以通過機器介面與調試器進行通信。在目標機器上(可能與本機一樣)有一個 調試存根 ,理論上它是一個非常小的操作系統調試庫的包裝程序,它執行所有的低級調試任務,如在地址上設置斷點。我說「在理論上」,因為如今調試存根變得越來越大。例如,我機器上的 LLDB 調試存根大小是 7.6MB。調試存根通過使用一些特定於操作系統的功能(在我們的例子中是 ptrace
)和被調試進程以及通過遠程協議的調試器通信。
最常見的遠程調試協議是 GDB 遠程協議。這是一種基於文本的數據包格式,用於在調試器和調試存根之間傳遞命令和信息。我不會詳細介紹它,但你可以在這裡進一步閱讀。如果你啟動 LLDB 並執行命令 log enable gdb-remote packets
,那麼你將獲得通過遠程協議發送的所有數據包的跟蹤信息。在 GDB 上,你可以用 set remotelogfile <file>
做同樣的事情。
作為一個簡單的例子,這是設置斷點的數據包:
$Z0,400570,1#43
$
標記數據包的開始。Z0
是插入內存斷點的命令。400570
和 1
是參數,其中前者是設置斷點的地址,後者是特定目標的斷點類型說明符。最後,#43
是校驗值,以確保數據沒有損壞。
GDB 遠程協議非常易於擴展自定義數據包,這對於實現平台或語言特定的功能非常有用。
共享庫和動態載入支持
調試器需要知道被調試程序載入了哪些共享庫,以便它可以設置斷點、獲取源代碼級別的信息和符號等。除查找被動態鏈接的庫之外,調試器還必須跟蹤在運行時通過 dlopen
載入的庫。為了達到這個目的,動態鏈接器維護一個 交匯結構體。該結構體維護共享庫描述符的鏈表,以及一個指向每當更新鏈表時調用的函數的指針。這個結構存儲在 ELF 文件的 .dynamic
段中,在程序執行之前被初始化。
一個簡單的跟蹤演算法:
- 追蹤程序在 ELF 頭中查找程序的入口(或者可以使用存儲在
/proc/<pid>/aux
中的輔助向量)。 - 追蹤程序在程序的入口處設置一個斷點,並開始執行。
- 當到達斷點時,通過在 ELF 文件中查找
.dynamic
的載入地址找到交匯結構體的地址。 - 檢查交匯結構體以獲取當前載入的庫的列表。
- 鏈接器更新函數上設置斷點。
- 每當到達斷點時,列表都會更新。
- 追蹤程序無限循環,繼續執行程序並等待信號,直到追蹤程序信號退出。
我給這些概念寫了一個小例子,你可以在這裡找到。如果有人有興趣,我可以將來寫得更詳細一點。
表達式計算
表達式計算是程序的一項功能,允許用戶在調試程序時對原始源語言中的表達式進行計算。例如,在 LLDB 或 GDB 中,可以執行 print foo()
來調用 foo
函數並列印結果。
根據表達式的複雜程度,有幾種不同的計算方法。如果表達式只是一個簡單的標識符,那麼調試器可以查看調試信息,找到該變數並列印出該值,就像我們在本系列最後一部分中所做的那樣。如果表達式有點複雜,則可能將代碼編譯成中間表達式 (IR) 並解釋來獲得結果。例如,對於某些表達式,LLDB 將使用 Clang 將表達式編譯為 LLVM IR 並將其解釋。如果表達式更複雜,或者需要調用某些函數,那麼代碼可能需要 JIT 到目標並在被調試者的地址空間中執行。這涉及到調用 mmap
來分配一些可執行內存,然後將編譯的代碼複製到該塊並執行。LLDB 通過使用 LLVM 的 JIT 功能來實現。
如果你想更多地了解 JIT 編譯,我強烈推薦 Eli Bendersky 關於這個主題的文章。
多線程調試支持
本系列展示的調試器僅支持單線程應用程序,但是為了調試大多數真實程序,多線程支持是非常需要的。支持這一點的最簡單的方法是跟蹤線程的創建,並解析 procfs 以獲取所需的信息。
Linux 線程庫稱為 pthreads
。當調用 pthread_create
時,庫會使用 clone
系統調用來創建一個新的線程,我們可以用 ptrace
跟蹤這個系統調用(假設你的內核早於 2.5.46)。為此,你需要在連接到調試器之後設置一些 ptrace
選項:
ptrace(PTRACE_SETOPTIONS, m_pid, nullptr, PTRACE_O_TRACECLONE);
現在當 clone
被調用時,該進程將收到我們的老朋友 SIGTRAP
信號。對於本系列中的調試器,你可以將一個例子添加到 handle_sigtrap
來處理新線程的創建:
case (SIGTRAP | (PTRACE_EVENT_CLONE << 8)):
//get the new thread ID
unsigned long event_message = 0;
ptrace(PTRACE_GETEVENTMSG, pid, nullptr, message);
//handle creation
//...
一旦收到了,你可以看看 /proc/<pid>/task/
並查看內存映射之類來獲得所需的所有信息。
GDB 使用 libthread_db
,它提供了一堆幫助函數,這樣你就不需要自己解析和處理。設置這個庫很奇怪,我不會在這展示它如何工作,但如果你想使用它,你可以去閱讀這個教程。
多線程支持中最複雜的部分是調試器中線程狀態的建模,特別是如果你希望支持不間斷模式或當你計算中涉及不止一個 CPU 的某種異構調試。
最後!
呼!這個系列花了很長時間才寫完,但是我在這個過程中學到了很多東西,我希望它是有幫助的。如果你有關於調試或本系列中的任何問題,請在 Twitter @TartanLlama或評論區聯繫我。如果你有想看到的其他任何調試主題,讓我知道我或許會再發其他的文章。
via: https://blog.tartanllama.xyz/writing-a-linux-debugger-advanced-topics/
作者:Simon Brand 譯者:geekpi 校對:wxy
本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive