GDB體系結構介紹(一)
GNU偵錯程式GDB是最早為自由軟體基金會編寫的程式之一,從那以後它一直是免費和開源軟體系統的主要部分。它最初設計為普通的Unix原始碼級偵錯程式,後來擴充套件到廣泛的用途,包括與許多嵌入式系統一起使用,並且從幾千行C增加到超過五十萬。
本章將深入研究GDB的整體內部結構,展示隨著新使用者需求和新功能的不斷湧現,它如何逐步發展。
4.1 目標
GDB旨在成為用C,C ++,Ada和Fortran等編譯命令式語言編寫的程式的符號偵錯程式。使用其原始命令列介面,典型用法如下所示:
% gdb myprog [...] (gdb) break buggy_function Breakpoint 1 at 0x12345678: file myprog.c, line 232. (gdb) run 45 92 Starting program: myprog Breakpoint 1, buggy_function (arg1=45, arg2=92) at myprog.c:232 232 result = positive_variable * arg1 + arg2; (gdb) print positive_variable $$1 = -34 (gdb)
GDB顯示了一些不正確的東西,開發人員說“aha”或“嗯”,然後必須決定錯誤是什麼以及如何解決它。
設計的重點在於像GDB這樣的工具基本上是一個用於在程式中進行探索的互動式工具箱,因此它需要響應不可預測的一系列請求。此外,它將與已由編譯器優化的程式以及利用每個硬體選項以獲得性能的程式一起使用,因此需要具有詳細知識,直至系統的最低級別。
GDB還需要能夠除錯由不同編譯器(不僅僅是GNU C編譯器)編譯的程式,除錯多年前由長期過時版本的編譯器編譯的程式,以及除錯其符號資訊缺失,過時的程式,或者只是不正確;因此,另一個設計考慮因素是GDB應該繼續工作並且即使有關該程式的資料丟失,損壞或者只是難以理解,也應該有用。
以下部分假設您熟悉從命令列使用GDB。如果您是GDB新手,請試一試並仔細閱讀本手冊。[SPS + 00]
4.2 GDB的起源
GDB是一個老程式。它於1985年左右出現,由Richard Stallman和GCC,GNU Emacs以及GNU的其他早期元件編寫。 (在那些日子裡,沒有公共原始碼控制儲存庫,現在大部分詳細的開發歷史都已丟失。)
最早的現成版本是從1988年開始的,與現今資料的比較表明,只有少數幾行具有相似之處;幾乎所有的GDB都被重寫了至少一次。關於早期版本的GDB的另一個引人注目的事情是,最初的目標是相當適度的,從那以後的大部分工作都是將GDB擴充套件到不屬於原始計劃的環境和用法中。
4.3 框圖
圖4.1:GDB的總體結構
從最大規模來看,GDB可以說有兩個方面:
- “符號方面”涉及有關該程式的符號資訊。符號資訊包括函式和變數名稱和型別,行號,機器暫存器使用等。符號端從程式的可執行檔案中提取符號資訊,解析表示式,查詢給定行號的記憶體地址,列出原始碼,並且通常在程式設計師編寫時使用該程式。
- “目標方”涉及目標系統的操縱。它具有啟動和停止程式,讀取儲存器和暫存器,修改它們,捕獲訊號等功能。如何做到這一點的具體細節可能在系統之間有很大差異;大多數Unix型別的系統提供一個名為ptrace的特殊系統呼叫,它使一個程序能夠讀寫不同程序的狀態。因此,GDB的目標方面主要是關於進行ptrace呼叫和解釋結果。但是,對於嵌入式系統的交叉除錯,目標端構造訊息包以通過線路傳送,並等待響應資料包作為回報。
雙方在某種程度上相互獨立;您可以檢視程式程式碼,顯示變數型別等,而無需實際執行程式。相反,即使沒有可用的符號,也可以進行純機器語言除錯。
在中間,將兩側綁在一起,是命令直譯器和主執行控制迴圈。
4.4 操作示例
為了簡單說明它們如何結合在一起,請考慮上面的print命令。命令直譯器找到print命令函式,它將表示式解析為一個簡單的樹結構,然後通過遍歷樹來計算它。在某些時候,求值程式將查詢符號表以找出positive_variable是一個整數全域性變數,儲存在例如記憶體地址0x601028中。然後它呼叫目標端函式來讀取該地址處的四個位元組的記憶體,並將位元組交給格式化函式,該函式將它們顯示為十進位制數。
為了顯示原始碼及其編譯版本,GDB組合了原始檔和目標系統的讀取,然後使用編譯器生成的行號資訊來連線兩者。在此處的示例中,行232具有地址0x4004be,行233具有0x4004ce,依此類推。
[...]
232 result = positive_variable * arg1 + arg2;
0x4004be <+10>: mov 0x200b64(%rip),%eax # 0x601028 <positive_variable>
0x4004c4 <+16>: imul -0x14(%rbp),%eax
0x4004c8 <+20>: add -0x18(%rbp),%eax
0x4004cb <+23>: mov %eax,-0x4(%rbp)
233 return result;
0x4004ce <+26>: mov -0x4(%rbp),%eax
[...]
step命令步驟隱藏了幕後複雜的邏輯。當用戶要求步程序序中的下一行時,要求目標端只執行程式的單個指令,然後再次停止(這是ptrace可以做的事情之一)。在被通知程式已經停止時,GDB請求程式計數器(PC)暫存器(另一個目標端操作),然後將其與符號側所說的與當前行相關聯的地址範圍進行比較。如果PC超出該範圍,則GDB將程式停止,找出新的原始碼行,並將其報告給使用者。如果PC仍然在當前行的範圍內,那麼GDB將按照另一條指令再次檢查並重複檢查,直到PC到達另一條行。這種基本演算法的優點是它始終做正確的事情,無論線路是否有跳轉,子程式呼叫等,並且不需要GDB來解釋機器指令集的所有細節。缺點是對於每個單步驟存在與目標的許多互動,對於一些嵌入的目標,其導致明顯緩慢的步進。
4.5 可移植性
作為一個需要廣泛訪問的程式,一直到晶片上的物理暫存器,GDB從一開始就被設計為可以在各種系統中移植。然而,多年來,其可移植性策略發生了很大變化。
最初,GDB的開端類似於當時的其他GNU程式;在C的最小公共子集中編碼,並使用前處理器巨集和Makefile片段的組合來適應特定的體系結構和作業系統。雖然GNU專案的既定目標是一個獨立的“GNU作業系統”,但是必須在各種現有系統上進行自舉; Linux核心在未來還需要幾年時間。 configure shell指令碼是該過程的第一個關鍵步驟。它可以執行各種操作,例如從特定於系統的檔案建立符號連結到通用標頭名稱,或者從片段構造檔案,更重要的是用於構建程式的Makefile。
像GCC和GDB這樣的程式比像cat或diff這樣的程式具有額外的可移植性需求,隨著時間的推移,GDB的可移植性位被分成三個類,每個類都有自己的Makefile片段和標頭檔案。
- “主機”定義適用於GDB本身執行的機器,可能包括主機整數型別的大小。最初是作為人工編寫的標頭檔案完成的,人們最終可以通過配置執行小測試程式來計算它們,使用將用於構建工具的相同編譯器。這就是autoconf [aut12]的全部內容,而今天幾乎所有的GNU工具和許多(如果不是大多數)Unix程式都使用autoconf生成的配置指令碼。
- “目標”定義特定於執行正在除錯的程式的機器。如果目標與主機相同,那麼我們正在進行“本機”除錯,否則它是“交叉”除錯,使用連線兩個系統的某種線路。目標定義又分為兩大類:
- “架構”定義:這些定義定義瞭如何反彙編機器程式碼,如何遍歷呼叫堆疊以及在斷點處插入哪個陷阱指令。最初使用巨集完成,它們被遷移到通過“gdbarch”物件訪問的常規C,下面將更詳細地描述。
- “Native”定義:這些定義了ptrace引數的細節(在Unix的各種風格之間有很大差異),如何查詢已載入的共享庫等等,這些僅適用於本機除錯案例。本機定義是20世紀80年代風格巨集的最後一個版本,儘管大多數現在使用autoconf計算出來。
4.6 資料結構
在深入研究GDB的各個部分之前,讓我們來看看GDB使用的主要資料結構。由於GDB是一個C程式,它們被實現為結構而不是C ++風格的物件,但在大多數情況下它們被視為物件,在這裡我們遵循GDBers經常練習它們的物件。
斷點
斷點是使用者可直接訪問的主要物件型別。使用者使用break命令建立斷點,該命令的引數指定位置,該位置可以是函式名稱,源行號或機器地址。 GDB為斷點物件分配一個小的正整數,使用者隨後使用該整數對斷點進行操作。在GDB中,斷點是一個包含許多欄位的C結構。該位置被轉換為機器地址,但也以其原始形式儲存,因為地址可能會更改並需要重新計算,例如,如果程式被重新編譯並重新載入到會話中。
幾種類似斷點的物件實際上共享斷點結構,包括觀察點,捕獲點和跟蹤點。這有助於確保始終可以使用建立,操作和刪除工具。
術語“位置”還指要安裝斷點的儲存器地址。在行內函數和C ++模板的情況下,可能是單個使用者指定的斷點可能對應於多個地址;例如,函式的每個內聯副本都需要一個單獨的位置,用於在函式體內的原始碼行上設定的斷點。
符號和符號表
符號表是GDB的關鍵資料結構,可能非常大,有時會增加佔用多個GB的RAM。在某種程度上,這是不可避免的; C ++中的大型應用程式本身可以擁有數百萬個符號,並且它可以提取系統標頭檔案,這些檔案可以包含數百萬個符號。每個區域性變數,每個命名型別,列舉的每個值 - 所有這些都是單獨的符號。
GDB使用許多技巧來減少符號表空間,例如部分符號表(稍後更多關於這些),結構中的位欄位等。
除了基本上將字串對映到地址和型別資訊的符號表之外,GDB還構建了支援在兩個方向上查詢的行表;從原始碼行到地址,然後從地址返回到原始碼行。 (例如,前面描述的單步演算法至關重要地取決於地址到源的對映。)
堆疊幀
設計GDB的過程語言共享一個共同的執行時體系結構,因為函式呼叫會導致程式計數器被壓入堆疊,以及函式引數和本地引數的某種組合。該組合稱為堆疊幀,或簡稱為“幀”,並且在程式執行的任何時刻,堆疊由連結在一起的一系列幀組成。堆疊幀的細節從一個晶片架構到下一個晶片架構有很大不同,並且還取決於作業系統,編譯器和優化選項。
GDB到新晶片的埠可能需要相當大量的程式碼來分析堆疊,因為程式(特別是有缺陷的程式,使用者最感興趣的程式)可以在任何地方停止,框架可能不完整,或部分覆蓋通過該計劃。更糟糕的是,為每個函式呼叫構造一個堆疊幀會降低應用程式的速度,一個好的優化編譯器將利用每個機會簡化堆疊幀,甚至完全消除它們,例如尾部呼叫。
GDB晶片特定堆疊分析的結果記錄在一系列幀物件中。最初,GDB通過使用固定幀指標暫存器的字面值來跟蹤幀。這種方法對行內函數呼叫和其他型別的編譯器優化進行了細分,從2002年開始,GDBers引入了顯式框架物件,記錄了每個框架已經找到的內容,並且連結在一起,映象了程式的堆疊框架。
表示式
與堆疊幀一樣,GDB假定它支援的各種語言的表示式具有一定程度的通用性,並將它們全部表示為由節點物件構建的樹結構。節點型別集實際上是所有不同語言中可能的所有表達型別的聯合;與編譯器不同,沒有理由阻止使用者嘗試從C變數中減去Fortran變數 - 也許兩者的差異顯然是2的冪,這給了我們“aha”時刻。
值
評估的結果本身可能比整數或記憶體地址更復雜,並且GDB還將評估結果保留在編號的歷史列表中,然後可以在後面的表示式中引用它。為了完成所有這些工作,GDB具有值的資料結構。值結構有許多記錄各種屬性的欄位;重要的包括指示值是r值還是l值(l值可以分配給,如C中所示),以及該值是否是懶惰構造的。