首頁  >  文章  >  系統教程  >  深入探討Linux調試器的高階話題

深入探討Linux調試器的高階話題

WBOY
WBOY轉載
2024-01-08 22:42:00427瀏覽
導讀 我們終於來到這個系列的最後一篇文章!這次,我將對調試中的一些更高級的概念進行高層的概述:遠端調試、共享庫支援、表達式計算和多線程支援。這些想法實現起來比較複雜,所以我不會詳細說明如何做,但是如果你有問題的話,我很樂意回答有關這些概念的問題。
系列索引
  1. 準備環境
  2. 斷點
  3. 暫存器和記憶體
  4. Elves 和 dwarves
  5. 原始碼和訊號
  6. 原始碼層逐步執行
  7. 原始碼層斷點
  8. 呼叫堆疊
  9. 處理變數
  10. 進階主題
遠端偵錯

遠端偵錯對於嵌入式系統或對不同環境進行偵錯非常有用。它還在高級偵錯器操作和與作業系統和硬體的互動之間設置了一個很好的分界線。事實上,像 GDB 和 LLDB 這樣的偵錯器即使在偵錯本機程式時也可以作為遠端偵錯器運行。一般架構是這樣的:
Linux 调试器之高级主题!

debugarch

#調試器是我們透過命令列互動的元件。也許如果你使用的是 IDE,那麼在其上有另一個層可以透過機器介面與調試器進行通訊。在目標機器上(可能與本機一樣)有一個調試存根,理論上它是一個非常小的操作系統調試庫的包裝程序,它執行所有的低級調試任務,如在地址上設置斷點。我說“在理論上”,因為如今調試存根變得越來越大。例如,我機器上的 LLDB 偵錯存根大小是 7.6MB。調試存根透過使用一些特定於作業系統的功能(在我們的例子中是 ptrace)和被調試進程以及透過遠端協定的調試器通訊。
最常見的遠端調試協定是 GDB 遠端協定。這是一種基於文字的資料包格式,用於在偵錯器和調試存根之間傳遞命令和資訊。我不會詳細介紹它,但你可以在這裡進一步閱讀。如果你啟動 LLDB 並執行命令 log enable gdb-remote packets,那麼你將獲得透過遠端協定發送的所有資料包的追蹤資訊。在 GDB 上,你可以用 set remotelogfile 做同樣的事情。

作為一個簡單的例子,這是設定斷點的資料包:

$Z0,400570,1#43

$ 標記資料包的開始。 Z0 是插入記憶體斷點的命令。 400570 和 1 是參數,其中前者是設定斷點的位址,後者是特定目標的斷點類型說明符。最後,#43 是校驗值,以確保資料沒有損壞。

GDB 遠端協定非常易於擴展自訂資料包,這對於實現平台或語言特定的功能非常有用。

共享庫和動態載入支援

偵錯器需要知道被偵錯程式載入了哪些共用程式庫,以便它可以設定斷點、取得原始程式碼層級的資訊和符號等。除查找被動態連結的庫之外,偵錯器還必須追蹤在運行時透過 dlopen 載入的庫。為了達到這個目的,動態連結器維護一個 交會結構體。此結構體維護共享庫描述符的鍊錶,以及一個指向每當更新鍊錶時調用的函數的指標。這個結構儲存在 ELF 檔案的 .dynamic 段中,在程式執行之前被初始化。

一個簡單的追蹤演算法:

  • 追蹤程式在 ELF 頭中尋找程式的入口(或可以使用儲存在 /proc//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//task/ 并查看内存映射之类来获得所需的所有信息。

GDB 使用 libthread_db,它提供了一堆帮助函数,这样你就不需要自己解析和处理。设置这个库很奇怪,我不会在这展示它如何工作,但如果你想使用它,你可以去阅读这个教程。

多线程支持中最复杂的部分是调试器中线程状态的建模,特别是如果你希望支持不间断模式或当你计算中涉及不止一个 CPU 的某种异构调试。

最后!

呼!这个系列花了很长时间才写完,但是我在这个过程中学到了很多东西,我希望它是有帮助的。如果你有关于调试或本系列中的任何问题,请在 Twitter @TartanLlama或评论区联系我。如果你有想看到的其他任何调试主题,让我知道我或许会再发其他的文章。

以上是深入探討Linux調試器的高階話題的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:linuxprobe.com。如有侵權,請聯絡admin@php.cn刪除