1. 程式人生 > >GDB體系結構介紹(二)

GDB體系結構介紹(二)

4.7 符號方面

GDB的符號端主要負責讀取可執行檔案,提取它找到的任何符號資訊,並將其構建到符號表中。

讀取過程從BFD庫開始。 BFD是一種用於處理二進位制檔案和目標檔案的通用庫;在任何主機上執行,​​它可以讀取和寫入原始的Unix a.out格式,COFF(用於System V Unix和MS Windows),ELF(現代Unix,GNU / Linux和大多數嵌入式系統),以及其他一些檔案格式。在內部,庫具有複雜的C巨集結構,可擴充套件為程式碼,包含許多不同系統的目標檔案格式的神祕細節。 GNU彙編器和連結器也使用BFD,它為任何目標生成目標檔案的能力是使用GNU工具進行交叉開發的關鍵。 (移植BFD也是將工具移植到新目標的關鍵第一步。)

GDB僅使用BFD讀取檔案,使用它將可執行檔案中的資料塊拉入GDB的記憶體。然後GDB有兩個級別的讀者功能。第一級用於基本符號或“最小符號”,它們只是連結器完成其工作所需的名稱。這些是帶地址的字串,而不是其他內容;我們假設文字部分中的地址是函式,資料部分中的地址是資料,等等。

第二級是詳細的符號資訊,通常具有與基本可執行檔案格式不同的格式;例如,DWARF除錯格式的資訊包含在ELF檔案的特殊命名部分中。相比之下,Berkeley Unix的舊stabs除錯格式使用了儲存在通用符號表中的特殊標記符號。

用於讀取符號資訊的程式碼有點單調乏味,因為不同的符號格式編碼可能在源程式中的每種型別資訊,但每種型別資訊都以其自己的特殊方式進行。 GDB閱讀器只是遍歷格式,構建我們認為對應於符號格式的GDB符號。

部分符號表

對於大尺寸的程式(例如Emacs或Firefox),符號表的構造可能需要相當長的時間,甚至可能需要幾分鐘。測量結果一致表明,時間不是人們所期望的檔案讀取時間,而是GDB符號的記憶體構造。實際上涉及數百萬個小型互連物體,時間加起來。

大多數符號資訊永遠不會在會話中被檢視,因為它是使用者可能永遠不會檢查的函式的本地資訊。因此,當GDB首先引入程式的符號時,它會粗略地掃描符號資訊,只查詢全域性可見符號並僅在符號表中記錄它們。僅當用戶在其中停止時,才會填寫函式或方法的完整符號資訊。

部分符號表允許GDB在幾秒鐘內啟動,即使對於大型程式也是如此。 (共享庫符號也是動態載入的,但過程是相當不同的。通常GDB使用特定於系統的技術在載入庫時得到通知,然後構建一個符號表,其中的函式位於動態決定的地址上鍊接。)

語言支援

源語言支援主要包括表示式解析和值列印。表示式解析的細節留給每種語言,但一般來說,解析器基於由手工製作的詞法分析器提供的Yacc語法。為了與GDB為互動式使用者提供更多靈活性的目標保持一致,解析器預計不會特別嚴格;例如,如果它可以猜測表示式的合理型別,它將簡單地假設該型別,而不是要求使用者新增強制轉換或型別轉換。

由於解析器不需要處理語句或型別宣告,因此它比完整語言解析器簡單得多。類似地,對於列印,只需要顯示少數幾種型別的值,並且特定於語言的列印功能通常可以呼叫通用程式碼來完成作業。

4.8 目標方

目標方面是關於程式執行和原始資料的操縱。從某種意義上說,目標端是一個完整的低階偵錯程式;如果您滿足於逐步執行指令並轉儲原始記憶體,則可以使用GDB而無需任何符號。 (無論如何,如果程式碰巧在符號被刪除的庫中停止,你最終可能會以這種模式執行。)

目標向量和目標堆疊

