摘要:
本文討論了GDB遠程調試技術在調試內核、嵌入式系統中的實現,簡要闡述GDB宿主機和GDB遠程串行協議,詳細分析GDB調試代理在內核層、應用層的各種實現方法。并提出了一種在不修改操作系統內核前提下調試應用程序的方法。這種方法可移植性強,而且消除了修改系統內核可能帶來的隱患,減少了因修改內核而帶來的工作量。在調試微內核操作系統服務的應用中表明,此方法非常有效。
關鍵詞:
遠程調試;stub;GDBserver; KGDB;嵌入式系統調試
Abstract:
This thesis discusses the realization of GDB remote debugging technology in kernel and embedded system. Firstly it describes the GDB host and GDB Remote Serial Protocol, then it analyses in detail the realization of GDBstub on kernel layer and application layer, at last the authors give a new method for debugging application while the OS kernel doesn’t be modified. This method has strong portability, and it eliminates the hidden trouble of OS kernel modification, also it reduces the workload subject to OS kernel modification. The application in debugging OS service of micro-kernel system shows that this method is reasonably efficient.
Key words:
Remote debugging; stub; GDBserver; KGDB; embedded system debugging
1、引言
調試是開發過程中必不可少的環節,然而內核、嵌入式系統的調試不同于傳統的調試系統。通常嵌入式系統不具備使用本地調試器的能力,由于:
系統自身的資源有限。內存小,輸入輸出設備不能用于調試。
傳統的調試系統需要文件系統,嵌入式系統通常無文件系統,內核調試時還不支持文件系統。
調試器的運行本身需要操作系統的支持,因此無法實現操作系統內核的調試。
最有效的解決方法是采用遠程調試技術。遠程調試是指調試器運行的環境(主機)和被調試的系統(目標機)在物理上是分離的,通過串口或者網絡進行連接的調試技術。
GNU免費提供的GDB就擁有強大的遠程調試功能,它能夠使開發人員以遠程調試的方式單步執行目標平臺上的程序代碼、設置斷點、查看內存,并同目標平臺交換信息。GDB遠程調試的實時、動態、方便、免費等優點使它逐漸成為嵌入式開發首選的調試方案。
遠程調試系統由三部分組成:主機上的本地調試器,目標機上的調試代理,遠程調試協議。如圖1。對應于GDB遠程調試系統的三部分:GDB,GDBstub, GDB遠程串行協議。下面就這三部分進行分析。

圖1. 遠程調試系統
2、RSP協議
GDB RSP(Remote Serial Protocol)定義了GDB宿主機與被調試目標機進行通信時數據包的格式。信息的格式是:$數據#校驗碼。多數的信息都使用ASCII碼,數據由一系列的ASCII碼組成,校驗碼是由兩個16進制數組成的單字節校驗碼。接受方接受數據并校驗,若正確則回應“+”,錯誤則回應“-”。通信的內容包括讀寫數據、控制程序運行、報告程序狀態等命令。RSP的基本命令從通信對話角度可以分為兩種:
1) 請求
?:讀當前系統狀態
g:讀所有寄存器
G:寫所有寄存器
m:讀內存
M:寫內存
c:繼續執行
s: 單步執行
k:終止進程
2) 答復
“”:告訴GDB上次請求命令不支持。
E:告訴GDB出錯
OK:上次請求正確
W:系統在exit_status狀態下退出。
X:系統在signal信號下終止。
S:系統在signal信號下停止。
O:告訴GDB控制臺輸出,這也是唯一向GDB發出的命令
3、GDB遠程調試功能
調試內核時通常還沒有文件系統,而且多數嵌入式由于自身資源的限制不具備文件系統,因此將與文件系統有關的源文件、目標文件及符號表都存放在主機上,由主機上的調試器處理。同樣,調試用的輸入輸出設備也是由主機提供。主機上的調試器接受用戶輸入的調試命令并進行預處理,對于有些命令(如breakpoint)的處理就在主機GDB上實現,不需要同目標機進行通信。當然,更多的指令需要在目標機上調試代理上實現的。主機將預處理完之后的命令根據RSP進行封裝,發送給目標機上的調試代理,調試代理接受命令后作相應的處理,并返回信息給主機上的調試器。
4、目標機上stub的實現
目標機上stub的基本功能是與主機GDB進行通信,實現讀寫內存、寄存器,stop,continue。主機GDB同目標機上stub進行通信的通用模型如圖2:

