http://www.ituring.com.cn/article/5916
2012
原文鏈接:http://www.aosabook.org/en/gdb.html
作者:Stan Shebs
GDB, 即GNU調(diào)試器(GNU Debugger)。它誕生自開源軟件基金會(huì)(Free Software Foundation)成立之初的第一批程序,并一直是免費(fèi)和開源軟件系統(tǒng)中的主要成員。最初GDB只是Unix系統(tǒng)上一個(gè)簡單的源碼層次的調(diào)試器,代碼量不過數(shù)千行C代碼,后來逐步發(fā)展壯大,拓展到包括嵌入式系統(tǒng)在內(nèi)多個(gè)平臺(tái),代碼量也達(dá)到了上百萬行。
GDB在發(fā)展,不斷地滿足著新的用戶需求并增加新的功能。這一章將我們將介紹GDB的整體內(nèi)部結(jié)構(gòu),探討一下GDB是如何做到這一點(diǎn)的。
4.1 目標(biāo)
GDB的設(shè)計(jì)目標(biāo)是一個(gè)針對使用命令式(imperative)語言(例如C,C++,Ada,Fortran等)編寫的程序的符號(hào)調(diào)試器。使用GDB原始命令行界面的一個(gè)示例如下:
% gdb myprog[...](gdb) break buggy_functionBreakpoint 1 at 0x12345678: file myprog.c, line 232.(gdb) run 45 92Starting program: myprogBreakpoint 1, buggy_function (arg1=45, arg2=92) at myprog.c:232232 result = positive_variable * arg1 + arg2;(gdb) print positive_variable$$1 = -34(gdb)
GDB能顯示程序中的錯(cuò)誤,開發(fā)者據(jù)此判斷錯(cuò)誤的類型并找到解決的方案。
設(shè)計(jì)GDB最需要考慮的是調(diào)試工具的交互性,因?yàn)橛脩粼谡{(diào)試時(shí)提交的請求是不可預(yù)測的。此外,GDB還需要深入到系統(tǒng)最底層,因?yàn)榫幾g器會(huì)充分利用硬件的各種選項(xiàng)來優(yōu)化程序的性能。
GDB還要求能夠調(diào)試不同編譯器編譯的程序(不僅僅是GNU C編譯器),能夠調(diào)試過時(shí)編譯器編譯的程序,能夠調(diào)試符號(hào)信息丟失、過時(shí)或錯(cuò)誤的程序。所以,另外一個(gè)設(shè)計(jì)要求是,即使程序中的數(shù)據(jù)丟失、損壞或干脆無法理解,GDB也能夠繼續(xù)工作并發(fā)揮作用。
接下來的幾章假定讀者熟悉GDB基本的命令行使用方法。如果你還是新手,建議先用一用GDB并細(xì)讀一下手冊[SPS+00]。
4.2 GDB的起源
GDB程序歷史悠久,早在1985年就已經(jīng)存在。它的作者是Richard Stallman,這個(gè)人還編寫了GCC,GNU Emacs和其它一些早期的GNU軟件。(由于當(dāng)時(shí)并沒有軟件倉庫,GDB開發(fā)過程的細(xì)節(jié)已不為人所知。)
GDB的最早的穩(wěn)定版本在1988年發(fā)布,但在今天的GDB源碼中已經(jīng)找不到多少相似的地方了,GDB被完全重寫過至少一次。令人驚訝的是,早期的GDB并沒有太大的野心,后來的平臺(tái)移植和功能擴(kuò)展并沒有包括在GDB最初的計(jì)劃之中。
4.3 GDB結(jié)構(gòu)框圖
總體來講,GDB內(nèi)部結(jié)構(gòu)可分為兩大塊:
目標(biāo)端,涉及到目標(biāo)系統(tǒng)的操控。目標(biāo)端包含了基本的調(diào)試工具,包括啟動(dòng)和終止程序,讀取或修改內(nèi)存和寄存器,捕捉信號(hào),等等。這些工具的實(shí)現(xiàn)在不同的系統(tǒng)上可能會(huì)相差很大。大部分Unix類操作系統(tǒng)上都提供了一個(gè)系統(tǒng)函數(shù)ptrace
,ptrace
可以讓一個(gè)進(jìn)程讀寫另一個(gè)進(jìn)程的狀態(tài)。因此,GDB的目標(biāo)端的主要工作就是調(diào)用ptrace
和解析結(jié)果。對于嵌入式系統(tǒng)的交叉調(diào)試,過程有所不同,目標(biāo)端通過數(shù)據(jù)線發(fā)送消息包,然后等待應(yīng)答。
這兩大模塊相互較為獨(dú)立,用戶可以查看程序的代碼,顯示變量類型,但不需要實(shí)際地運(yùn)行程序。反過來,不用符號(hào)信息完全使用機(jī)器碼調(diào)試也是可能的。
將符號(hào)端和目標(biāo)端連接起來的中間層是命令解釋器和主程序運(yùn)行控制循環(huán)。
4.4 操作實(shí)例
為了解GDB各部分是如何協(xié)同工作的,不妨考慮一下前面示例中提到的print
命令。命令解釋器會(huì)搜索print
命令函數(shù),該函數(shù)將表達(dá)式轉(zhuǎn)化為一個(gè)簡單的樹結(jié)構(gòu),通過遍歷樹結(jié)構(gòu)來運(yùn)算這個(gè)表達(dá)式。運(yùn)算器會(huì)查詢符號(hào)表,它發(fā)現(xiàn)positive_variable
是一個(gè)全局整型變量,其存儲(chǔ)地址是0x601028
。隨后,它會(huì)調(diào)用一個(gè)目標(biāo)端中的函數(shù)來獲取該地址中的4個(gè)字節(jié)內(nèi)容,將結(jié)果傳遞給格式化函數(shù),并顯示為一個(gè)數(shù)字。
為了同步顯示源碼和對應(yīng)的編譯后代碼,GDB同時(shí)讀取源碼文件和目標(biāo)文件,然后使用編譯器產(chǎn)生的行號(hào)信息將兩者聯(lián)系起來。在本例中,232行的地址是0x4004be
,233行是0x4004ce
,等等。
[...]232 result = positive_variable * arg1 + arg2;0x4004be <+10>: mov 0x200b64(%rip),%eax # 0x601028 <positive_variable>0x4004c4 <+16>: imul -0x14(%rbp),%eax0x4004c8 <+20>: add -0x18(%rbp),%eax0x4004cb <+23>: mov %eax,-0x4(%rbp)233 return result;0x4004ce <+26>: mov -0x4(%rbp),%eax[...]
單步執(zhí)行命令step
背后則稍為復(fù)雜。當(dāng)用戶使用step
請求執(zhí)行到下一行時(shí),目標(biāo)端只執(zhí)行程序的一個(gè)指令后就再次暫停(ptrace
支持類似的操作)。當(dāng)獲悉程序已經(jīng)停止了,GDB讀取程序計(jì)數(shù)器(PC, program counter)寄存器(另外一個(gè)目標(biāo)端操作),并與符號(hào)端中記錄的當(dāng)前行的地址范圍進(jìn)行比較。如果程序計(jì)數(shù)器在這個(gè)范圍之外,GDB允許程序停止,并獲取新的代碼行反饋給用戶。如果程序計(jì)數(shù)器仍然在地址范圍之內(nèi),GDB會(huì)重復(fù)執(zhí)行指令和檢查計(jì)數(shù)器的操作,直到程序計(jì)數(shù)器到達(dá)新的代碼行。這個(gè)簡單的算法保證了調(diào)試的邏輯正確性,無論當(dāng)前行是跳轉(zhuǎn)還是子函數(shù)調(diào)用都不會(huì)出錯(cuò),而且也不要求GDB去理解機(jī)器指令集的所有細(xì)節(jié)。但不足之處是,一個(gè)單步執(zhí)行命令(step
)會(huì)產(chǎn)生與目標(biāo)之間的多個(gè)交互過程。對于嵌入式調(diào)試,這個(gè)算法會(huì)導(dǎo)致單步執(zhí)行變得很慢。
4.5 可移植性
由于需要大量地訪問芯片上的物理寄存器,GDB最初的設(shè)計(jì)就考慮了面向不同系統(tǒng)的可移植性問題。 但是GDB的可移植性策略卻是與時(shí)俱進(jìn)的。
最初, GDB和當(dāng)時(shí)的其它GNU程序一樣, 使用C語言的最小子集編碼, 結(jié)合預(yù)處理宏和Makefile
腳本來適應(yīng)特定的硬件結(jié)構(gòu)和操作系統(tǒng)。 GNU項(xiàng)目的目標(biāo)是完備的"GNU操作系統(tǒng)"(實(shí)際上,很多年之后Linux內(nèi)核才問世), 因此其系統(tǒng)引導(dǎo)程序(bootstrapping)必須考慮多種已有平臺(tái)。在面向多平臺(tái)移植的過程中, configure
腳本是第一個(gè)關(guān)鍵步驟。configure
要做的事情很多,比如用符號(hào)鏈接將特定平臺(tái)的文件統(tǒng)一為通用的頭文件,比如從多個(gè)配置文件出發(fā)生成結(jié)果文件(在構(gòu)建軟件時(shí),主要目標(biāo)是生成Makefile
文件)。
和cat
,diff
之類的程序一樣,GCC和GDB也有額外的平臺(tái)移植需求。隨時(shí)間變化,GDB的可移植性問題分成了三類,每一類都有獨(dú)立的Makefile
腳本和頭文件。
"主機(jī)(host)"定義。指GDB運(yùn)行時(shí)所在的機(jī)器的信息,包含了主機(jī)整型大小等信息。原來這些頭文件是手工編寫的,漸漸地人們發(fā)現(xiàn)configure
可以自動(dòng)完成這個(gè)工作。configure
腳本會(huì)調(diào)用一些測試程序,這些測試程序與GDB使用的是同一個(gè)編譯器。這些都是autoconf
[aut12]的工作, 幾乎所有的GNU工具和許多Unix程序都在使用autoconf
來生成configure
腳本。
"目標(biāo)(target)"定義。待調(diào)試程序所在的特定機(jī)器上的信息。如果目標(biāo)機(jī)器和主機(jī)是同一臺(tái)機(jī)器,這樣的調(diào)試稱為本地(native)調(diào)試,否則稱為交叉(cross)調(diào)試(主機(jī)和目標(biāo)機(jī)器通過數(shù)據(jù)線連接)。目標(biāo)定義又分為兩個(gè)主要類別:
gdbarch
"對象(后面會(huì)進(jìn)一步介紹)。ptrace
的參數(shù)規(guī)范(不同Unix之間變化很大),如何搜索已加載的動(dòng)態(tài)鏈接庫,等等。本地定義只適用于本地調(diào)試的情況,它是從80年代的那些宏中遺留下來的,其它的都已經(jīng)被autoconf
取代了。4.6 數(shù)據(jù)結(jié)構(gòu)
在深入了解GDB各部分之前, 讓我們先看看GDB主要的數(shù)據(jù)結(jié)構(gòu)。作為一個(gè)C程序,GDB必然使用struct
而非C++對象(object)。但是,struct也是可以視為對象(object)的,而GDB開發(fā)者也喜歡稱其為對象, 那么我們也就入鄉(xiāng)隨俗了。
斷點(diǎn)(breakpoint)
斷點(diǎn)是用戶能夠直接訪問得到的主要對象。用戶使用break
命令創(chuàng)建一個(gè)斷點(diǎn),其參數(shù)為斷點(diǎn)的位置,位置可以是函數(shù)名,源碼行號(hào)或機(jī)器地址。GDB為每個(gè)斷點(diǎn)對象指定一個(gè)正整數(shù)作為其標(biāo)識(shí)符,用戶將通過這個(gè)正整數(shù)來操縱斷點(diǎn)。在GDB中,斷點(diǎn)是一個(gè)內(nèi)容豐富的C語言結(jié)構(gòu)體。位置信息會(huì)被翻譯為機(jī)器地址,但仍然保留其原始形式,因?yàn)闄C(jī)器地址可能會(huì)發(fā)生變化(比如在不退出會(huì)話的情況下重新編譯運(yùn)行)。
還有其它一些斷點(diǎn)類對象也使用斷點(diǎn)的數(shù)據(jù)結(jié)構(gòu),包括觀察點(diǎn)(watchpoint),捕捉點(diǎn)(catchpoint),跟蹤點(diǎn)(tracepoint)。數(shù)據(jù)結(jié)構(gòu)的共享保證了一些公共操作(創(chuàng)建,操縱和刪除)對這些對象的通用性。
"位置"一詞還可以指斷點(diǎn)定義處的內(nèi)存地址。對于inline函數(shù)和C++模板,用戶定義的一個(gè)斷點(diǎn)可能會(huì)對應(yīng)到多個(gè)地址。比如當(dāng)一個(gè)斷點(diǎn)定義到inline函數(shù)上時(shí),代碼中所有使用這個(gè)函數(shù)的位置都會(huì)有斷點(diǎn)存在。
符號(hào)和符號(hào)表
符號(hào)表是GDB中的核心數(shù)據(jù)結(jié)構(gòu), 它的數(shù)據(jù)量很大, 有時(shí)甚至?xí)_(dá)到數(shù)G字節(jié)。 從某種意義上說, 這也是無法避免的。 每個(gè)局部變量,每種類型,每個(gè)枚舉值,都是獨(dú)立的符號(hào)。一個(gè)大型C++程序本身就包含了數(shù)百萬個(gè)符號(hào),而 它所引用的頭文件同樣會(huì)有數(shù)百萬符號(hào)。
GDB使用了很多技巧來減少符號(hào)表占用的空間,比如使用不完全符號(hào)表(partial symbol table,后面會(huì)有介紹),在結(jié)構(gòu)體中使用比特位,等等。
符號(hào)表的作用是建立字符串到地址和類型信息之間的映射,除此之外, GDB還建立了一些支持雙向查詢的行號(hào)表: 從源碼行查詢地址,從地址查詢源碼行。(早前介紹的單步執(zhí)行算法就嚴(yán)重依賴于地址到源碼的映射。)
棧幀
GDB支持的過程式語言運(yùn)行時(shí)都有一個(gè)相似過程, 即函數(shù)調(diào)用會(huì)引起程序計(jì)數(shù)器,函數(shù)參數(shù),以及局部參數(shù)的入棧。這些入棧數(shù)據(jù)的組合體稱為"棧幀(stack frame)", 或簡稱"幀"。在程序執(zhí)行的任何時(shí)刻,棧中都包含了多個(gè)串連在一起的幀。棧幀的細(xì)節(jié)取決于芯片體系結(jié)構(gòu),還和操作系統(tǒng),編譯器,以及優(yōu)化選項(xiàng)有關(guān)系。
將GDB遷移到新的芯片時(shí)需要編寫大量的代碼來分析棧,因?yàn)橛脩舫绦?特別是帶Bug的程序)可能在任何地方暫停運(yùn)行,屆時(shí)幀可能并不完整,部分甚至?xí)怀绦蚋采w。更糟糕的是,為每個(gè)函數(shù)調(diào)用創(chuàng)建一個(gè)棧幀會(huì)影響程序效率,因而編譯器在優(yōu)化時(shí)會(huì)盡可能地簡化棧幀,甚至完全消除(tail調(diào)用即是如此)。
對于特定芯片的棧的分析結(jié)果保存在一系列的幀對象中。最初,GDB使用一個(gè)固定幀指針寄存器來跟蹤幀。但這個(gè)方法對inline函數(shù)調(diào)用以及其它編譯器優(yōu)化不起作用。從2002年開始,GDB開發(fā)人員引入了顯式幀對象(explicit frame object)來記錄每一幀的信息,這些顯式幀對象鏈接在一起,并映射到程序的棧幀上。
表達(dá)式
對于棧幀,GDB假定它所支持的不同語言的表達(dá)式具有一定的共性,并將表達(dá)式表達(dá)為一個(gè)由結(jié)點(diǎn)對象構(gòu)成的樹結(jié)構(gòu)。實(shí)際上, 結(jié)點(diǎn)的類型集合是所有不同語言中所有可能的表達(dá)式類型的一個(gè)聯(lián)合。和編譯器不一樣,GDB允許Fortran變量和C變量之間的減法,雖然兩種變量類型相差甚遠(yuǎn)并且結(jié)果會(huì)人大吃一驚。
值(value)
表達(dá)式計(jì)算得到的結(jié)果可能要比一個(gè)整數(shù)或內(nèi)存地址更為復(fù)雜,GDB將這些結(jié)果保存在一個(gè)經(jīng)過編號(hào)的歷史列表中,以便在后面的表達(dá)式中能夠訪問得到。為實(shí)現(xiàn)這個(gè)功能,GDB有一個(gè)關(guān)于值(value)的數(shù)據(jù)結(jié)構(gòu)。value結(jié)構(gòu)體(struct
)包含了大量的成員來記錄其屬性,包括標(biāo)記這個(gè)值是左值還是右值(左值可以被賦值),以及這個(gè)值是否由懶構(gòu)造(lazy construction)得到。
4.7. 符號(hào)端
GDB的符號(hào)端的功能主要是讀取可執(zhí)行文件,提取所有的符號(hào)信息,然后構(gòu)造一個(gè)符號(hào)表。
讀取可執(zhí)行文件的首先要調(diào)用BFD軟件庫。 BFD是一個(gè)通用的處理二進(jìn)制和對象(object)文件的軟件庫,支持從任意主機(jī)上讀取Unix的a.out
格式,COFF格式(用于System V Unix系統(tǒng)和微軟Windows操作系統(tǒng)),ELF格式(用于現(xiàn)代Unix, GNU/Linux和大部分嵌入式系統(tǒng)),以及其它文件格式。BFD內(nèi)部采用一個(gè)復(fù)雜的C語言宏,這個(gè)宏展開成代碼之后能夠深入到對象文件格式的復(fù)雜細(xì)節(jié)中去,而這些對象文件可能來自于幾十個(gè)不同平臺(tái)。BFD從1990年被引入到GNU匯編器和鏈接器,它對多種對象文件輸出的支持成為跨平臺(tái)開發(fā)的關(guān)鍵因素。(自然,將GDB移植到一個(gè)新的平臺(tái)的首要條件是將BFD移植過去。)
GDB只用BFD來讀取文件,將可執(zhí)行文件中的數(shù)據(jù)塊讀到GDB的內(nèi)存空間中去。GDB本身擁有兩個(gè)層次的讀入函數(shù)。第一個(gè)層次針對基本符號(hào),或最簡符號(hào)(minimal symbols),只包含了鏈接器需要的名稱。這些基本符號(hào)只是一些帶地址的字符串,在這一層次下,我們假定文本節(jié)(text section)中的地址都是函數(shù),而數(shù)據(jù)節(jié)(data section)中的都是數(shù)據(jù),依此類推。
第二個(gè)層次針對詳細(xì)的符號(hào)信息,通常這種符號(hào)信息擁有與可執(zhí)行文件不同的格式。例如,DWARF調(diào)試格式中的信息存儲(chǔ)在ELF文件中單獨(dú)命名的節(jié)(section)內(nèi)。而Berkeley Unix系統(tǒng)中用到的舊的stabs
調(diào)試格式將這些詳細(xì)符號(hào)信息加上特別的標(biāo)記后存儲(chǔ)在通用符號(hào)表中。
閱讀符號(hào)信息的代碼非常無聊,因?yàn)椴煌姆?hào)格式都需要將源碼中的每個(gè)類型信息進(jìn)行編碼,而每一種符號(hào)格式的編碼方式又都不一樣。GDB的文件閱讀器的工作就是掃描符號(hào)格式,將其轉(zhuǎn)化為原來的形式。
不完全符號(hào)表
對于較大規(guī)模的程序(如Emacs或Firefox),建立符號(hào)表是比較費(fèi)時(shí)的,有可能會(huì)達(dá)到幾分鐘的時(shí)間。實(shí)踐表明文件加載時(shí)間倒不是主要的,主要是瓶頸在于內(nèi)存中GDB符號(hào)的構(gòu)造。一個(gè)程序中往往存在著上百萬個(gè)小對象相互聯(lián)系著,處理起來時(shí)間開銷非常大。
大部分符號(hào)信息在一個(gè)GDB會(huì)話中從來不會(huì)用到,因?yàn)樗鼈儊碜杂诤瘮?shù)的局部作用域。所以,GDB第一次導(dǎo)入程序的符號(hào)時(shí),它先掃描一下符號(hào)信息,只把全局可見的符號(hào)存進(jìn)符號(hào)表。當(dāng)用戶在某個(gè)函數(shù)內(nèi)暫停運(yùn)行時(shí),這個(gè)函數(shù)的完整的符號(hào)信息才會(huì)動(dòng)態(tài)加載進(jìn)來。
在GDB中,不完全符號(hào)表使得大程序也能在數(shù)秒內(nèi)啟動(dòng)。(動(dòng)態(tài)鏈接庫的符號(hào)也會(huì)動(dòng)態(tài)加載,但過程完全不同。當(dāng)動(dòng)態(tài)鏈接庫被加載時(shí), 平臺(tái)會(huì)通知GDB建立一個(gè)符號(hào)表,符號(hào)表中存儲(chǔ)了動(dòng)態(tài)鏈接地址對應(yīng)的那些函數(shù)。這個(gè)過程取決于特定平臺(tái)的消息機(jī)制,不同的平臺(tái)會(huì)有所不同。)
語言支持
對源碼語言的支持主要包括表達(dá)式解析和值的打印。表達(dá)式解析由語言自身負(fù)責(zé),但一般來說表達(dá)式解析器是一個(gè)基于Yacc語法的詞法分析器。為了讓GDB在用戶交互操作時(shí)具有更大的靈活性,解析器不需要對語法有嚴(yán)格的要求。比如,如果用戶能合理地猜出來表達(dá)式的類型,那他就不需要顯式地做類型轉(zhuǎn)換。
GDB表達(dá)式解析器不需要考慮變量聲明和類型聲明,比完整的語言解析器要簡單得多。類似的,值的打印,也只有考慮一部分類型的值,甚至還可以由特定語言的函數(shù)來實(shí)現(xiàn)。
4.8. 目標(biāo)端
目標(biāo)端的功能是操縱程序的執(zhí)行和處理底層原始數(shù)據(jù)。從某種意義上講,目標(biāo)端是一個(gè)完全低層次的調(diào)試器。如果只是逐個(gè)指令調(diào)試并打印原始內(nèi)存,用戶根本就不需符號(hào)信息。(如果程序剛好在一個(gè)被剝離符號(hào)的軟件庫中暫停,你也只能使用這種模式。)
目標(biāo)向量和目標(biāo)向量棧
最初,GDB的目標(biāo)端由一些特定平臺(tái)上的文件組成,用于處理ptrace的調(diào)用,啟動(dòng)可執(zhí)行文件等等。但這對于長時(shí)間運(yùn)行的GDB會(huì)話來說是不夠靈活的,因?yàn)橛脩艨赡軙?huì)中途變化調(diào)試目標(biāo)或方式,比如從本地調(diào)試切換到遠(yuǎn)程調(diào)試,從調(diào)試core文件切換到調(diào)試運(yùn)行的程序,從附加(attach)線程變?yōu)榉蛛x(detach),等等。1990年,John Gilmore重新設(shè)計(jì)了GDB的目標(biāo)端,使用目標(biāo)向量來流水處理特定目標(biāo)的操作。目標(biāo)向量主要是由一類定義了目標(biāo)系統(tǒng)特性的對象,每個(gè)目標(biāo)向量是多個(gè)函數(shù)指針(通常稱為"方法")構(gòu)成的結(jié)構(gòu)體,這些方法的功能包括讀寫寄存器內(nèi)存,恢復(fù)程序運(yùn)行,設(shè)置處理共享庫時(shí)的參數(shù)。GDB中大概有40多個(gè)目標(biāo)向量,包括有名的針對Linux的目標(biāo)向量,以及不那么出名的操縱Xilinx MicroBlaze的目標(biāo)向量。對Core dump的支持使用了一個(gè)從corefile中獲取數(shù)據(jù)的目標(biāo)向量,對應(yīng)的,還有從可執(zhí)行文件中獲取數(shù)據(jù)的目標(biāo)向量。
通常將幾個(gè)目標(biāo)向量混合使用比較有利。以在Unix上打印一個(gè)已初始化的全局變量為例,在程序開始運(yùn)行之前,GDB也要能夠支持對這個(gè)變量的打印,但這個(gè)時(shí)候進(jìn)程并沒有啟動(dòng),數(shù)據(jù)只能從可執(zhí)行文件的.data
節(jié)(section)獲取,所以GDB只能使用針對可執(zhí)行文件的目標(biāo)向量來讀取二進(jìn)制文件。但是如果程序已經(jīng)運(yùn)行,數(shù)據(jù)就應(yīng)該從進(jìn)程的地址空間中獲取。這時(shí)候,GDB就會(huì)使用"目標(biāo)向量棧",運(yùn)行進(jìn)程目標(biāo)向量被推入棧頂,置于可執(zhí)行文件目標(biāo)向量之上,當(dāng)進(jìn)程退出時(shí)棧頂目標(biāo)向量就會(huì)被彈出。
實(shí)際上,目標(biāo)向量棧和你想像中的棧并不完全相同,目標(biāo)向量之間并不是完全獨(dú)立的。如果一個(gè)GDB會(huì)話同時(shí)調(diào)試一個(gè)可執(zhí)行文件和一個(gè)運(yùn)行進(jìn)程,幾乎總是讓進(jìn)程的方法覆蓋可執(zhí)行文件的方法。所以GDB提出"階層(stratum)"的概念,令所有"進(jìn)程類"的目標(biāo)向量位于較高的階層,而所有"文件類"的目標(biāo)向量位于較低階層,目標(biāo)向量棧支持目標(biāo)向量的推入(push)和彈出(pop),還支持插入操作。
(雖然GDB的維護(hù)者們并不怎么喜歡目標(biāo)向量棧,但是還沒有人能提出或?qū)崿F(xiàn)更好的的方案。)
Gdbarch
因?yàn)槌绦蛑苯雍虲PU的指令打交道,GDB需要深入了解芯片的細(xì)節(jié),比如,所有的寄存器的信息,不同種類數(shù)據(jù)的大小,地址空間的大小和形狀,調(diào)用約定是怎么工作的,什么指令會(huì)導(dǎo)致trap異常,等等。GDB中這一類工作的代碼量取決于芯片的復(fù)雜度,從1000行到10000行的C代碼都是有可能的。
最初,這個(gè)工作是由特定目標(biāo)的預(yù)處理宏來完成的,但是隨著調(diào)試器變得越來越復(fù)雜,這些宏變得越來越長,以致于不得不讓部分宏變成了C函數(shù)(由其它宏來調(diào)用)。雖然這暫時(shí)減小了宏的復(fù)雜度,但是無助于解決平臺(tái)的多樣性問題(ARM或Thumb, 32位或64位, 64位MPIS或x86,等等)。更糟糕的是,多體系結(jié)構(gòu)設(shè)計(jì)開始出現(xiàn),對此,宏已經(jīng)無能為力。1995年,我提出使用面向?qū)ο蟮脑O(shè)計(jì)來解決這個(gè)問題。從1998年開始Cygnus Solutions公司資助Andrew Cagney來開始實(shí)現(xiàn)這個(gè)設(shè)計(jì)。(Cygnus Solutions是一家1989年創(chuàng)立的提供免費(fèi)軟件商業(yè)支持的公司,2000年被Red Hat收購)。在幾十個(gè)黑客數(shù)年的努力下,這個(gè)工作終于完成,其代碼量大概有80000行。
新引入的結(jié)構(gòu)稱為gdbarch
對象, 目前它包含了多達(dá)130個(gè)方法和變量來定義目標(biāo)體系結(jié)構(gòu), 其實(shí)一個(gè)簡單的目標(biāo)平臺(tái)也許只需要幾十個(gè)。
為了比較其差異,我們來看一下"將x86平臺(tái)下long doubles類型的大小定義為96"在新舊方式下分別是如何實(shí)現(xiàn)的:
gdb/i38-tdep.c
中2012行處的代碼(舊方式)
#define TARGET_LONG_DOUBLE_BIT 96
gdb/config/i386/tm-i386.h
中2002行(新方式)
i386_gdbarch_init( [...] ){ [...] set_gdbarch_long_double_bit (gdbarch, 96); [...]}
運(yùn)行控制
GDB的核心是運(yùn)行控制循環(huán), 前面描述單步執(zhí)行一行代碼時(shí)提到過這個(gè)名詞: 用一個(gè)簡單的循環(huán),來判斷指令是否運(yùn)行到了下一行源代碼。這個(gè)循環(huán)稱為wait_for_inferior
,或簡稱為wfi。
從概念上看, wfi位于主程序命令循環(huán)內(nèi)部, 并且只有在程序恢復(fù)執(zhí)行時(shí)才會(huì)進(jìn)入wfi循環(huán)。當(dāng)用戶提交continue
或step
命令時(shí),看起來似乎什么也沒發(fā)生,其實(shí)這時(shí)候的GDB忙得很。除了前面提到的單步運(yùn)行循環(huán),程序還可能會(huì)執(zhí)行到trap指令并將此異常匯報(bào)給GDB。如果遇到一個(gè)由斷點(diǎn)引發(fā)的trap異常,GDB會(huì)判斷這個(gè)斷點(diǎn)的條件,如果條件為假,則移除此trap指令,繼續(xù)執(zhí)行單步運(yùn)行循環(huán),然后重新插入trap指令并令程序恢復(fù)執(zhí)行。類似的,如果接收到一個(gè)信號(hào),GDB可能會(huì)選擇忽略,或根據(jù)預(yù)先指定的方式來處理。
所有這些活動(dòng)都由wait_for_inferior
來管理。最初wfi只是一個(gè)簡單的循環(huán),等待目標(biāo)停止執(zhí)行然后決定接下來怎么辦,但移植到新的平臺(tái)意味著增加新的需求,這個(gè)循環(huán)漸漸地增加到了1000行代碼,而且變得難懂以至于不得不使用goto
語句。隨著增加對更多種Unix系統(tǒng)的支持,沒有一個(gè)人能夠理解所有的代碼,也沒有人能夠?qū)λ械拇a進(jìn)行回歸測試。所以代碼重構(gòu)顯得非常有必要,保留已有平臺(tái)的行為然后使用goto語句跳過循環(huán)中的部分代碼只是一個(gè)權(quán)宜之計(jì)。
這個(gè)龐大的循環(huán)在異步處理時(shí)也是有問題的。 因?yàn)?,在調(diào)試多線程程序時(shí), 用戶需要在程序其它部分保持運(yùn)行的同時(shí)調(diào)試某一個(gè)線程。
GDB從wfi轉(zhuǎn)變?yōu)槭录?qū)動(dòng)模型花費(fèi)了數(shù)年的時(shí)間。1999年,我將wait_for_inferior
拆分開來,引入了一個(gè)執(zhí)行控制狀態(tài)結(jié)構(gòu)體,取代本地和全局的大量雜亂的變量,并將復(fù)雜的跳轉(zhuǎn)封裝到一些小型獨(dú)立的函數(shù)中。同時(shí)Elena Zannoni和其它人引入了事件隊(duì)列,該隊(duì)列的輸入既包含用戶的操作,還包括來自底層的通知。
遠(yuǎn)程協(xié)議
雖然GDB的目標(biāo)向量體系允許在不同計(jì)算機(jī)上以多種方式來控制程序的運(yùn)行, 但是我們傾向于使用單一的協(xié)議。這個(gè)協(xié)議并沒有一個(gè)獨(dú)立而準(zhǔn)確的名稱,它使用過的名稱包括"遠(yuǎn)程協(xié)議(remote protocol)","GDB遠(yuǎn)程協(xié)議", "遠(yuǎn)程串行協(xié)議(Remote serial protocal, 簡寫為RSP)","遠(yuǎn)程C協(xié)議(用實(shí)現(xiàn)語言命名)",或"樁協(xié)議(stub protocol)",其實(shí)都是指目標(biāo)系統(tǒng)對這個(gè)協(xié)議的實(shí)現(xiàn)。
基本的協(xié)議比較簡單,主要面向19世紀(jì)80年代的小型嵌入式系統(tǒng),其內(nèi)存不過幾千字節(jié)。GDB向所有的寄存器發(fā)出協(xié)議數(shù)據(jù)包$g
,請求獲得所有寄存器的所有內(nèi)容,GDB假定這些寄存器的數(shù)目,大小和順序都是已知的。
協(xié)議假定連接是可靠的,且每個(gè)發(fā)出去的數(shù)據(jù)包都能得到應(yīng)答,在發(fā)包時(shí)只是加上一個(gè)檢驗(yàn)和數(shù)字($g
發(fā)送成$g#67
)。
遠(yuǎn)程協(xié)議中必要的數(shù)據(jù)包類型并不多(對應(yīng)于6個(gè)最重要的目標(biāo)向量方法),但為了支持硬件斷點(diǎn),跟蹤點(diǎn)(tracepoint)和共享庫, 又逐步加入了數(shù)十個(gè)可選的數(shù)據(jù)包格式。
對于目標(biāo)平臺(tái)本身來說,遠(yuǎn)程協(xié)議可以以多種形式來實(shí)現(xiàn)。GDB的手冊中有完整的協(xié)議文檔,只要用戶不違反GNU協(xié)議就可以實(shí)現(xiàn)自己的協(xié)議。事實(shí)上,許多設(shè)備制造商已經(jīng)在實(shí)驗(yàn)或?qū)嵺`中實(shí)現(xiàn)了一些使用GDB遠(yuǎn)程協(xié)議的代碼。比如,廣為人知的Cisco的IOS,就一直運(yùn)行在該公司的許多網(wǎng)絡(luò)設(shè)備上。
目標(biāo)平臺(tái)對于遠(yuǎn)程協(xié)議的實(shí)現(xiàn)通常稱為"調(diào)試樁(debugging stub)",或者簡稱為"樁(stub)",意指它不會(huì)獨(dú)立完成任何工作。GDB的源碼中包含了一些樁的示例代碼,大約只有1000行左右的C代碼。對于一個(gè)沒有操作系統(tǒng)的電路板, 樁必須能夠自己處理硬件異常, 特別是能夠捕捉trap指令。如果硬件鏈接是串行的,它還需要有串行驅(qū)動(dòng)的支持。實(shí)際的協(xié)議處理過程是比較簡單的,因?yàn)樗斜仨毜臄?shù)據(jù)包都是單個(gè)字符,可以使用一個(gè)簡單的switch語句來解碼。
另外一個(gè)實(shí)現(xiàn)遠(yuǎn)程協(xié)議的方法是構(gòu)建一個(gè)"sprite",作為GDB和調(diào)試硬件(包括JTAG設(shè)備,"wiggler"等)之間的接口。通常這些設(shè)備需要在與目標(biāo)板相連的計(jì)算機(jī)上運(yùn)行一個(gè)特殊的軟件庫,這個(gè)庫的API往往與GDB內(nèi)部結(jié)構(gòu)不相容。所以,與其讓GDB直接使用硬件控制庫,還不如更簡單地讓sprite作為一個(gè)獨(dú)立的程序運(yùn)行,它能夠理解遠(yuǎn)程協(xié)議并將數(shù)據(jù)包翻譯成設(shè)備軟件庫函數(shù)。
GDBserver
GDB源碼中已經(jīng)包含了一個(gè)完整和可靠的目標(biāo)端遠(yuǎn)程協(xié)議的實(shí)現(xiàn): GDBserver。GDBserver是一個(gè)在目標(biāo)操作系統(tǒng)上運(yùn)行的本地程序,它響應(yīng)通過遠(yuǎn)程協(xié)議接收到的數(shù)據(jù)包,控制目標(biāo)操作系統(tǒng)上的其它程序來提供本地調(diào)試支持。換句話說,它類似于本地調(diào)試的一個(gè)代理。
GDBserver不做本地GDB能力范圍之外的事,也就是說,如果目標(biāo)系統(tǒng)可以運(yùn)行GDBserver,那么理論上它也可以運(yùn)行GDB。但是,GDBserver只有GDB軟件規(guī)模的1/10,而且不需要管理符號(hào)表,所以用于嵌入式GNU/Linux之類的系統(tǒng)的調(diào)試是非常方便。
GDB和GDBServer共享相同的代碼,雖然大家都知道要將平臺(tái)依賴的控制代碼封裝起來,但是實(shí)際中GDB的這個(gè)遷移工作進(jìn)展緩慢,因?yàn)閷⒈镜谿DB中的依賴關(guān)系分離開來是比較困難的。
4.9. GDB界面
GDB本質(zhì)上是一個(gè)命令行調(diào)試器。人們始終沒有放棄嘗試將其發(fā)展為一個(gè)圖形窗口調(diào)試器,但是即使投入了大量的時(shí)間和努力,至今也沒有一個(gè)得到廣泛接受的方案。
命令行界面
命令行接口使用了標(biāo)準(zhǔn)的GNU軟件庫readline
來處理GDB和用戶之間的交互。readline
用于命令行的編輯和自動(dòng)補(bǔ)全,因而用戶可以像使用光標(biāo)一樣在命令行中移動(dòng)和修改。
GDB接收readline
返回的命令,然后在一個(gè)瀑布型的命令表結(jié)構(gòu)中查詢這條命令,命令中每個(gè)后續(xù)單詞會(huì)選擇一個(gè)額外的表格。比如,"set print elements 80
"使用了3個(gè)表格,第一個(gè)是包含了所有命令的表格,第二個(gè)是包含了set
選項(xiàng)的表格,第三個(gè)是print
選項(xiàng)的表格,其中elements
選項(xiàng)用于控制打印一個(gè)集合體(如字符串或數(shù)組)中輸出對象的個(gè)數(shù)。最后瀑布型表格將控制權(quán)交給一個(gè)實(shí)際的命令處理函數(shù),命令的參數(shù)將傳遞給這個(gè)函數(shù)來解析。一些命令, 比如run
, 處理參數(shù)的方式和傳統(tǒng)C語言的argc/argv
標(biāo)準(zhǔn)類似, 而其它一些命令, 比如print
, 則假定參數(shù)是一個(gè)程序表達(dá)式, 并將其完整傳遞給源碼解析器。
機(jī)器界面
一種GUI調(diào)試器方案是將GDB作為圖形用戶界面程序的后端,將鼠標(biāo)點(diǎn)擊翻譯成GDB命令,然后將打印的結(jié)果顯示在窗口中。這種方案已經(jīng)在一些軟件中實(shí)現(xiàn),比如KDbg和DDD(Data Display Debugger)。但這個(gè)方法仍然不理想,因?yàn)橛袝r(shí)候顯示結(jié)果時(shí)為了可讀性會(huì)省略掉一些細(xì)節(jié),前端提供上下文的能力也會(huì)影響到結(jié)果的顯示。
為解決這個(gè)問題,GDB提供了一個(gè)被稱為機(jī)器界面(Machine Interface,MI)的接口。本質(zhì)上MI仍然是一個(gè)命令行界面,但是命令和結(jié)果都增加了額外的語法,使得其意義更為顯然:每個(gè)參數(shù)都使用了引號(hào),復(fù)雜輸出則使用定界符來分組,使用參數(shù)名來分塊。此外, MI的命令還可以加上順序標(biāo)識(shí)符作為前綴, 并在結(jié)果中返回,保證了結(jié)果和命令的匹配。
為了比較兩種界面, 分別給出它們對于同一命令的使用情況。下面是正常的step命令及GDB的響應(yīng)
(gdb) stepbuggy_function (arg1=45, arg2=92) at ex.c:232232 result = positive_variable * arg1 + arg2;With the MI, the input and output are more verbose, but easier for other software to parse accurately:
下面是MI的輸入和輸出,雖然顯得有些冗余,但更加精確,便于第三方軟件進(jìn)行解析。
4321-exec-step4321^done,reason="end-stepping-range", frame={addr="0x00000000004004be", func="buggy_function", args=[{name="arg1",value="45"}, {name="arg2",value="92"}], file="ex.c", fullname="/home/sshebs/ex.c", line="232"}
Eclipse[ecl12]開發(fā)環(huán)境是最著名的使用MI的調(diào)試環(huán)境。
其它用戶界面
其它GDB前端軟件包括基于tcl/tk的GDBtk或Insight,基于文字界面的TUI(最初由Hewlett-Packard開發(fā))。GDBtk是一個(gè)傳統(tǒng)的多面板圖形用戶界面,使用tk軟件庫開發(fā),而TUI是一個(gè)在終端中使用的分屏文字界面。
4.10. 開發(fā)過程
維護(hù)者
作為一個(gè)GNU程序,GDB的開發(fā)遵循"大教堂(cathedral)"開發(fā)模型。GDB最初由Stallman編寫,隨后維護(hù)者幾易其人,每個(gè)人都是身兼設(shè)計(jì)師,補(bǔ)丁審查員,發(fā)布管理員數(shù)職,他們有權(quán)訪問僅向少數(shù)Cygnus雇員開放的源碼倉庫。
1999年,GDB被遷移到一個(gè)公共源碼倉庫,維護(hù)團(tuán)隊(duì)也擴(kuò)展到了幾十人,并且還有一些擁有簽入(commit)權(quán)限的個(gè)人從旁協(xié)助。這個(gè)模式顯著加速了GDB的開發(fā),從原來的每周10個(gè)簽入增加到了100個(gè)以上。
測試,測試
由于GDB高度依賴于特定平臺(tái),幾乎涵蓋全系列的計(jì)算設(shè)備,而且包含了數(shù)以百計(jì)的命令,選項(xiàng)以及使用風(fēng)格,即使是一個(gè)經(jīng)驗(yàn)豐富的GDB黑客也難以完全預(yù)料一個(gè)修改所產(chǎn)生的后果。
于是,測試套件變得舉足輕重。GDB的測試套件包含了眾多測試程序以及expect
腳本,使用一個(gè)基于tcl被稱為DejaGNU的測試框架。其基本模式是, 每個(gè)腳本驅(qū)動(dòng)GDB去調(diào)試一個(gè)測試程序, 然后向其發(fā)送命令, 并使用模式匹配來判斷結(jié)果正確與否。
這個(gè)測試套件還能進(jìn)行交叉調(diào)試,既支持真實(shí)硬件也支持模擬器,它還能對于特定平臺(tái)或配置進(jìn)行測試。
到2011年底,GDB測試套件包含了大約18000個(gè)測試用例,包括了基本功能測試,語言特性測試,體系特性測試,和MI測試。所有這些測試都是通用的,適用于所有配置。GDB需要志愿者來測試打補(bǔ)丁后的源碼,新的功能也需要新的測試。但是,因?yàn)闆]有人能在所有平臺(tái)上測試同一修改,要實(shí)現(xiàn)測試的完全通過是不現(xiàn)實(shí)的。對于本地調(diào)試來說, 主干GDB測試時(shí)失敗10-20次左右是可以授受的, 嵌入式系統(tǒng)則更容易出錯(cuò)。
4.11. 經(jīng)驗(yàn)教訓(xùn)
開放是王道
GDB是"大教堂"開發(fā)模型的典范,在該模式下,維護(hù)者嚴(yán)密控制源碼,而外部用戶則跟蹤其進(jìn)度。補(bǔ)丁提交數(shù)目較少,封閉的開發(fā)過程實(shí)際上并不鼓勵(lì)補(bǔ)丁。自從采用開放模式之后,補(bǔ)丁數(shù)量顯著增多,而軟件質(zhì)量則一如既往,甚至更好。
制訂計(jì)劃, 但計(jì)劃趕不上變化
開源軟件開發(fā)過程實(shí)際上會(huì)比較混亂,因?yàn)殚_發(fā)者之間是松散的,流動(dòng)性很大。
但是,制訂開發(fā)計(jì)劃并發(fā)布仍然很有意義。這有助于指導(dǎo)開發(fā)者完成相關(guān)任務(wù),而且能夠吸引潛在的贊助者,另外志愿者在嘗試做出貢獻(xiàn)時(shí)也能有一定的依據(jù)。
但是不要嘗試設(shè)置截止時(shí)間,即使是每個(gè)人都熱情地朝著一個(gè)方向努力,也不要指望大家都能全身心地投入并按時(shí)完成任務(wù)。
鑒于此,不要堅(jiān)持一個(gè)已經(jīng)過時(shí)的計(jì)劃。長期以來,GDB都有重構(gòu)為軟件庫libgdb
的計(jì)劃,這樣, 別的程序就可以通過使用libgdb
來實(shí)現(xiàn)一個(gè)擁有GUI的調(diào)試器。開發(fā)人員甚至嘗試過將構(gòu)建libgdb.a
作為整個(gè)構(gòu)建過程的一個(gè)中間步驟。雖然這個(gè)想法一直存在,但隨著Eclipse和MI的成功,libgdb
被擱置了起來,到2012年1月這個(gè)想法最終壽終正寢。
無比聰明該多好
看到曾經(jīng)提交的修改,我們也許會(huì)想:為什么一開始不這么做呢? 唉,只因?yàn)槲覀儾粔蚵斆鳌?/p>
我們本可以預(yù)料到GDB會(huì)如此流行,并且會(huì)移植到數(shù)以百計(jì)的平臺(tái)上,還支持本地和交叉調(diào)試。如果事先知道這些,說不定一開始就會(huì)使用gdbarch
對象,而不會(huì)數(shù)年來都在用陳舊的宏和全局變量,目標(biāo)向量也早該出現(xiàn)。
我們本可以預(yù)料到GDB將會(huì)被用到GUI中, 畢竟1986年Mac和X窗口系統(tǒng)已經(jīng)出現(xiàn)了2年。與其設(shè)計(jì)一個(gè)傳統(tǒng)的命令行界面,我們更應(yīng)該讓其支持異步事件處理。
然而,真正的教訓(xùn)不在于GDB開發(fā)者們有多蠢,而是我們不可能如此聰明地未卜先知。1986年, 窗口-鼠標(biāo)風(fēng)格的界面的未來還并不清晰, 我們預(yù)料不到它會(huì)像今天這樣流行,如果第一個(gè)版本的GDB就設(shè)計(jì)為在GUI下使用,我們就可以稱得上天才了,但這種好運(yùn)不是人人都能有的。相反,在一個(gè)有限的范圍內(nèi)讓GDB有所作為,我們已經(jīng)為今后的擴(kuò)展和重構(gòu)打下了用戶基礎(chǔ)。
學(xué)會(huì)接受缺陷
盡力完成過渡,但是時(shí)間總是太快,你只能接受缺陷。
在2003年的GCC峰會(huì)上, Zack Weinberg哀嘆GCC的"不完整過渡",新的底層結(jié)構(gòu)已經(jīng)引入,但是舊的卻尾大不掉。GDB有著同樣的問題,但是我們應(yīng)該看到積極的一面,因?yàn)楫吘挂恍┻^渡已經(jīng)完成,比如目標(biāo)向量,gdbarch
等等。雖然過渡需要多年來完成,調(diào)試卻要一直繼續(xù)。
謹(jǐn)防著迷于代碼
當(dāng)你遇到一個(gè)對你非常重要的項(xiàng)目,你會(huì)花費(fèi)大量時(shí)間在單個(gè)代碼上, 你會(huì)很容易沉迷其中,甚至為了迎合代碼而改變自己的想法。但是,很有可能你已經(jīng)誤入歧途,退一步說不定海闊天空。
這樣的事情要杜絕發(fā)生。
所有代碼都源自于一系列清醒的判斷:有些來自靈感,有些則不是。1991年節(jié)省空間的小伎倆對于2011年的數(shù)個(gè)G的內(nèi)存來說是毫無意義的。
GDB曾經(jīng)支持Gould超級(jí)計(jì)算機(jī)。當(dāng)他們在2000年關(guān)閉最后一臺(tái)機(jī)器時(shí),保留對這種機(jī)器的支持已是毫無意義。那些代碼只是GDB過往歷史中的一些小小篇章,然而現(xiàn)在大部分的發(fā)行版中仍然有些"懷舊"。
事實(shí)上,很多激進(jìn)的修改已經(jīng)擺上日程或已經(jīng)開展,包括對Python腳本的支持,對并行多核平臺(tái)的支持,重編碼為C++等。這些修改可能要花費(fèi)數(shù)年,但其動(dòng)機(jī)卻來自于今天(等到它們完成時(shí)說不定已經(jīng)過時(shí))。
聯(lián)系客服