開發(fā)一個Linux調(diào)試器(十):高級主題
我們終于來到這個系列的最后一篇文章!這一次,我將對調(diào)試中的一些更高級的概念進(jìn)行高層的概述:遠(yuǎn)程調(diào)試、共享庫支持、表達(dá)式計算和多線程支持。這些想法實(shí)現(xiàn)起來比較復(fù)雜,所以我不會詳細(xì)說明如何做,但是如果你有問題的話,我很樂意回答有關(guān)這些概念的問題。
系列索引
遠(yuǎn)程調(diào)試
遠(yuǎn)程調(diào)試對于嵌入式系統(tǒng)或?qū)Σ煌h(huán)境進(jìn)行調(diào)試非常有用。它還在高級調(diào)試器操作和與操作系統(tǒng)和硬件的交互之間設(shè)置了一個很好的分界線。事實(shí)上,像 GDB 和 LLDB 這樣的調(diào)試器即使在調(diào)試本地程序時也可以作為遠(yuǎn)程調(diào)試器運(yùn)行。一般架構(gòu)是這樣的:
debugarch
調(diào)試器是我們通過命令行交互的組件。也許如果你使用的是 IDE,那么在其上有另一個層可以通過機(jī)器接口與調(diào)試器進(jìn)行通信。在目標(biāo)機(jī)器上(可能與本機(jī)一樣)有一個調(diào)試存根debug stub ,理論上它是一個非常小的操作系統(tǒng)調(diào)試庫的包裝程序,它執(zhí)行所有的低級調(diào)試任務(wù),如在地址上設(shè)置斷點(diǎn)。我說“在理論上”,因為如今調(diào)試存根變得越來越大。例如,我機(jī)器上的 LLDB 調(diào)試存根大小是 7.6MB。調(diào)試存根通過使用一些特定于操作系統(tǒng)的功能(在我們的例子中是 ptrace)和被調(diào)試進(jìn)程以及通過遠(yuǎn)程協(xié)議的調(diào)試器通信。
最常見的遠(yuǎn)程調(diào)試協(xié)議是 GDB 遠(yuǎn)程協(xié)議。這是一種基于文本的數(shù)據(jù)包格式,用于在調(diào)試器和調(diào)試存根之間傳遞命令和信息。我不會詳細(xì)介紹它,但你可以在這里進(jìn)一步閱讀。如果你啟動 LLDB 并執(zhí)行命令 log enable gdb-remote packets,那么你將獲得通過遠(yuǎn)程協(xié)議發(fā)送的所有數(shù)據(jù)包的跟蹤信息。在 GDB 上,你可以用 set remotelogfile <file> 做同樣的事情。
作為一個簡單的例子,這是設(shè)置斷點(diǎn)的數(shù)據(jù)包:
- $Z0,400570,1#43
$ 標(biāo)記數(shù)據(jù)包的開始。Z0 是插入內(nèi)存斷點(diǎn)的命令。400570 和 1 是參數(shù),其中前者是設(shè)置斷點(diǎn)的地址,后者是特定目標(biāo)的斷點(diǎn)類型說明符。最后,#43 是校驗值,以確保數(shù)據(jù)沒有損壞。
GDB 遠(yuǎn)程協(xié)議非常易于擴(kuò)展自定義數(shù)據(jù)包,這對于實(shí)現(xiàn)平臺或語言特定的功能非常有用。
共享庫和動態(tài)加載支持
調(diào)試器需要知道被調(diào)試程序加載了哪些共享庫,以便它可以設(shè)置斷點(diǎn)、獲取源代碼級別的信息和符號等。除查找被動態(tài)鏈接的庫之外,調(diào)試器還必須跟蹤在運(yùn)行時通過 dlopen 加載的庫。為了達(dá)到這個目的,動態(tài)鏈接器維護(hù)一個 交匯結(jié)構(gòu)體。該結(jié)構(gòu)體維護(hù)共享庫描述符的鏈表,以及一個指向每當(dāng)更新鏈表時調(diào)用的函數(shù)的指針。這個結(jié)構(gòu)存儲在 ELF 文件的 .dynamic 段中,在程序執(zhí)行之前被初始化。
一個簡單的跟蹤算法:
- 追蹤程序在 ELF 頭中查找程序的入口(或者可以使用存儲在 /proc/<pid>/aux 中的輔助向量)。
- 追蹤程序在程序的入口處設(shè)置一個斷點(diǎn),并開始執(zhí)行。
- 當(dāng)?shù)竭_(dá)斷點(diǎn)時,通過在 ELF 文件中查找 .dynamic 的加載地址找到交匯結(jié)構(gòu)體的地址。
- 檢查交匯結(jié)構(gòu)體以獲取當(dāng)前加載的庫的列表。
- 鏈接器更新函數(shù)上設(shè)置斷點(diǎn)。
- 每當(dāng)?shù)竭_(dá)斷點(diǎn)時,列表都會更新。
- 追蹤程序無限循環(huán),繼續(xù)執(zhí)行程序并等待信號,直到追蹤程序信號退出。
我給這些概念寫了一個小例子,你可以在這里找到。如果有人有興趣,我可以將來寫得更詳細(xì)一點(diǎn)。
表達(dá)式計算
表達(dá)式計算是程序的一項功能,允許用戶在調(diào)試程序時對原始源語言中的表達(dá)式進(jìn)行計算。例如,在 LLDB 或 GDB 中,可以執(zhí)行 print foo() 來調(diào)用 foo 函數(shù)并打印結(jié)果。
根據(jù)表達(dá)式的復(fù)雜程度,有幾種不同的計算方法。如果表達(dá)式只是一個簡單的標(biāo)識符,那么調(diào)試器可以查看調(diào)試信息,找到該變量并打印出該值,就像我們在本系列最后一部分中所做的那樣。如果表達(dá)式有點(diǎn)復(fù)雜,則可能將代碼編譯成中間表達(dá)式 (IR) 并解釋來獲得結(jié)果。例如,對于某些表達(dá)式,LLDB 將使用 Clang 將表達(dá)式編譯為 LLVM IR 并將其解釋。如果表達(dá)式更復(fù)雜,或者需要調(diào)用某些函數(shù),那么代碼可能需要 JIT 到目標(biāo)并在被調(diào)試者的地址空間中執(zhí)行。這涉及到調(diào)用 mmap 來分配一些可執(zhí)行內(nèi)存,然后將編譯的代碼復(fù)制到該塊并執(zhí)行。LLDB 通過使用 LLVM 的 JIT 功能來實(shí)現(xiàn)。
如果你想更多地了解 JIT 編譯,我強(qiáng)烈推薦 Eli Bendersky 關(guān)于這個主題的文章。
多線程調(diào)試支持
本系列展示的調(diào)試器僅支持單線程應(yīng)用程序,但是為了調(diào)試大多數(shù)真實(shí)程序,多線程支持是非常需要的。支持這一點(diǎn)的最簡單的方法是跟蹤線程的創(chuàng)建,并解析 procfs 以獲取所需的信息。
Linux 線程庫稱為 pthreads。當(dāng)調(diào)用 pthread_create 時,庫會使用 clone 系統(tǒng)調(diào)用來創(chuàng)建一個新的線程,我們可以用 ptrace 跟蹤這個系統(tǒng)調(diào)用(假設(shè)你的內(nèi)核早于 2.5.46)。為此,你需要在連接到調(diào)試器之后設(shè)置一些 ptrace 選項:
- ptrace(PTRACE_SETOPTIONS, m_pid, nullptr, PTRACE_O_TRACECLONE);
現(xiàn)在當(dāng) clone 被調(diào)用時,該進(jìn)程將收到我們的老朋友 SIGTRAP 信號。對于本系列中的調(diào)試器,你可以將一個例子添加到 handle_sigtrap 來處理新線程的創(chuàng)建:
- 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/ 并查看內(nèi)存映射之類來獲得所需的所有信息。
GDB 使用 libthread_db,它提供了一堆幫助函數(shù),這樣你就不需要自己解析和處理。設(shè)置這個庫很奇怪,我不會在這展示它如何工作,但如果你想使用它,你可以去閱讀這個教程。
多線程支持中最復(fù)雜的部分是調(diào)試器中線程狀態(tài)的建模,特別是如果你希望支持不間斷模式或當(dāng)你計算中涉及不止一個 CPU 的某種異構(gòu)調(diào)試。
最后!
呼!這個系列花了很長時間才寫完,但是我在這個過程中學(xué)到了很多東西,我希望它是有幫助的。如果你有關(guān)于調(diào)試或本系列中的任何問題,請在 Twitter @TartanLlama或評論區(qū)聯(lián)系我。如果你有想看到的其他任何調(diào)試主題,讓我知道我或許會再發(fā)其他的文章。