圖2. GDB同目標機上stub通信的通用模型
目標機與主機通過硬件連接,被調試部分插入stub,GDB與被調試部分通過RSP進行通信。根據stub所處層的不同來實現不同層的調試,包括內核層、應用層的調試。
4.1 內核層調試模型

圖3. 使用stub對內核進行調試
如圖3,將stub插入到內核里就可以實現內核的調試了。Linux內核調試機制KGDB就是使用這種模式。KGDB可以分為初始化模塊和控制模塊。
4.1.1初始化模塊
修改異常處理函數,使得在異常發生時都進入函數handle_exception(),這樣GDB就能夠捕獲這些異常。初始化之后使用breakpoint()函數將系統控制權直接交給GDB。KGDB對異常處理函數的修改基本上可以分為二種。
定義宏CHK_REMOTE_DEBUG
#define CHK_REMOTE_DEBUG(trapnr,signr,error_code,regs,after) { if (linux_debug_hook != (gdb_debug_hook *) NULL && !user_mode(regs)) { (*linux_debug_hook)(trapnr, signr, error_code, regs) ; after; } }
改變程序的流程,以int3的處理函數為例
#define DO_VM86_ERROR(trapnr, signr, str, name) asmlinkage void do_##name(struct pt_regs * regs, long error_code) { CHK_REMOTE_DEBUG(trapnr,signr,error_code,regs,goto skip_trap) do_trap(trapnr, signr, str, 1, regs, error_code, NULL); skip_trap: return; }
展開DO_VM86_ERROR (3,SIGTRAP,"int3",int3)
asmlinkage void do_int3(struct pt_regs *regs, long error_code)
{ if (linux_debug_hook != ( gdb_debug_hook *)NULL&&! user_mode(regs))
{ (*linux_debug_hook)(3, SIGTRAP, errorcode, regs);
goto skip_trap;
}
do_trap(3, SIGTRAP, "int3", 1, regs, error_code, NULL);
skip_trap:
return;
}
從以上代碼可見,進入內核調試狀態之后,異常處理函數就是handle_exception(),程序流程跳過了非調試狀態時的處理函數do_trap。
不改變程序的流程,以異常divide_error 的處理函數為例
#define DO_VM86_ERROR_INFO(trapnr, signr, str, name, sicode, siaddr) asmlinkage void do_##name(struct pt_regs * regs, long error_code) { …… do_trap(trapnr, signr, str, 1, regs, error_code, &info); }
展開DO_VM86_ERROR_INFO( 0, SIGFPE, "divide error", divide_error, FPE_INTDIV, regs->eip)
asmlinkage void do_divide_error (struct pt_regs *regs, long error_code)
{ if (linux_debug_hook != ( gdb_debug_hook *)NULL&&! user_mode(regs))
{ (*linux_debug_hook)(3, SIGTRAP, errorcode, regs);
}
do_trap(0, SIGTRAP, "divide erro", 1, regs, error_code, &info);
}
從以上代碼中看不出調試狀態跟非調試狀態的區別,然而我們看一下do_trap函數中可能會調用的函數die()。
void die(const char * str, struct pt_regs * regs, long err)
{
CHK_REMOTE_DEBUG(1,SIGTRAP,err,regs,)
do_exit(SIGSEGV);
}
由此可見,調試狀態下的異常處理函數還是進入了handle_exception函數。不過與上面一種異常不同之處在于:異常處理函數在調試與非調試狀態下的程序流程是相同的,handle_exception只提供獲取系統當時的狀態,繼續運行的結果還是do_exit。
雖然不是所有異常函數都是按上述兩種方法定義的,但本質上都可以歸劃為其一,顯然絕大多數處理函數的修改屬于第二種,因為第一種異常就是為調試準備的。因此在目標機具有調試用的輸出設備的情況下,完全可以不修改第二種異常處理函數,因為linux內核在非調試狀態下的異常處理函數已經輸出必要的狀態信息、出錯信息。
4.1.2控制模塊
在控制模塊完成與主機GDB的通信,具體流程如圖4,handle_exception函數首先判斷CPU是否處于VM86模式或用戶態,若是則返回,可見KGDB只調試內核態程序。然后接受GDB發來的信息,根據接受的信息作出相應的操作和回復。流程圖的虛線框內是所有GDBstub中handle_exception函數的通用流程。
4.2 應用程序調試模型
在嵌入式Linux開發領域里調試應用程序常用調試代理工具GDBserver,其工作原理并不是將stub編譯在被調試應用程序內,而是把被調試程序作為GDBserver的子進程,這樣GDBserver就可以利用內核提供的代碼跟蹤機制(ptrace)監控被調試進程的運行,從而來完成調試任務。此工作原理同GDB本地調試相似。其調試模型如圖5。GDBserver的工作流程是:GDBserver創建子進程->綁定跟蹤ptrace(ptrace_traceme,,)->從主機傳來的各種調試命令通過GDBserver轉化為各種操作需求的ptrace。顯然,如果要用GDBserver來進行遠程調試的話,就需要內核操作系統的支持,包括子進程、代碼跟蹤機制,這樣對于其他嵌入式系統內核工作量會比較大。而且ptrace也有其局限性,比如只能跟蹤它的子進程,在調試進程和被調試進程之間傳送一個長字的數據。使用通用的調試模式工作量會更小。如圖6,將stub編譯在應用程序中,并在應用程序入口處就插入斷點,程序開始就上控制權交給GDB,之后的流程跟內核層調試類似。