最初GDB的目標端由少數特定於平臺的檔案組成,這些檔案處理呼叫ptrace,啟動可執行檔案等的細節。這對於長時間執行的除錯會話來說不夠靈活,在這些會話中,使用者可能會從本地除錯切換到遠端除錯,從檔案切換到核心轉儲到實時程式,連線和分離等,所以在1990年John Gilmore重新設計了目標端GDB通過目標向量傳送所有特定於目標的操作,目標向量基本上是一類物件,每個物件定義一種目標系統的細節。每個目標向量都實現為幾十個函式指標(通常稱為“方法”)的結構,其目的包括讀取和寫入記憶體和暫存器,恢復程式執行,設定引數以處理共享庫。 GDB中有大約40個目標向量,從用於Linux的常用目標向量到模糊向量,例如執行Xilinx MicroBlaze的向量。核心轉儲支援使用通過讀取核心檔案來獲取資料的目標向量,還有另一個目標向量從可執行檔案中讀取資料。

混合來自幾個目標載體的方法通常是有用的。考慮在Unix上列印初始化的全域性變數;在程式開始執行之前,列印變數應該可以工作,但是此時沒有要讀取的程序,並且位元組需要來自可執行檔案的.data部分。因此,GDB使用目標向量來執行可執行檔案並從二進位制檔案中讀取。但是在程式執行時,位元組應該來自程序的地址空間。因此,GDB有一個“目標堆疊”,當程序開始執行時,實時程序的目標向量被推送到可執行檔案的目標向量之上,並在它退出時彈出。

實際上,目標堆疊並不像人們想象的那樣像堆疊一樣。目標向量並不真正彼此正交;如果你在會話中同時擁有可執行檔案和實時程序,雖然讓實時程序的方法覆蓋可執行檔案的方法是有意義的,但反過來幾乎沒有意義。因此,GDB最終得出了一個層次的概念,其中“類似過程”的目標向量都在一個層,而“類檔案”目標向量被分配到較低層,並且目標向量可以插入以及推了推。

(雖然GDB維護者不太喜歡目標堆疊,但沒有人提出或原型化 - 任何更好的替代方案。)

Gdbarch

作為直接與CPU指令一起工作的程式,GDB需要深入瞭解晶片的細節。它需要知道所有暫存器,不同型別資料的大小,地址空間的大小和形狀,呼叫約定的工作方式,導致陷阱異常的指令等等。 GDB的所有這些程式碼通常在1,000到10,000多行C之間,具體取決於架構的複雜性。

最初這是使用特定於目標的前處理器巨集來處理的,但隨著偵錯程式變得越來越複雜,這些巨集越來越大,並且隨著時間的推移,巨集定義被定義為從巨集呼叫的常規C函式。雖然這有所幫助,但它對架構變體(ARM與Thumb,32位與64位版本的MIPS或x86等)沒有太大幫助,更糟糕的是,多架構設計即將出現,巨集根本不起作用。 1995年,我提議用基於物件的設計解決這個問題,從1998年開始,Cygnus Solutions資助Andrew Cagney開始轉換。 (Cygnus Solutions是一家成立於1989年的公司,為Red Hat在2000年收購的自由軟體提供商業支援。)花了幾年的時間和數十名黑客的貢獻來完成這項工作,總共影響了80,000行程式碼。

引入的構造稱為gdbarch物件,此時可能包含多達130個定義目標體系結構的方法和變數,儘管一個簡單的目標可能只需要十幾個這樣的方法。

要了解新舊方法的比較,請參閱gdb / config / i386 / tm-i386.h(大約2002年)x86 long double的大小為96位的宣告:

#define TARGET_LONG_DOUBLE_BIT 96

從2012年的gdb / i386-tdep.c:

i386_gdbarch_init( [...] )
{
  [...]

  set_gdbarch_long_double_bit (gdbarch, 96);

  [...]
}

執行控制

GDB的核心是它的執行控制迴圈。我們在描述單行踩線時更早地提到了它;該演算法需要迴圈多個指令,直到找到與不同源線相關聯的指令。迴圈稱為wait_for_inferior,簡稱為“wfi”。

從概念上講,它位於主命令迴圈中,並且僅為導致程式恢復執行的命令輸入。當用戶鍵入continue或step然後等待而沒有發生任何事情時,GDB實際上可能非常繁忙。除了上面提到的單步迴圈之外,程式可能正在命中陷阱指令並將異常報告給GDB。如果異常是由於陷阱是由GDB插入的斷點,則它會測試斷點的條件,如果為false,則刪除陷阱,單步執行原始指令,重新插入陷阱,然後讓程式恢復。類似地,如果發出訊號,GDB可以選擇忽略它,或者以預先指定的幾種方式之一處理它。

