摘 要: 在Linux內(nèi)核中加入kgdb,通過開發(fā)機上的gdb對目標機上的內(nèi)核進行源代碼級的調(diào)試技術(shù)。
關(guān)鍵詞: 遠程調(diào)試 kgdb 串口通信協(xié)議 異常處理
操作系統(tǒng)的內(nèi)核調(diào)試器除完成一般的調(diào)試功能外,還必須工作在內(nèi)核中。因此,內(nèi)核調(diào)試器與用戶級的調(diào)試器有很大的區(qū)別。在Linux系統(tǒng)中,對內(nèi)核的調(diào)試有很多種方法,例如:可在內(nèi)核中插入printk( )函數(shù),將調(diào)試信息輸出,然后針對輸出的信息進行分析;可以使用/proc文件系統(tǒng)對內(nèi)核進行分析;可以利用strace命令對系統(tǒng)調(diào)用進行分析。但這些方法都不能直接對內(nèi)核源代碼級進行調(diào)試。因此,本文介紹一種在內(nèi)核源代碼中插入kgdb(Kernel GNU Debuger,GNU內(nèi)核調(diào)試器),通過開發(fā)機上的gdb調(diào)試器對運行于目標機上的內(nèi)核進行源代碼級調(diào)試的技術(shù)。
1 kgdb調(diào)試內(nèi)核的實現(xiàn)原理
1.1 通信協(xié)議
kgdb提供了一種使用gdb調(diào)試Linux內(nèi)核的機制。使用kgdb調(diào)試內(nèi)核需要二臺機器,一臺作為開發(fā)機,另一臺作為目標機,通過串口將它們連接起來。在將要調(diào)試的內(nèi)核中插入kgdb,重新編譯內(nèi)核,使其運行在目標機上。而gdb在開發(fā)機上運行,gdb通過串口與要調(diào)試的內(nèi)核進行通信,對目標機上的內(nèi)核進行控制,從而實現(xiàn)遠程調(diào)試內(nèi)核的目的。
要使gdb有效地控制調(diào)試目標機上的內(nèi)核,必須與目標機上的kgdb約定相互的通信協(xié)議。而gdb本身帶有一個遠程串行通信協(xié)議,所以在kgdb中包含相同的協(xié)議即可實現(xiàn)開發(fā)機與目標機之間的通信。
開發(fā)機上的gdb可以向目標機發(fā)送一些命令數(shù)據(jù)包,如果kgdb能夠?qū)崿F(xiàn)g、G、m、M、c和s等主要命令,則在使用gdb對目標機上的內(nèi)核進行調(diào)試時就像在本機上調(diào)試程序一樣。這六個命令的功能描述如下:
g:查看CPU寄存器的值;
G:設(shè)置CPU寄存器的值;
maddr,count:從addr位置開始讀count個字節(jié)的數(shù)據(jù);
Maddr,count:從addr位置開始寫count個字節(jié)的數(shù)據(jù);
c/caddr:在當前位置上繼續(xù)執(zhí)行程序或從地址addr處開始執(zhí)行;
s/saddr:單步執(zhí)行當前的指令,或者執(zhí)行到指定的addr位置。
1.2 kgdb遠程調(diào)試技術(shù)分析
遠程調(diào)試常采用插樁(stub)的方法實現(xiàn)。在目標操作系統(tǒng)內(nèi)和開發(fā)機上的調(diào)試器內(nèi)分別加入某些功能模塊(如通信模塊和異常處理模塊等),二者互通信息來完成調(diào)試工作,這些功能模塊統(tǒng)稱為插樁。gdb本身帶有這些功能模塊,所以在調(diào)試內(nèi)核時,只需要在內(nèi)核中加入stub插樁模塊即可實現(xiàn)用gdb遠程調(diào)試內(nèi)核的目的。gdb遠程調(diào)試器的結(jié)構(gòu)如圖1所示。
通過對kgdb源程序進行分析可知,kgdb主要有二大模塊:一個是初始化模塊,完成初始化過程,接管所有異常、設(shè)置串口通信等低層實現(xiàn);另一個是控制模塊,實現(xiàn)通信,對接收到的信息包進行解析并執(zhí)行,對應(yīng)答包進行打包發(fā)送。
1.2.1 kgdb初始化模塊的實現(xiàn)
在Linux啟動時要調(diào)用start_kernel( )函數(shù)。該函數(shù)調(diào)用一系列初始化函數(shù),完成系統(tǒng)初始化工作。kgdb通過對該函數(shù)進行修改,使其在內(nèi)核初始化工作完成后將控制權(quán)交給插樁程序kgdb,由插樁程序完成調(diào)試內(nèi)核的任務(wù)。
(1)設(shè)置異常中斷處理程序入口
kgdb插樁模塊要對目標機上被調(diào)試的程序進行控制,必須對目標機上所發(fā)生的所有異常進行統(tǒng)一管理。在上述Linux啟動過程中,若要執(zhí)行start_kernel( )函數(shù),則應(yīng)在該函數(shù)中調(diào)用trap_init( )函數(shù)設(shè)置各種入口地址,如異常事件處理程序入口、系統(tǒng)調(diào)用入口等。其中trap0~trap19為各種異常事件錯誤入口,如被0除、溢出、存儲器越界等。為了使gdb能夠捕獲這些發(fā)生在目標機上的異常,kgdb要對各個需要捕獲的異常處理函數(shù)進行修改。當發(fā)生異常時使異常處理事件進入異常處理函數(shù)handle_excepton( ),han-
dle_excepton( )就會將目標機上發(fā)生的異常以信號“Sxx”的方式通知主機上的gdb,主機上的gdb即可知道目標機上發(fā)生的異常。
(2)串口初始化
將所有異常入口地址進行修改后,應(yīng)該對串口進行初始化,并設(shè)置linux_debug_traps指針變量的值,使其指向異常處理程序入口地址。此工作結(jié)束后,kgdb就接管所有發(fā)生在目標機上的異常事件。
1.2.2 kgdb控制模塊的實現(xiàn)
kgdb控制模塊主要完成對協(xié)議的解析和執(zhí)行,這些功能都在handle_exception( )函數(shù)中實現(xiàn)。通過該函數(shù)可實現(xiàn)在內(nèi)核中設(shè)置斷點、單步執(zhí)行代碼和監(jiān)視變量的值。
handle_exception( )函數(shù)首先判斷CPU是否處于虛擬86模式或用戶態(tài),若是則返回。由此可見,kgdb只對內(nèi)核態(tài)程序進行調(diào)試。其次判斷是否發(fā)生讀/寫內(nèi)存異常,若是則為讀/寫內(nèi)存操作設(shè)置一個有效內(nèi)存地址,返回重試,否則向主機發(fā)送信息“Sxx”,其中xx是發(fā)生異常的信號量值,即通知開發(fā)機上的gdb在目標機上發(fā)生了什么樣的異常。接下來handle_exception( )接收來自主機的調(diào)試命令,對來自主機的命令進行解析并完成命令的執(zhí)行,在kgdb中主要完成的調(diào)試命令有m、M、g、G、s、c。當這些命令執(zhí)行完后還需要向開發(fā)機發(fā)送應(yīng)答信息,報告完成命令的情況。此函數(shù)的流程如圖2所示。
(1)g讀寄存器命令的實現(xiàn)。當系統(tǒng)發(fā)生異常時,系統(tǒng)首先將發(fā)生異常的進程的CPU狀態(tài)保存在核心堆棧中。用g命令讀寄存器時,實際上就是從核心棧中去取,但gdb所規(guī)定的寄存器的排列與Linux內(nèi)核中對寄存器的排列順序不一致,需要進行寄存器的轉(zhuǎn)換。通過函數(shù)regs_to_gdb_regs(gdb_regs,®s)實現(xiàn)轉(zhuǎn)換,轉(zhuǎn)換結(jié)束后將寄存器的值打包送回主機。
(2)G寫寄存器命令的實現(xiàn)。對接收到的數(shù)據(jù)包進行解包,把結(jié)果放入gdb_regs結(jié)構(gòu)中,再通過gdb_regs_to_regs(gdb_regs,®s)函數(shù)將接收到的寄存器的值進行轉(zhuǎn)換,并放入核心棧中,通過對核心棧的修改得到修改寄存器的目標。
(3)讀/寫內(nèi)存命令的實現(xiàn)。對內(nèi)存的讀寫有可能再次發(fā)生讀寫內(nèi)存異常。例如對一個非法的地址進行讀寫的kgdb解決方法如下:
kgdb通過函數(shù)int get_char(char?鄢char)讀內(nèi)存,通過函數(shù)void set_char(char?鄢addr,int val)寫內(nèi)存。kgdb在讀/寫不確定的內(nèi)存時,通過二個變量來判斷是否讀/寫內(nèi)存發(fā)生異常。一個變量是kgdb_memerr_expected,標志是否讀寫不確定的內(nèi)存;另一個變量是kgdb_memerr,標志在讀寫內(nèi)存時是否發(fā)生了異常。這樣,對不確定內(nèi)存的讀/寫過程可分三步。
第一步:對這二個變量賦值。將kgdb_memerr_expected賦值為1,表示對不確定內(nèi)存進行讀/寫操作。對kgdb_me-merr賦值為0,表示在讀/寫內(nèi)存時沒有發(fā)生異常。
第二步:對內(nèi)存進行讀寫操作。若讀寫地址是非法地址,則進入異常處理程序,轉(zhuǎn)第三步。若正確,則返回,并清變量kgdb_memerr_expected為0,表示讀寫內(nèi)存結(jié)束。
第三步:進入異常處理程序中。首先要判斷kgdb_me-merr_expected是否為1。若是1,則說明對內(nèi)存讀/寫時發(fā)生了異常。這時先清kgdb_ memerr_expected為0,置kgdb_memerr為1,標志在讀/寫內(nèi)存時發(fā)生了異常,并為讀/寫內(nèi)存設(shè)置一個有效地址。這個有效地址是一個可讀/寫的內(nèi)存地址,可以對其進行操作,操作的結(jié)果是無效的。
(4)s單步命令的實現(xiàn)?;趚86 CPU的體系結(jié)構(gòu)支持單步執(zhí)行,但它只是指令級的單步,而不是源代碼級的單步。這里的s命令是實現(xiàn)指令級的單步,它的處理方法是:將產(chǎn)生異常的進程標志寄存器的陷阱標志位TF置1,異常中斷處理程序結(jié)束返回到異常點繼續(xù)執(zhí)行。因TF標志位為1,所以程序執(zhí)行一條指令后又進入異常處理程序。主機處的gdb利用此命令實現(xiàn)源代碼級的單步跟蹤。
(5)c繼續(xù)運行命令的實現(xiàn)。異常處理程序結(jié)束,恢復(fù)異常處的CPU狀態(tài),使程序繼續(xù)執(zhí)行。
2 kgdb插樁技術(shù)的不足與改進
現(xiàn)有的kgdb不具有使目標程序進行調(diào)試模式和正常運行模式的切換功能。目前的處理方法是:當程序調(diào)試好后,將kgdb卸下,重新編譯程序。當發(fā)生錯誤時再將kgdb插入目標程序中,編譯后再在目標機上調(diào)試。由此可見操作十分不便。本文提出一種新的設(shè)計思路,使目標機上運行的程序可隨時進行調(diào)試模式與正常運行模式的切換,這樣更便于調(diào)試。下面討論其實現(xiàn)方法。
(1)在目標機方:通過對kgdb源代碼分析可知,在進入各異常函數(shù)中,首先判斷指向gdb_debug_hook類型的函數(shù)指針變量linux_debug_hook是否為空。若不為空則執(zhí)行異常處理函數(shù)handle_exception( ),否則,執(zhí)行原異常處理程序。這樣通過修改這個變量的值,即可實現(xiàn)調(diào)試模式與正常運行模式的轉(zhuǎn)換。當設(shè)置linux_debug_hook為(gdb_debug_hook?鄢)NULL時,目標機程序運行在正常運行狀態(tài);當設(shè)置linux_debug_hook為指向異常函數(shù)handle_exception( )時,則目標機程序運行在調(diào)試狀態(tài)。
(2)在主機方:調(diào)試狀態(tài)到正常運行狀態(tài)的轉(zhuǎn)換:在kgdb中設(shè)置一個變量int stop_gdb。當主機對該變量進行賦值時,目標機上的kgdb獲取信息后,使指向函數(shù)的指針變量linux_debug_hook指向(gdb_debug_hook?鄢)NULL,然后,kgdb構(gòu)造c命令并執(zhí)行,使目標機上的程序脫離調(diào)試運行模式進入正常運行模式。
主機端發(fā)送命令:set stop_gdb=1
這條命令經(jīng)過主機端的gdb解析,實際上是執(zhí)行協(xié)議命令M,即寫內(nèi)存命令。這樣就需要在kgdb中對M命令進行修改。
目標端kgdb 執(zhí)行M命令的算法描述如下:
(1)向主機返回應(yīng)答信息“OK”;
(2)如果所寫地址與變量stop_gdb的地址相同,則轉(zhuǎn)(4);
(3)執(zhí)行原M命令,轉(zhuǎn)(8);
(4)設(shè)置變量initialized為0;
(5)設(shè)置指針變量linux_debug_hook為(gdb_debug_hook*)NULL;
(6)構(gòu)造命令c;
(7)執(zhí)行命令c,目標機上被調(diào)試的程序轉(zhuǎn)為正常運行模式;
(8)結(jié)束。
正常運行狀態(tài)到調(diào)試狀態(tài)的轉(zhuǎn)換:目標程序從正常運行狀態(tài)切換到調(diào)試狀態(tài),就不像從調(diào)試狀態(tài)到正常運行狀態(tài)切換那樣對一個變量進行設(shè)置而觸發(fā)狀態(tài)的切換。因為目標機已不在主機端gdb的控制下運行,主機端的gdb與目標機端的kgdb已失去了聯(lián)系。所以要激活目標機進入調(diào)試狀態(tài),只能使用中斷技術(shù),利用串口中斷來激活對目標機由正常運行狀態(tài)切換到調(diào)試狀態(tài)。
在gdb與目標機建立連接時,gdb首先要向目標機發(fā)送一個數(shù)據(jù)包$Hc-1#09,它會觸發(fā)目標機上串口中斷處理程序的執(zhí)行,并判斷接收的數(shù)據(jù)包是否為“Hc-1”。若是則設(shè)置linux_debug_hook指向異常中斷處理程序handle_exception( ),并執(zhí)行breakpoint( )函數(shù),使目標機進入調(diào)試狀態(tài)。
通過上述方法即可實現(xiàn)目標機上程序運行狀態(tài)的切換,使程序調(diào)試更加靈活。
3 結(jié)束語
通過對kgdb調(diào)試技術(shù)的分析與改進,增加了調(diào)試的方法和思路。此調(diào)試技術(shù)可以應(yīng)用于嵌入式系統(tǒng)的設(shè)計和開發(fā)中,為嵌入式開發(fā)工具增加了一種廉價而強有力的調(diào)試工具。
參考文獻
1 李善平,劉文峰.Linux內(nèi)核2.4版源代碼分析大全.北京: 機械工業(yè)出版社,2002
2 李紅衛(wèi),李翠萍.嵌入式軟件的調(diào)試技術(shù).計算機時代,2002;(8)