圖4. GDBKGDB中handle_exception函數流程

圖5. 使用GDBserver對應用程序進行調試

圖6. 使用stub對應用程序進行調試
5. 不修改內核前提下調試應用程序
GDB實現設置斷點的方式是使用內存的讀寫,即將原指令用一個trap指令代替,使得程序執行到該指令時產生單步調試中斷,然后就進入異常處理函數,針對調試器的各種操作處理函數需要作出相應的操作。不同的系統提供不同的調試異常指令,如int3,trap2等,顯然對于使用這些硬件平臺提供的斷點指令為了實現GDBstub調試功能需要改寫這些指令異常處理函數。因此一般的調試系統器或調試代理都需要涉及單步調試指令的處理函數,需要系統內核的支持。上面提到的KGDB修改了異常處理函數,GDBserver需要系統內核提供ptrace函數。這種方法存在一些不足之處:修改內核工作量大,移植性差。針對這些情況我們可以采用另一種斷點實現方案:在stub中定義一個設置斷點函數。
斷點函數模擬調試異常指令,實現保護現場、調用異常處理函數、恢復現場并將控制權交給被調試程序。斷點函數的基本流程如下。
#define BREAKPOINT __asm__ __volatile__(" bl ent_exception\n)
void debug_trap()
{ __asm__ __volatile__(
" ent_exception: \n"
保存現場
" bl handle_exception \n"
" out_exception: \n"
恢復現場
);
}
handle_exception()函數流程類似圖4中的虛線框部分。其中有一點,也是這種方法實現的關鍵部分是:斷點指令的替換。斷點設置時從GDB傳過來的硬件平臺提供的斷點異常指令的二進制碼,必須將此二進制碼替換成在stub中新定義的BREAKPOINT二進制碼,這樣才能進入調試異常處理函數。因此在handle_exception()函數中,如果收到的請求是“M”,則需要作些處理,流稱如圖7:

圖7. 替換指令
這種方法理論上在內核調試和應用程序調試中都可以使用,但在應用程序的調試中其優點更明顯。這種方法在寫stub時候不涉及內核,在調試應用程序時不需要切換到內核模式下,直接在用戶模式中就可以完成。這種方法也存在些不足之處。為了實現現場保護,要求用戶了解系統內的寄存器。隨著stub本身復雜度的增加,它的正確性需要更多的檢驗。
6、結束語
加stub的遠程調試方法方便而有效,而且可以降低項目成本,在實際工作中得到廣泛的研究和應用。本文提到在不修改內核前提下調試應用程序的方法已成功應用于我們自己開發的微內核結構的操作系統里,為該系統的開發應用提供良好的調試手段。當然加stub的遠程調試方法也存在一些不足。顯然stub的應用是在串口通信的基礎上,因此串口處理函數以及stub自身處理函數的正確性是確保stub安全調試的前提。
參考文獻
[1].李紅衛李翠萍,kgdb調試Linux內核肋剖析與改進,微型機與應用,2004年第10期
[2].郭勝超,GDB遠程調試及其在嵌入式Linux系統中的應用,計算機工程與應用,2004年第26卷第10期.
[3].彭進展,GRDBS:一種針對嵌入式系統的通用遠程調試系統,計算機工程,2003年2月第29卷第2期.
[4] .Gatliff, Bill, Embedding with GNU: the gdb Remote Serial Protocol, Embedded Systems Programming, September 1999, p. 109.
[5].Gilmore J, Shebs S, GDB Internals: A Guild to the Internals of the GNU Debugger, Free Software Foundation Inc.,1999.
作者單位:浙江大學計算機系
地址:浙江大學玉泉校區4舍230 310027
Email:liulin@zju.edu.cn