所有這些活動都由wait_for_inferior管理。最初這是一個簡單的迴圈,等待目標停止,然後決定如何處理它,但由於各種系統的埠需要特殊處理,它增長到一千行,goto語句縱橫交錯,原因很難理解。例如,隨著Unix變種的激增,沒有一個人能理解他們所有的優點,也沒有人能夠訪問所有他們的迴歸測試,所以有一種強烈的動機來修改程式碼保留現有埠的行為 - 並且跳過部分迴圈是一個非常簡單的策略。

對於任何型別的執行緒程式的非同步處理或除錯,單個大迴圈也是一個問題,其中使用者想要啟動和停止單個執行緒,同時允許程式的其餘部分繼續執行。

轉換為面向事件的模型花了幾年時間。我在1999年分解了wait_for_inferior,引入了一個執行控制狀態結構來替換一堆區域性變數和全域性變數,並將跳轉的糾結轉換為更小的獨立函式。與此同時,Elena Zannoni和其他人引入了事件佇列,其中包括來自使用者的輸入和來自下級的通知。

遠端協議

儘管GDB的目標向量架構允許多種方式來控制在不同計算機上執行的程式,但我們只有一個首選協議。它沒有區別名稱,通常只稱為“遠端協議”,“GDB遠端協議”,“遠端序列協議”(縮寫為“RSP”),“remote.c協議”(原始檔之後)實現它),或者有時是“存根協議”,指的是目標的協議實現。

基本協議很簡單,反映了將其用於20世紀80年代的小型嵌入式系統的願望,其儲存器以千位元組為單位進行測量。例如,協議包$ g請求所有暫存器,並期望包含所有暫存器的所有位元組的應答,所有暫存器一起執行 - 假設它們的數量,大小和順序將與GDB知道的相匹配。

該協議期望對傳送的每個資料包進行單一回復,並假設連線是可靠的,僅向傳送的資料包新增校驗和(因此$ g實際上通過網路傳送為$ g#67)。

儘管只有少數必需的資料包型別(對應於最重要的六種目標向量方法),但多年來已添加了大量額外的可選資料包,以支援從硬體斷點,跟蹤點到共享的所有內容。庫。

在目標本身上,遠端協議的實現可以採用多種形式。該協議在GDB手冊中有完整的記錄,這意味著可以編寫一個不受GNU許可證限制的實現,事實上,許多裝置製造商已經在實驗室和實驗室中集成了代表GDB遠端協議的程式碼。場。執行大部分網路裝置的思科IOS就是一個眾所周知的例子。

目標的協議實現通常被稱為“除錯存根”,或者只是“存根”,意味著它不會自己做很多工作。 GDB源包括一些示例存根,通常約為1,000行低級別C。在沒有作業系統的完全裸板上,存根必須安裝自己的處理程式以用於硬體異常,最重要的是捕獲陷阱指令。如果硬體連結是序列線,它還需要序列驅動程式程式碼。實際的協議處理很簡單,因為所有必需的資料包都是可以用switch語句解碼的單個字元。

遠端協議的另一種方法是構建一個“精靈”,它在GDB和專用除錯硬體之間進行介面,包括JTAG裝置,“搖擺器”等。這些裝置通常都有一個必須在物理上連線到目標的計算機上執行的庫。通常,庫API在架構上與GDB的內部結構不相容。因此,雖然GDB的配置直接呼叫了硬體控制庫,但事實證明,將sprite作為一個獨立的程式執行更簡單,該程式瞭解遠端協議並將資料包轉換為裝置庫呼叫。

GDBSERVER

GDB源確實包括遠端協議目標端的一個完整且有效的實現:GDBserver。 GDBserver是一個本機程式,在目標作業系統下執行,並使用其本機除錯支援控制目標作業系統上的其他程式,以響應通過遠端協議接收的資料包。換句話說,它充當本機除錯的一種代理。

GDBserver不執行本機GDB無法執行的任何操作;如果您的目標系統可以執行GDBserver,那麼從理論上講它可以執行GDB。但是,GDBserver小10倍,不需要管理符號表,因此對於嵌入式GNU / Linux用法等非常方便。

                                           圖4.2:GDBserver

GDB和GDBserver共享一些程式碼,但是雖然封裝特定於作業系統的程序控制是一個明顯的想法,但在本機GDB中分離出預設依賴關係存在實際困難,並且轉換進展緩慢。

 

原文連結