上篇回顧:一文Linux內(nèi)核調(diào)試方法(一)
KGDB
kgdb提供了一種使用 gdb調(diào)試 Linux 內(nèi)核的機制。使用KGDB可以象調(diào)試普通的應(yīng)用程序那樣,在內(nèi)核中進行設(shè)置斷點、檢查變量值、單步跟蹤程序運行等操作。使用KGDB調(diào)試時需要兩臺機器,一臺作為開發(fā)機(Development Machine),另一臺作為目標(biāo)機(Target Machine),兩臺機器之間通過串口或者以太網(wǎng)口相連。串口連接線是一根RS-232接口的電纜,在其內(nèi)部兩端的第2腳(TXD)與第3腳(RXD)交叉相連,第7腳(接地腳)直接相連。調(diào)試過程中,被調(diào)試的內(nèi)核運行在目標(biāo)機上,gdb調(diào)試器運行在開發(fā)機上。 目前,kgdb發(fā)布支持i386、x86_64、32-bit PPC、SPARC等幾種體系結(jié)構(gòu)的調(diào)試器。
嵌入式進階教程分門別類整理好了,看的時候十分方便,由于內(nèi)容較多,這里就截取一部分圖吧。
需要的朋友私信【內(nèi)核】即可領(lǐng)取。
內(nèi)核學(xué)習(xí)地址:Linux內(nèi)核源碼/內(nèi)存調(diào)優(yōu)/文件系統(tǒng)/進程管理/設(shè)備驅(qū)動/網(wǎng)絡(luò)協(xié)議棧-學(xué)習(xí)視頻教程-騰訊課堂
kgdb的調(diào)試原理
安裝kgdb調(diào)試環(huán)境需要為Linux內(nèi)核應(yīng)用kgdb補丁,補丁實現(xiàn)的gdb遠程調(diào)試所需要的功能包括命令處理、陷阱處理及串口通訊3個主要的部分。kgdb補丁的主要作用是在Linux內(nèi)核中添加了一個調(diào)試Stub。調(diào)試Stub是Linux內(nèi)核中的一小段代碼,提供了運行g(shù)db的開發(fā)機和所調(diào)試內(nèi)核之間的一個媒介。gdb和調(diào)試stub之間通過gdb串行協(xié)議進行通訊。gdb串行協(xié)議是一種基于消息的ASCII碼協(xié)議,包含了各種調(diào)試命令。當(dāng)設(shè)置斷點時,kgdb負(fù)責(zé)在設(shè)置斷點的指令前增加一條trap指令,當(dāng)執(zhí)行到斷點時控制權(quán)就轉(zhuǎn)移到調(diào)試stub中去。此時,調(diào)試stub的任務(wù)就是使用遠程串行通信協(xié)議將當(dāng)前環(huán)境傳送給gdb,然后從gdb處接受命令。gdb命令告訴stub下一步該做什么,當(dāng)stub收到繼續(xù)執(zhí)行的命令時,將恢復(fù)程序的運行環(huán)境,把對CPU的控制權(quán)重新交還給內(nèi)核
Kgdb的安裝與設(shè)置
下面我們將以Linux 2.6.7內(nèi)核為例詳細(xì)介紹kgdb調(diào)試環(huán)境的建立過程。
軟硬件準(zhǔn)備
以下軟硬件配置取自筆者進行試驗的系統(tǒng)配置情況: kgdb補丁的版本遵循如下命名模式:Linux-A-kgdb-B,其中A表示Linux的內(nèi)核版本號,B為kgdb的版本號。以試驗使用的kgdb補丁為例,linux內(nèi)核的版本為linux-2.6.7,補丁版本為kgdb-2.2。 物理連接好串口線后,使用以下命令來測試兩臺機器之間串口連接情況,stty命令可以對串口參數(shù)進行設(shè)置: 在development機上執(zhí)行:
stty ispeed 115200 ospeed 115200 -F /dev/ttyS0在target機上執(zhí)行:stty ispeed 115200 ospeed 115200 -F /dev/ttyS0在developement機上執(zhí)行:echo hello > /dev/ttyS0
在target機上執(zhí)行:
cat /dev/ttyS0
如果串口連接沒問題的話在將在target機的屏幕上顯示”hello”。
安裝與配置
下面我們需要應(yīng)用kgdb補丁到Linux內(nèi)核,設(shè)置內(nèi)核選項并編譯內(nèi)核。這方面的資料相對較少,筆者這里給出詳細(xì)的介紹。下面的工作在開發(fā)機(developement)上進行,以上面介紹的試驗環(huán)境為例,某些具體步驟在實際的環(huán)境中可能要做適當(dāng)?shù)母膭樱?/p>
內(nèi)核的配置與編譯
[root@lisl tmp]# tar -jxvf linux-2.6.7.tar.bz2[root@lisl tmp]#tar -jxvf linux-2.6.7-kgdb-2.2.tar.tar[root@lisl tmp]#cd inux-2.6.7
請參照目錄補丁包中文件README給出的說明,執(zhí)行對應(yīng)體系結(jié)構(gòu)的補丁程序。由于試驗在i386體系結(jié)構(gòu)上完成,所以只需要安裝一下補丁:core-lite.patch、i386-lite.patch、8250.patch、eth.patch、core.patch、i386.patch。應(yīng)用補丁文件時,請遵循kgdb軟件包內(nèi)series文件所指定的順序,否則可能會帶來預(yù)想不到的問題。eth.patch文件是選擇以太網(wǎng)口作為調(diào)試的連接端口時需要運用的補丁。 應(yīng)用補丁的命令如下所示:
[root@lisl tmp]#patch -p1 <../linux-2.6.7-kgdb-2.2/core-lite.patch
如果內(nèi)核正確,那么應(yīng)用補丁時應(yīng)該不會出現(xiàn)任何問題(不會產(chǎn)生*.rej文件)。為Linux內(nèi)核添加了補丁之后,需要進行內(nèi)核的配置。內(nèi)核的配置可以按照你的習(xí)慣選擇配置Linux內(nèi)核的任意一種方式。
[root@lisl tmp]#make menuconfig
在內(nèi)核配置菜單的Kernel hacking選項中選擇kgdb調(diào)試項,例如:
[*] KGDB: kernel debugging with remote gdb Method for KGDB communication (KGDB: On generic serial port (8250)) —> [*] KGDB: Thread analysis [*] KGDB: Console messages through gdb[root@lisl tmp]#make
編譯內(nèi)核之前請注意Linux目錄下Makefile中的優(yōu)化選項,默認(rèn)的Linux內(nèi)核的編譯都以-O2的優(yōu)化級別進行。在這個優(yōu)化級別之下,編譯器要對內(nèi)核中的某些代碼的執(zhí)行順序進行改動,所以在調(diào)試時會出現(xiàn)程序運行與代碼順序不一致的情況??梢园袽akefile中的-O2選項改為-O,但不可去掉-O,否則編譯會出問題。為了使編譯后的內(nèi)核帶有調(diào)試信息,注意在編譯內(nèi)核的時候需要加上-g選項。
不過,當(dāng)選擇”Kernel debugging->Compile the kernel with debug info”選項后配置系統(tǒng)將自動打開調(diào)試選項。另外,選擇”kernel debugging with remote gdb”后,配置系統(tǒng)將自動打開”Compile the kernel with debug info”選項。
內(nèi)核編譯完成后,使用scp命令進行將相關(guān)文件拷貝到target機上(當(dāng)然也可以使用其它的網(wǎng)絡(luò)工具,如rcp)。
[root@lisl tmp]#scp arch/i386/boot/bzImage [email protected]:/boot/vmlinuz-2.6.7-kgdb[root@lisl tmp]#scp System.map [email protected]:/boot/System.map-2.6.7-kgdb
如果系統(tǒng)啟動使所需要的某些設(shè)備驅(qū)動沒有編譯進內(nèi)核的情況下,那么還需要執(zhí)行如下操作:
[root@lisl tmp]#mkinitrd /boot/initrd-2.6.7-kgdb 2.6.7[root@lisl tmp]#scp initrd-2.6.7-kgdb [email protected]:/boot/ initrd-2.6.7-kgdb
kgdb的啟動
在將編譯出的內(nèi)核拷貝到target機器之后,需要配置系統(tǒng)引導(dǎo)程序,加入內(nèi)核的啟動選項。以下是kgdb內(nèi)核引導(dǎo)參數(shù)的說明: 如表中所述,在kgdb 2.0版本之后內(nèi)核的引導(dǎo)參數(shù)已經(jīng)與以前的版本有所不同。使用grub引導(dǎo)程序時,直接將kgdb參數(shù)作為內(nèi)核vmlinuz的引導(dǎo)參數(shù)。下面給出引導(dǎo)器的配置示例。
title 2.6.7 kgdbroot (hd0,0)kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait kgdb8250=1,115200
在使用lilo作為引導(dǎo)程序時,需要把kgdb參放在由append修飾的語句中。下面給出使用lilo作為引導(dǎo)器時的配置示例。
image=/boot/vmlinuz-2.6.7-kgdblabel=kgdb read-only root=/dev/hda3append=”gdb gdbttyS=1 gdbbaud=115200″
保存好以上配置后重新啟動計算機,選擇啟動帶調(diào)試信息的內(nèi)核,內(nèi)核將在短暫的運行后在創(chuàng)建init內(nèi)核線程之前停下來,打印出以下信息,并等待開發(fā)機的連接。
Waiting for connection from remote gdb…
在開發(fā)機上執(zhí)行:
gdbfile vmlinuxset remotebaud 115200target remote /dev/ttyS0
其中vmlinux是指向源代碼目錄下編譯出來的Linux內(nèi)核文件的鏈接,它是沒有經(jīng)過壓縮的內(nèi)核文件,gdb程序從該文件中得到各種符號地址信息。
這樣,就與目標(biāo)機上的kgdb調(diào)試接口建立了聯(lián)系。一旦建立聯(lián)接之后,對Linux內(nèi)的調(diào)試工作與對普通的運用程序的調(diào)試就沒有什么區(qū)別了。任何時候都可以通過鍵入ctrl+c打斷目標(biāo)機的執(zhí)行,進行具體的調(diào)試工作。 在kgdb 2.0之前的版本中,編譯內(nèi)核后在arch/i386/kernel目錄下還會生成可執(zhí)行文件gdbstart。將該文件拷貝到target機器的/boot目錄下,此時無需更改內(nèi)核的啟動配置文件,直接使用命令:
[root@lisl boot]#gdbstart -s 115200 -t /dev/ttyS0
可以在KGDB內(nèi)核引導(dǎo)啟動完成后建立開發(fā)機與目標(biāo)機之間的調(diào)試聯(lián)系。 通過網(wǎng)絡(luò)接口進行調(diào)試
kgdb也支持使用以太網(wǎng)接口作為調(diào)試器的連接端口。在對Linux內(nèi)核應(yīng)用補丁包時,需應(yīng)用eth.patch補丁文件。配置內(nèi)核時在Kernel hacking中選擇kgdb調(diào)試項,配置kgdb調(diào)試端口為以太網(wǎng)接口,例如:
[*]KGDB: kernel debugging with remote gdbMethod for KGDB communication (KGDB: On ethernet) —> ( ) KGDB: On generic serial port (8250)(X) KGDB: On ethernet
另外使用eth0網(wǎng)口作為調(diào)試端口時,grub.list的配置如下:
title 2.6.7 kgdbroot (hd0,0)kernel /boot/vmlinuz-2.6.7-kgdb ro root=/dev/hda1 kgdbwait [email protected]/,@192.168. 6.13/
其他的過程與使用串口作為連接端口時的設(shè)置過程相同。 注意:盡管可以使用以太網(wǎng)口作為kgdb的調(diào)試端口,使用串口作為連接端口更加簡單易行,kgdb項目組推薦使用串口作為調(diào)試端口。 模塊的調(diào)試方法
內(nèi)核可加載模塊的調(diào)試具有其特殊性。由于內(nèi)核模塊中各段的地址是在模塊加載進內(nèi)核的時候才最終確定的,所以develop機的gdb無法得到各種符號地址信息。所以,使用kgdb調(diào)試模塊所需要解決的一個問題是,需要通過某種方法獲得可加載模塊的最終加載地址信息,并把這些信息加入到gdb環(huán)境中。 I、在Linux 2.4內(nèi)核中的內(nèi)核模塊調(diào)試方法 在Linux2.4.x內(nèi)核中,可以使用insmod -m命令輸出模塊的加載信息,例如:
[root@lisl tmp]# insmod -m hello.ko >modaddr
查看模塊加載信息文件modaddr如下:
.this 00000060 c88d8000 2**2.text 00000035 c88d8060 2**2.rodata 00000069 c88d80a0 2**5…….data 00000000 c88d833c 2**2.bss 00000000 c88d833c 2**2……
在這些信息中,我們關(guān)心的只有4個段的地址:.text、.rodata、.data、.bss。在development機上將以上地址信息加入到gdb中,這樣就可以進行模塊功能的測試了。
(gdb) Add-symbol-file hello.o 0xc88d8060 -s .data 0xc88d80a0 -s .rodata 0xc88d80a0 -s .bss 0x c88d833c
這種方法也存在一定的不足,它不能調(diào)試模塊初始化的代碼,因為此時模塊初始化代碼已經(jīng)執(zhí)行過了。而如果不執(zhí)行模塊的加載又無法獲得模塊插入地址,更不可能在模塊初始化之前設(shè)置斷點了。對于這種調(diào)試要求可以采用以下替代方法。 在target機上用上述方法得到模塊加載的地址信息,然后再用rmmod卸載模塊。在development機上將得到的模塊地址信息導(dǎo)入到gdb環(huán)境中,在內(nèi)核代碼的調(diào)用初始化代碼之前設(shè)置斷點。這樣,在target機上再次插入模塊時,代碼將在執(zhí)行模塊初始化之前停下來,這樣就可以使用gdb命令調(diào)試模塊初始化代碼了。 另外一種調(diào)試模塊初始化函數(shù)的方法是:當(dāng)插入內(nèi)核模塊時,內(nèi)核模塊機制將調(diào)用函數(shù)sys_init_module(kernel/modle.c)執(zhí)行對內(nèi)核模塊的初始化,該函數(shù)將調(diào)用所插入模塊的初始化函數(shù)。程序代碼片斷如下:
…… ……if (mod->init != NULL)ret = mod->init();…… ……
在該語句上設(shè)置斷點,也能在執(zhí)行模塊初始化之前停下來。
在Linux 2.6.x內(nèi)核中的內(nèi)核模塊調(diào)試方法
Linux 2.6之后的內(nèi)核中,由于module-init-tools工具的更改,insmod命令不再支持-m參數(shù),只有采取其他的方法來獲取模塊加載到內(nèi)核的地址。通過分析ELF文件格式,我們知道程序中各段的意義如下: .text(代碼段):用來存放可執(zhí)行文件的操作指令,也就是說是它是可執(zhí)行程序在內(nèi)存種的鏡像。 .data(數(shù)據(jù)段):數(shù)據(jù)段用來存放可執(zhí)行文件中已初始化全局變量,也就是存放程序靜態(tài)分配的變量和全局變量。 .bss(BSS段):BSS段包含了程序中未初始化全局變量,在內(nèi)存中 bss段全部置零。 .rodata(只讀段):該段保存著只讀數(shù)據(jù),在進程映象中構(gòu)造不可寫的段。 通過在模塊初始化函數(shù)中放置一下代碼,我們可以很容易地獲得模塊加載到內(nèi)存中的地址。
……int bss_var;static int hello_init(void){ printk(KERN_ALERT “Text location .text(Code Segment):%p “,hello_init); static int data_var=0; printk(KERN_ALERT “Data Location .data(Data Segment):%p “,&data_var); printk(KERN_ALERT “BSS Location: .bss(BSS Segment):%p “,&bss_var); ……}Module_init(hello_init);
這里,通過在模塊的初始化函數(shù)中添加一段簡單的程序,使模塊在加載時打印出在內(nèi)核中的加載地址。.rodata段的地址可以通過執(zhí)行命令readelf -e hello.ko,取得.rodata在文件中的偏移量并加上段的align值得出。
為了使讀者能夠更好地進行模塊的調(diào)試,kgdb項目還發(fā)布了一些腳本程序能夠自動探測模塊的插入并自動更新gdb中模塊的符號信息。這些腳本程序的工作原理與前面解釋的工作過程相似。硬件斷點
kgdb提供對硬件調(diào)試寄存器的支持。在kgdb中可以設(shè)置三種硬件斷點:執(zhí)行斷點(Execution Breakpoint)、寫斷點(Write Breakpoint)、訪問斷點(Access Breakpoint)但不支持I/O訪問的斷點。 目前,kgdb對硬件斷點的支持是通過宏來實現(xiàn)的,最多可以設(shè)置4個硬件斷點,這些斷點的用法如下: 在有些情況下,硬件斷點的使用對于內(nèi)核的調(diào)試是非常方便的。
在VMware中搭建調(diào)試環(huán)境,kgdb調(diào)試環(huán)境需要使用兩臺微機分別充當(dāng)development機和target機,使用VMware后我們只使用一臺計算機就可以順利完成kgdb調(diào)試環(huán)境的搭建。
以windows下的環(huán)境為例,創(chuàng)建兩臺虛擬機,一臺作為開發(fā)機,一臺作為目標(biāo)機。 虛擬機之間的串口連接
虛擬機中的串口連接可以采用兩種方法。一種是指定虛擬機的串口連接到實際的COM上,例如開發(fā)機連接到COM1,目標(biāo)機連接到COM2,然后把兩個串口通過串口線相連接。另一種更為簡便的方法是:在較高一些版本的VMware中都支持把串口映射到命名管道,把兩個虛擬機的串口映射到同一個命名管道。例如,在兩個虛擬機中都選定同一個命名管道 .pipecom_1,指定target機的COM口為server端,并選擇”The other end is a virtual machine”屬性;指定development機的COM口端為client端,同樣指定COM口的”The other end is a virtual machine”屬性。
對于IO mode屬性,在target上選中”Yield CPU on poll”復(fù)選擇框,development機不選。這樣,可以無需附加任何硬件,利用虛擬機就可以搭建kgdb調(diào)試環(huán)境。 即降低了使用kgdb進行調(diào)試的硬件要求,也簡化了建立調(diào)試環(huán)境的過程。
VMware的使用技巧
VMware虛擬機是比較占用資源的,尤其是象上面那樣在Windows中使用兩臺虛擬機。因此,最好為系統(tǒng)配備512M以上的內(nèi)存,每臺虛擬機至少分配128M的內(nèi)存。這樣的硬件要求,對目前主流配置的PC而言并不是過高的要求。
出于系統(tǒng)性能的考慮,在VMware中盡量使用字符界面進行調(diào)試工作。同時,Linux系統(tǒng)默認(rèn)情況下開啟了sshd服務(wù),建議使用SecureCRT登陸到Linux進行操作,這樣可以有較好的用戶使用界面。 在Linux下的虛擬機中使用kgdb
對于在Linux下面使用VMware虛擬機的情況,筆者沒有做過實際的探索。從原理上而言,只需要在Linux下只要創(chuàng)建一臺虛擬機作為target機,開發(fā)機的工作可以在實際的Linux環(huán)境中進行,搭建調(diào)試環(huán)境的過程與上面所述的過程類似。由于只需要創(chuàng)建一臺虛擬機,所以使用Linux下的虛擬機搭建kgdb調(diào)試環(huán)境對系統(tǒng)性能的要求較低。(vmware已經(jīng)推出了Linux下的版本)還可以在development機上配合使用一些其他的調(diào)試工具,例如功能更強大的cgdb、圖形界面的DDD調(diào)試器等,以方便內(nèi)核的調(diào)試工作。
kgdb的一些特點和不足
使用kgdb作為內(nèi)核調(diào)試環(huán)境最大的不足在于對kgdb硬件環(huán)境的要求較高,必須使用兩臺計算機分別作為target和development機。盡管使用虛擬機的方法可以只用一臺PC即能搭建調(diào)試環(huán)境,但是對系統(tǒng)其他方面的性能也提出了一定的要求,同時也增加了搭建調(diào)試環(huán)境時復(fù)雜程度。
另外,kgdb內(nèi)核的編譯、配置也比較復(fù)雜,需要一定的技巧,筆者當(dāng)時做的時候也是費了很多周折。當(dāng)調(diào)試過程結(jié)束后時,還需要重新制作所要發(fā)布的內(nèi)核。使用kgdb并不能進行全程調(diào)試,也就是說kgdb并不能用于調(diào)試系統(tǒng)一開始的初始化引導(dǎo)過程。
不過,kgdb是一個不錯的內(nèi)核調(diào)試工具,使用它可以進行對內(nèi)核的全面調(diào)試,甚至可以調(diào)試內(nèi)核的中斷處理程序。如果在一些圖形化的開發(fā)工具的幫助下,對內(nèi)核的調(diào)試將更方便。
使用SkyEye構(gòu)建Linux內(nèi)核調(diào)試環(huán)境
SkyEye是一個開源軟件項目(OPenSource Software),SkyEye項目的目標(biāo)是在通用的Linux和Windows平臺上模擬常見的嵌入式計算機系統(tǒng)。SkyEye實現(xiàn)了一個指令級的硬件模擬平臺,可以模擬多種嵌入式開發(fā)板,支持多種CPU指令集。SkyEye 的核心是 GNU 的 gdb 項目,它把gdb和 ARM Simulator很好地結(jié)合在了一起。加入ARMulator 的功能之后,它就可以來仿真嵌入式開發(fā)板,在它上面不僅可以調(diào)試硬件驅(qū)動,還可以調(diào)試操作系統(tǒng)。Skyeye項目目前已經(jīng)在嵌入式系統(tǒng)開發(fā)領(lǐng)域得到了很大的推廣。
SkyEye的安裝和μcLinux內(nèi)核編譯
SkyEye的安裝
SkyEye的安裝不是本文要介紹的重點,目前已經(jīng)有大量的資料對此進行了介紹。有關(guān)SkyEye的安裝與使用的內(nèi)容請查閱參考資料[11]。由于skyeye面目主要用于嵌入式系統(tǒng)領(lǐng)域,所以在skyeye上經(jīng)常使用的是μcLinux系統(tǒng),當(dāng)然使用Linux作為skyeye上運行的系統(tǒng)也是可以的。由于介紹μcLinux 2.6在skyeye上編譯的相關(guān)資料并不多,所以下面進行詳細(xì)介紹。 μcLinux 2.6.x的編譯
要在SkyEye中調(diào)試操作系統(tǒng)內(nèi)核,首先必須使被調(diào)試內(nèi)核能在SkyEye所模擬的開發(fā)板上正確運行。因此,正確編譯待調(diào)試操作系統(tǒng)內(nèi)核并配置SkyEye是進行內(nèi)核調(diào)試的第一步。下面我們以SkyEye模擬基于Atmel AT91X40的開發(fā)板,并運行μcLinux 2.6為例介紹SkyEye的具體調(diào)試方法。 安裝交叉編譯環(huán)境
先安裝交叉編譯器。盡管在一些資料中說明使用工具鏈arm-elf-tools-20040427.sh ,但是由于arm-elf-xxx與arm-linux-xxx對宏及鏈接處理的不同,經(jīng)驗證明使用arm-elf-xxx工具鏈在鏈接vmlinux的最后階段將會出錯。所以這里我們使用的交叉編譯工具鏈?zhǔn)牵篴rm-uclinux-tools-base-gcc3.4.0-20040713.sh,關(guān)于該交叉編譯工具鏈的下載地址請參見[6]。注意以下步驟最好用root用戶來執(zhí)行。
[root@lisl tmp]#chmod +x arm-uclinux-tools-base-gcc3.4.0-20040713.sh[root@lisl tmp]#./arm-uclinux-tools-base-gcc3.4.0-20040713.sh
安裝交叉編譯工具鏈之后,請確保工具鏈安裝路徑存在于系統(tǒng)PATH變量中。
制作μcLinux內(nèi)核
得到μcLinux發(fā)布包的一個最容易的方法是直接訪問uClinux.org站點[7]。該站點發(fā)布的內(nèi)核版本可能不是最新的,但你能找到一個最新的μcLinux補丁以及找一個對應(yīng)的Linux內(nèi)核版本來制作一個最新的μcLinux內(nèi)核。這里,將使用這種方法來制作最新的μcLinux內(nèi)核。目前(筆者記錄編寫此文章時),所能得到的發(fā)布包的最新版本是uClinux-dist.20041215.tar.gz。
下載uClinux-dist.20041215.tar.gz,文件的下載地址請參見[7]。 下載linux-2.6.9-hsc0.patch.gz,文件的下載地址請參見[8]。 下載linux-2.6.9.tar.bz2,文件的下載地址請參見[9]。 現(xiàn)在我們得到了整個的linux-2.6.9源代碼,以及所需的內(nèi)核補丁。請準(zhǔn)備一個有2GB空間的目錄里來完成以下制作μcLinux內(nèi)核的過程。
[root@lisl tmp]# tar -jxvf uClinux-dist-20041215.tar.bz2[root@lisl uClinux-dist]# tar -jxvf linux-2.6.9.tar.bz2[root@lisl uClinux-dist]# gzip -dc linux-2.6.9-hsc0.patch.gz | patch -p0
或者使用:
[root@lisl uClinux-dist]# gunzip linux-2.6.9-hsc0.patch.gz [root@lisl uClinux-dist]patch -p0 < linux-2.6.9-hsc0.patch
執(zhí)行以上過程后,將在linux-2.6.9/arch目錄下生成一個補丁目錄-armnommu。刪除原來μcLinux目錄里的linux-2.6.x(即那個linux-2.6.9-uc0),并將我們打好補丁的Linux內(nèi)核目錄更名為linux-2.6.x。
[root@lisl uClinux-dist]# rm -rf linux-2.6.x/[root@lisl uClinux-dist]# mv linux-2.6.9 linux-2.6.x
配置和編譯μcLinux內(nèi)核
因為只是出于調(diào)試μcLinux內(nèi)核的目的,這里沒有生成uClibc庫文件及romfs.img文件。在發(fā)布μcLinux時,已經(jīng)預(yù)置了某些常用嵌入式開發(fā)板的配置文件,因此這里直接使用這些配置文件,過程如下:
[root@lisl uClinux-dist]# cd linux-2.6.x[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- atmel_deconfig
atmel_deconfig文件是μcLinux發(fā)布時提供的一個配置文件,存放于目錄linux-2.6.x /arch/armnommu/configs/中。
[root@lisl linux-2.6.x]#make ARCH=armnommu CROSS_COMPILE=arm-uclinux- oldconfig
下面編譯配置好的內(nèi)核:
[root@lisl linux-2.6.x]# make ARCH=armnommu CROSS_COMPILE=arm-uclinux- v=1
一般情況下,編譯將順利結(jié)束并在Linux-2.6.x/目錄下生成未經(jīng)壓縮的μcLinux內(nèi)核文件vmlinux。需要注意的是為了調(diào)試μcLinux內(nèi)核,需要打開內(nèi)核編譯的調(diào)試選項-g,使編譯后的內(nèi)核帶有調(diào)試信息。打開編譯選項的方法可以選擇: “Kernel debugging->Compile the kernel with debug info”后將自動打開調(diào)試選項。也可以直接修改linux-2.6.x目錄下的Makefile文件,為其打開調(diào)試開關(guān)。方法如下:。
CFLAGS += -g
最容易出現(xiàn)的問題是找不到arm-uclinux-gcc命令的錯誤,主要原因是PATH變量中沒有 包含arm-uclinux-gcc命令所在目錄。在arm-linux-gcc的缺省安裝情況下,它的安裝目錄是/root/bin/arm-linux-tool/,使用以下命令將路徑加到PATH環(huán)境變量中。
Export PATH $PATH:/root/bin/arm-linux-tool/bin
文件系統(tǒng)的制作
Linux內(nèi)核在啟動的時的最后操作之一是加載根文件系統(tǒng)。根文件系統(tǒng)中存放了嵌入式 系統(tǒng)使用的所有應(yīng)用程序、文件及其他一些需要用到的服務(wù)。出于文章篇幅的考慮,這里不打算介紹文件系統(tǒng)的制作方法,讀者可以查閱一些其他的相關(guān)資料。值得注意的是,由配置文件skyeye.conf指定了裝載到內(nèi)核中的跟蹤文件系統(tǒng)。
使用SkyEye調(diào)試
編譯完μcLinux內(nèi)核后,就可以在SkyEye中調(diào)試該ELF執(zhí)行文件格式的內(nèi)核了。前面已經(jīng)說過利用SkyEye調(diào)試內(nèi)核與使用gdb調(diào)試運用程序的方法相同。 需要提醒讀者的是,SkyEye的配置文件-skyeye.conf記錄了模擬的硬件配置和模擬執(zhí)行行為。該配置文件是SkyEye系統(tǒng)中一個及其重要的文件,很多錯誤和異常情況的發(fā)生都和該文件有關(guān)。在安裝配置SkyEye出錯時,請首先檢查該配置文件然后再進行其他的工作。此時,所有的準(zhǔn)備工作已經(jīng)完成,就可以進行內(nèi)核的調(diào)試工作了。
使用SkyEye調(diào)試內(nèi)核的特點和不足
在SkyEye中可以進行對Linux系統(tǒng)內(nèi)核的全程調(diào)試。由于SkyEye目前主要支持基于ARM內(nèi)核的CPU,因此一般而言需要使用交叉編譯工具編譯待調(diào)試的Linux系統(tǒng)內(nèi)核。另外,制作SkyEye中使用的內(nèi)核編譯、配置過程比較復(fù)雜、繁瑣。不過,當(dāng)調(diào)試過程結(jié)束后無需重新制作所要發(fā)布的內(nèi)核。 SkyEye只是對系統(tǒng)硬件進行了一定程度上的模擬,所以在SkyEye與真實硬件環(huán)境相比較而言還是有一定的差距,這對一些與硬件緊密相關(guān)的調(diào)試可能會有一定的影響,例如驅(qū)動程序的調(diào)試。不過對于大部分軟件的調(diào)試,SkyEye已經(jīng)提供了精度足夠的模擬了。 SkyEye的下一個目標(biāo)是和eclipse結(jié)合,有了圖形界面,能為調(diào)試和查看源碼提供一些方便。
KDB
Linux 內(nèi)核調(diào)試器(KDB)允許您調(diào)試 Linux 內(nèi)核。這個恰如其名的工具實質(zhì)上是內(nèi)核代碼的補丁,它允許高手訪問內(nèi)核內(nèi)存和數(shù)據(jù)結(jié)構(gòu)。KDB 的主要優(yōu)點之一就是它不需要用另一臺機器進行調(diào)試:您可以調(diào)試正在運行的內(nèi)核。
設(shè)置一臺用于 KDB 的機器需要花費一些工作,因為需要給內(nèi)核打補丁并進行重新編譯。KDB 的用戶應(yīng)當(dāng)熟悉 Linux 內(nèi)核的編譯(在一定程度上還要熟悉內(nèi)核的內(nèi)部機理)。 在本文中,我們將從有關(guān)下載 KDB 補丁、打補丁、(重新)編譯內(nèi)核以及啟動 KDB 方面的信息著手。然后我們將了解 KDB 命令并研究一些較常用的命令。最后,我們將研究一下有關(guān)設(shè)置和顯示選項方面的一些詳細(xì)信息。
入門
KDB 項目是由 Silicon Graphics 維護的,您需要從它的 FTP 站點下載與內(nèi)核版本有關(guān)的補丁。(在編寫本文時)可用的最新 KDB 版本是 4.2。您將需要下載并應(yīng)用兩個補丁。一個是“公共的”補丁,包含了對通用內(nèi)核代碼的更改,另一個是特定于體系結(jié)構(gòu)的補丁。補丁可作為 bz2 文件獲取。例如,在運行 2.4.20 內(nèi)核的 x86 機器上,您會需要 kdb-v4.2-2.4.20-common-1.bz2 和 kdb-v4.2-2.4.20-i386-1.bz2。 這里所提供的所有示例都是針對 i386 體系結(jié)構(gòu)和 2.4.20 內(nèi)核的。您將需要根據(jù)您的機器和內(nèi)核版本進行適當(dāng)?shù)母摹D€需要擁有 root 許可權(quán)以執(zhí)行這些操作。 將文件復(fù)制到 /usr/src/linux 目錄中并從用 bzip2 壓縮的文件解壓縮補丁文件:
#bzip2 -d kdb-v4.2-2.4.20-common-1.bz2#bzip2 -d kdb-v4.2-2.4.20-i386-1.bz2
您將獲得 kdb-v4.2-2.4.20-common-1 和 kdb-v4.2-2.4-i386-1 文件。 現(xiàn)在,應(yīng)用這些補?。?/p>
#patch -p1 2-2.4.20-common-1#patch -p1 2-2.4.20-i386-1
這些補丁應(yīng)該干凈利落地加以應(yīng)用。查找任何以 .rej 結(jié)尾的文件。這個擴展名表明這些是失敗的補丁。如果內(nèi)核樹沒問題,那么補丁的應(yīng)用就不會有任何問題。
接下來,需要構(gòu)建內(nèi)核以支持 KDB。第一步是設(shè)置 CONFIG_KDB 選項。使用您喜歡的配置機制(xconfig 和 menuconfig 等)來完成這一步。轉(zhuǎn)到結(jié)尾處的“Kernel hacking”部分并選擇“Built-in Kernel Debugger support”選項。
您還可以根據(jù)自己的偏好選擇其它兩個選項。選擇“Compile the kernel with frame pointers”選項(如果有的話)則設(shè)置CONFIG_FRAME_POINTER 標(biāo)志。這將產(chǎn)生更好的堆棧回溯,因為幀指針寄存器被用作幀指針而不是通用寄存器。您還可以選擇“KDB off by default”選項。這將設(shè)置 CONFIG_KDB_OFF 標(biāo)志,并且在缺省的情況下將關(guān)閉 KDB。我們將在后面一節(jié)中對此進行詳細(xì)介紹。
保存配置,然后退出。重新編譯內(nèi)核。建議在構(gòu)建內(nèi)核之前執(zhí)行“make clean”。用常用的方式安裝內(nèi)核并引導(dǎo)它。
初始化并設(shè)置環(huán)境變量
您可以定義將在 KDB 初始化期間執(zhí)行的 KDB 命令。需要在純文本文件 kdb_cmds 中定義這些命令,該文件位于 Linux 源代碼樹(當(dāng)然是在打了補丁之后)的 KDB 目錄中。該文件還可以用來定義設(shè)置顯示和打印選項的環(huán)境變量。文件開頭的注釋提供了編輯文件方面的幫助。使用這個文件的缺點是,在您更改了文件之后需要重新構(gòu)建并重新安裝內(nèi)核。
激活 KDB
如果編譯期間沒有選中 CONFIG_KDB_OFF ,那么在缺省情況下 KDB 是活動的。否則,您需要顯式地激活它 - 通過在引導(dǎo)期間將kdb=on 標(biāo)志傳遞給內(nèi)核或者通過在掛裝了 /proc 之后執(zhí)行該工作:
#echo “1” >/proc/sys/kernel/kdb
倒過來執(zhí)行上述步驟則會取消激活 KDB。也就是說,如果缺省情況下 KDB 是打開的,那么將 kdb=off 標(biāo)志傳遞給內(nèi)核或者執(zhí)行下面這個操作將會取消激活 KDB:
#echo “0” >/proc/sys/kernel/kdb
在引導(dǎo)期間還可以將另一個標(biāo)志傳遞給內(nèi)核。 kdb=early 標(biāo)志將導(dǎo)致在引導(dǎo)過程的初始階段就把控制權(quán)傳遞給 KDB。如果您需要在引導(dǎo)過程初始階段進行調(diào)試,那么這將有所幫助。 調(diào)用 KDB 的方式有很多。如果 KDB 處于打開狀態(tài),那么只要內(nèi)核中有緊急情況就自動調(diào)用它。按下鍵盤上的 PAUSE 鍵將手工調(diào)用 KDB。調(diào)用 KDB 的另一種方式是通過串行控制臺。當(dāng)然,要做到這一點,需要設(shè)置串行控制臺并且需要一個從串行控制臺進行讀取的程序。按鍵序列 Ctrl-A 將從串行控制臺調(diào)用 KDB。
KDB 命令
KDB 是一個功能非常強大的工具,它允許進行幾個操作,比如內(nèi)存和寄存器修改、應(yīng)用斷點和堆棧跟蹤。根據(jù)這些,可以將 KDB 命令分成幾個類別。
下面是有關(guān)每一類中最常用命令的詳細(xì)信息:內(nèi)存顯示和修改
這一類別中最常用的命令就是 md 、 mdr 、 mm 和 mmW 。 md 命令以一個地址/符號和行計數(shù)為參數(shù),顯示從該地址開始的 line-count 行的內(nèi)存。如果沒有指定 line-count ,那么就使用環(huán)境變量所指定的缺省值。如果沒有指定地址,那么 md 就從上一次打印的地址繼續(xù)。地址打印在開頭,字符轉(zhuǎn)換打印在結(jié)尾。 mdr 命令帶有地址/符號以及字節(jié)計數(shù),顯示從指定的地址開始的 byte-count 字節(jié)數(shù)的初始內(nèi)存內(nèi)容。它本質(zhì)上和 md 一樣,但是它不顯示起始地址并且不在結(jié)尾顯示字符轉(zhuǎn)換。 mdr 命令較少使用。
mm 命令修改內(nèi)容。它以地址/符號和新內(nèi)容作為參數(shù),用 new-contents 替換地址處的內(nèi)容。 mmW 命令更改從地址開始的 W 個字節(jié)。請注意, mm 更改一個機器字。 示例 顯示從 0xc000000 開始的 15 行內(nèi)存:
[0]kdb> md 0xc000000 15
將內(nèi)存位置為 0xc000000 上的內(nèi)容更改為 0x10:
[0]kdb> mm 0xc000000 0x10
寄存器顯示和修改
這一類別中的命令有 rd 、 rm 和 ef 。 rd 命令(不帶任何參數(shù))顯示處理器寄存器的內(nèi)容。它可以有選擇地帶三個參數(shù)。如果傳遞了 c 參數(shù),則 rd 顯示處理器的控制寄存器;如果帶有 d 參數(shù),那么它就顯示調(diào)試寄存器;如果帶有 u 參數(shù),則顯示上一次進入內(nèi)核的當(dāng)前任務(wù)的寄存器組。
rm 命令修改寄存器的內(nèi)容。它以寄存器名稱和 new-contents 作為參數(shù),用 new-contents 修改寄存器。寄存器名稱與特定的體系結(jié)構(gòu)有關(guān)。目前,不能修改控制寄存器。 ef 命令以一個地址作為參數(shù),它顯示指定地址處的異常幀。 示例
顯示通用寄存器組:[0]kdb> rd[0]kdb> rm %ebx 0x25
斷點
常用的斷點命令有 bp 、 bc 、 bd 、 be 和 bl 。 bp 命令以一個地址/符號作為參數(shù),它在地址處應(yīng)用斷點。當(dāng)遇到該斷點時則停止執(zhí)行并將控制權(quán)交予 KDB。該命令有幾個有用的變體。 bpa 命令對 SMP 系統(tǒng)中的所有處理器應(yīng)用斷點。 bph 命令強制在支持硬件寄存器的系統(tǒng)上使用它。 bpha 命令類似于 bpa 命令,差別在于它強制使用硬件寄存器。 bd 命令禁用特殊斷點。它接收斷點號作為參數(shù)。該命令不是從斷點表中除去斷點,而只是禁用它。斷點號從 0 開始,根據(jù)可用性順序分配給斷點。 be 命令啟用斷點。該命令的參數(shù)也是斷點號。 bl 命令列出當(dāng)前的斷點集。它包含了啟用的和禁用的斷點。 bc 命令從斷點表中除去斷點。它以具體的斷點號或 * 作為參數(shù),在后一種情況下它將除去所有斷點。 示例
對函數(shù) sys_write() 設(shè)置斷點:
[0]kdb> bp sys_write
列出斷點表中的所有斷點:
[0]kdb> bl
清除斷點號 1:
[0]kdb> bc 1
堆棧跟蹤
主要的堆棧跟蹤命令有 bt 、 btp 、 btc 和 bta 。 bt 命令設(shè)法提供有關(guān)當(dāng)前線程的堆棧的信息。它可以有選擇地將堆棧地址作為參數(shù)。如果沒有提供地址,那么它就采用當(dāng)前寄存器來回溯堆棧。否則,它假定所提供的地址是有效的堆棧幀起始地址并設(shè)法進行回溯。如果內(nèi)核編譯期間設(shè)置了CONFIG_FRAME_POINTER 選項,那么就用幀指針寄存器來維護堆棧,從而就可以正確地執(zhí)行堆?;厮?。
如果沒有設(shè)置CONFIG_FRAME_POINTER ,那么 bt 命令可能會產(chǎn)生錯誤的結(jié)果。 btp 命令將進程標(biāo)識作為參數(shù),并對這個特定進程進行堆?;厮?。
btc 命令對每個活動 CPU 上正在運行的進程執(zhí)行堆棧回溯。它從第一個活動 CPU 開始執(zhí)行 bt ,然后切換到下一個活動 CPU,以此類推。
bta 命令對處于某種特定狀態(tài)的所有進程執(zhí)行回溯。若不帶任何參數(shù),它就對所有進程執(zhí)行回溯??梢杂羞x擇地將各種參數(shù)傳遞給該命令。將根據(jù)參數(shù)處理處于特定狀態(tài)的進程。選項以及相應(yīng)的狀態(tài)如下:
- D:不可中斷狀態(tài)
- R:正運行
- S:可中斷休眠
- T:已跟蹤或已停止
- Z:僵死
- U:不可運行
這類命令中的每一個都會打印出一大堆信息。
示例
跟蹤當(dāng)前活動線程的堆棧:
[0]kdb> bt
跟蹤標(biāo)識為 575 的進程的堆棧:
[0]kdb> btp 575
其它命令
下面是在內(nèi)核調(diào)試過程中非常有用的其它幾個東西 KDB 命令。 id 命令以一個地址/符號作為參數(shù),它對從該地址開始的指令進行反匯編。環(huán)境變量 IDCOUNT 確定要顯示多少行輸出。
ss 命令單步執(zhí)行指令然后將控制返回給 KDB。該指令的一個變體是 ssb ,它執(zhí)行從當(dāng)前指令指針地址開始的指令(在屏幕上打印指令),直到它遇到將引起分支轉(zhuǎn)移的指令為止。分支轉(zhuǎn)移指令的典型示例有 call 、 return 和 jump 。
go 命令讓系統(tǒng)繼續(xù)正常執(zhí)行。一直執(zhí)行到遇到斷點為止(如果已經(jīng)應(yīng)用了一個斷點的話)。
reboot 命令立刻重新引導(dǎo)系統(tǒng)。它并沒有徹底關(guān)閉系統(tǒng),因此結(jié)果是不可預(yù)測的。 ll 命令以地址、偏移量和另一個 KDB 命令作為參數(shù)。它對鏈表中的每個元素反復(fù)執(zhí)行作為參數(shù)的這個命令。所執(zhí)行的命令以列表中當(dāng)前元素的地址作為參數(shù)。 示例
反匯編從例程 schedule 開始的指令。所顯示的行數(shù)取決于環(huán)境變量 IDCOUNT :
[0]kdb> id schedule
執(zhí)行指令直到它遇到分支轉(zhuǎn)移條件(在本例中為指令 jne )為止:
[0]kdb> ssb0xc0105355 default_idle+0x25: cli0xc0105356 default_idle+0x26: mov 0x14(%edx),%eax0xc0105359 default_idle+0x29: test %eax, %eax0xc010535b default_idle+0x2b: jne 0xc0105361 default_idle+0x31
技巧和訣竅
調(diào)試一個問題涉及到:使用調(diào)試器(或任何其它工具)找到問題的根源以及使用源代碼來跟蹤導(dǎo)致問題的根源。單單使用源代碼來確定問題是極其困難的,只有老練的內(nèi)核黑客才有可能做得到。相反,大多數(shù)的新手往往要過多地依靠調(diào)試器來修正錯誤。這種方法可能會產(chǎn)生不正確的問題解決方案。我們擔(dān)心的是這種方法只會修正表面癥狀而不能解決真正的問題。此類錯誤的典型示例是添加錯誤處理代碼以處理 NULL 指針錯誤地引用,卻沒有查出無效引用的真正原因。
結(jié)合研究代碼和使用調(diào)試工具這兩種方法是識別和修正問題的最佳方案。 調(diào)試器的主要用途是找到錯誤的位置、確認(rèn)癥狀(在某些情況下還有起因)、確定變量的值,以及確定程序是如何出現(xiàn)這種情況的(即,建立調(diào)用堆棧)。有經(jīng)驗的黑客會知道對于某種特定的問題應(yīng)使用哪一個調(diào)試器,并且能迅速地根據(jù)調(diào)試器獲取必要的信息,然后繼續(xù)分析代碼以識別起因。 因此,這里為您介紹了一些技巧,以便您能使用 KDB 快速地取得上述結(jié)果。當(dāng)然,要記住,調(diào)試的速度和精確度來自經(jīng)驗、實踐和良好的系統(tǒng)知識(硬件和內(nèi)核的內(nèi)部機理等)。 技巧 #1
在 KDB 中,在提示處輸入地址將返回與之最為匹配的符號。這在堆棧分析以及確定全局?jǐn)?shù)據(jù)的地址/值和函數(shù)地址方面極其有用。同樣,輸入符號名則返回其虛擬地址。 示例 表明函數(shù) sys_read 從地址 0xc013db4c 開始:
[0]kdb> 0xc013db4c0xc013db4c = 0xc013db4c (sys_read)
同樣,表明 sys_write 位于地址 0xc013dcc8:
[0]kdb> sys_writesys_write = 0xc013dcc8 (sys_write)
這些有助于在分析堆棧時找到全局?jǐn)?shù)據(jù)和函數(shù)地址。 技巧 #2
在編譯帶 KDB 的內(nèi)核時,只要 CONFIG_FRAME_POINTER 選項出現(xiàn)就使用該選項。為此,需要在配置內(nèi)核時選擇“Kernel hacking”部分下面的“Compile the kernel with frame pointers”選項。這確保了幀指針寄存器將被用作幀指針,從而產(chǎn)生正確的回溯。實際上,您可以手工轉(zhuǎn)儲幀指針寄存器的內(nèi)容并跟蹤整個堆棧。例如,在 i386 機器上,%ebp 寄存器可以用來回溯整個堆棧。
例如,在函數(shù) rmqueue() 上次執(zhí)行第一個指令后,堆??瓷先ヮ愃朴谙旅孢@樣:
[0]kdb> md %ebp0xc74c9f38 c74c9f60 c0136c40 000001f0 000000000xc74c9f48 08053328 c0425238 c04253a8 000000000xc74c9f58 000001f0 00000246 c74c9f6c c0136a250xc74c9f68 c74c8000 c74c9f74 c0136d6d c74c9fbc0xc74c9f78 c014fe45 c74c8000 00000000 08053328[0]kdb> 0xc0136c400xc0136c40 = 0xc0136c40 (__alloc_pages +0x44)[0]kdb> 0xc0136a250xc0136a25 = 0xc0136a25 (_alloc_pages +0x19)[0]kdb> 0xc0136d6d0xc0136d6d = 0xc0136d6d (__get_free_pages +0xd)
我們可以看到 rmqueue() 被 __alloc_pages 調(diào)用,后者接下來又被動 _alloc_pages 調(diào)用,以此類推。
每一幀的第一個雙字(double word)指向下一幀,這后面緊跟著調(diào)用函數(shù)的地址。因此,跟蹤堆棧就變成一件輕松的工作了。 技巧 #3
go 命令可以有選擇地以一個地址作為參數(shù)。如果您想在某個特定地址處繼續(xù)執(zhí)行,則可以提供該地址作為參數(shù)。另一個辦法是使用rm 命令修改指令指針寄存器,然后只要輸入 go 。如果您想跳過似乎會引起問題的某個特定指令或一組指令,這就會很有用。但是,請注意,該指令使用不慎會造成嚴(yán)重的問題,系統(tǒng)可能會嚴(yán)重崩潰。 技巧 #4
您可以利用一個名為 defcmd 的有用命令來定義自己的命令集。例如,每當(dāng)遇到斷點時,您可能希望能同時檢查某個特殊變量、檢查某些寄存器的內(nèi)容并轉(zhuǎn)儲堆棧。通常,您必須要輸入一系列命令,以便能同時執(zhí)行所有這些工作。 defcmd 允許您定義自己的命令,該命令可以包含一個或多個預(yù)定義的 KDB 命令。然后只需要用一個命令就可以完成所有這三項工作。其語法如下:
[0]kdb> defcmd name “usage” “help”[0]kdb> [defcmd] type the commands here[0]kdb> [defcmd] endefcmd
例如,可以定義一個(簡單的)新命令 hari ,它顯示從地址 0xc000000 開始的一行內(nèi)存、顯示寄存器的內(nèi)容并轉(zhuǎn)儲堆棧:
[0]kdb> defcmd hari “” “no arguments needed”[0]kdb> [defcmd] md 0xc000000 1[0]kdb> [defcmd] rd[0]kdb> [defcmd] md %ebp 1[0]kdb> [defcmd] endefcmd
該命令的輸出會是:
[0]kdb> hari[hari]kdb> md 0xc000000 10xc000000 00000001 f000e816 f000e2c3 f000e816[hari]kdb> rdeax = 0x00000000 ebx = 0xc0105330 ecx = 0xc0466000 edx = 0xc0466000…….[hari]kdb> md %ebp 10xc0467fbc c0467fd0 c01053d2 00000002 000a0200[0]kdb>
技巧 #5
可以使用 bph 和 bpha 命令(假如體系結(jié)構(gòu)支持使用硬件寄存器)來應(yīng)用讀寫斷點。這意味著每當(dāng)從某個特定地址讀取數(shù)據(jù)或?qū)?shù)據(jù)寫入該地址時,我們都可以對此進行控制。當(dāng)調(diào)試數(shù)據(jù)/內(nèi)存毀壞問題時這可能會極其方便,在這種情況中您可以用它來識別毀壞的代碼/進程。 示例 每當(dāng)將四個字節(jié)寫入地址 0xc0204060 時間就進入內(nèi)核調(diào)試器:
[0]kdb> bph 0xc0204060 dataw 4
在讀取從 0xc000000 開始的至少兩個字節(jié)的數(shù)據(jù)時進入內(nèi)核調(diào)試器:
[0]kdb> bph 0xc000000 datar 2
結(jié)束語
對于執(zhí)行內(nèi)核調(diào)試,KDB 是一個方便的且功能強大的工具。它提供了各種選項,并且使我們能夠分析內(nèi)存內(nèi)容和數(shù)據(jù)結(jié)構(gòu)。最妙的是,它不需要用另一臺機器來執(zhí)行調(diào)試。
Kprobes
Kprobes 是 Linux 中的一個簡單的輕量級裝置,讓您可以將斷點插入到正在運行的內(nèi)核之中。 Kprobes 提供了一個強行進入任何內(nèi)核例程并從中斷處理器無干擾地收集信息的接口。使用 Kprobes 可以 輕松地收集處理器寄存器和全局?jǐn)?shù)據(jù)結(jié)構(gòu)等調(diào)試信息。開發(fā)者甚至可以使用 Kprobes 來修改 寄存器值和全局?jǐn)?shù)據(jù)結(jié)構(gòu)的值。 為完成這一任務(wù),Kprobes 向運行的內(nèi)核中給定地址寫入斷點指令,插入一個探測器。 執(zhí)行被探測的指令會導(dǎo)致斷點錯誤。Kprobes 鉤?。╤ook in)斷點處理器并收集調(diào)試信息。Kprobes 甚至可以單步執(zhí)行被探測的指令。
1 安裝
要安裝 Kprobes,需要從 Kprobes 主頁下載最新的補丁。 打包的文件名稱類似于 kprobes-2.6.8-rc1.tar.gz。解開補丁并將其安裝到 Linux 內(nèi)核:
$tar -xvzf kprobes-2.6.8-rc1.tar.gz $cd /usr/src/linux-2.6.8-rc1 $patch -p1 < ../kprobes-2.6.8-rc1-base.patch
Kprobes 利用了 SysRq 鍵,這個 DOS 時代的產(chǎn)物在 Linux 中有了新的用武之地。您可以在 Scroll Lock鍵左邊找到 SysRq 鍵;它通常標(biāo)識為 Print Screen。要為 Kprobes 啟用 SysRq 鍵,需要安裝 kprobes-2.6.8-rc1-sysrq.patch 補?。?/p>
$patch -p1 < ../kprobes-2.6.8-rc1-sysrq.patch
使用 make xconfig/ make menuconfig/ make oldconfig 配置內(nèi)核,并 啟用 CONFIG_KPROBES 和 CONFIG_MAGIC_SYSRQ標(biāo)記。 編譯并引導(dǎo)到新內(nèi)核。您現(xiàn)在就已經(jīng)準(zhǔn)備就緒,可以插入 printk 并通過編寫簡單的 Kprobes 模塊來動態(tài)而且無干擾地 收集調(diào)試信息。
2 編寫 Kprobes 模塊
對于每一個探測器,您都要分配一個結(jié)構(gòu)體 struct kprobe kp; (參考 include/linux/kprobes.h 以獲得關(guān)于此數(shù)據(jù)結(jié)構(gòu)的詳細(xì)信息)。 清單 9. 定義 pre、post 和 fault 處理器
/* pre_handler: this is called just before the probed instruction is * executed. */int handler_pre(struct kprobe *p, struct pt_regs *regs) {printk(“pre_handler: p->addr=0x%p, eflags=0x%lx “,p->addr,regs->eflags);return 0;}/* post_handler: this is called after the probed instruction is executed * (provided no exception is generated). */void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {printk(“post_handler: p->addr=0x%p, eflags=0x%lx “, p->addr,regs->eflags);}/* fault_handler: this is called if an exception is generated for any * instruction within the fault-handler, or when Kprobes * single-steps the probed instruction. */int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) {printk(“fault_handler:p->addr=0x%p, eflags=0x%lx “, p->addr,regs->eflags);return 0;}
2.1 獲得內(nèi)核例程的地址 在注冊過程中,您還需要指定插入探測器的內(nèi)核例程的地址。使用這些方法中的任意一個來獲得內(nèi)核例程 的地址:
從 System.map 文件直接得到地址。
例如,要得到 do_fork 的地址,可以在命令行執(zhí)行 $grep do_fork /usr/src/linux/System.map 。
使用 nm 命令。
$nm vmlinuz |grep do_fork
從 /proc/kallsyms 文件獲得地址。
$cat /proc/kallsyms |grep do_fork
使用 kallsyms_lookup_name() 例程。
這個例程是在 kernel/kallsyms.c 文件中定義的,要使用它,必須啟用 CONFIG_KALLSYMS 編譯內(nèi)核。kallsyms_lookup_name() 接受一個字符串格式內(nèi)核例程名, 返回那個內(nèi)核例程的地址。例如:kallsyms_lookup_name(“do_fork”);
然后在 init_moudle 中注冊您的探測器: 清單 10. 注冊一個探測器
/* specify pre_handler address */kp.pre_handler=handler_pre;/* specify post_handler address */kp.post_handler=handler_post;/* specify fault_handler address */kp.fault_handler=handler_fault;/* specify the address/offset where you want to insert probe. * You can get the address using one of the methods described above. */kp.addr = (kprobe_opcode_t *) kallsyms_lookup_name(“do_fork”);/* check if the kallsyms_lookup_name() returned the correct value. */if (kp.add == NULL) {printk(“kallsyms_lookup_name could not find addressfor the specified symbol name “);return 1;}/* or specify address directly. * $grep “do_fork” /usr/src/linux/System.map * or * $cat /proc/kallsyms |grep do_fork * or * $nm vmlinuz |grep do_fork */kp.addr = (kprobe_opcode_t *) 0xc01441d0;/* All set to register with Kprobes */ register_kprobe(&kp);
一旦注冊了探測器,運行任何 shell 命令都會導(dǎo)致一個對 do_fork 的調(diào)用,您將可以在控制臺上或者運行 dmesg 命令來查看您的 printk。做完后要記得注銷探測器: unregister_kprobe(&kp); 下面的輸出顯示了 kprobe 的地址以及 eflags 寄存器的內(nèi)容:
$tail -5 /var/log/messages Jun 14 18:21:18 llm05 kernel: pre_handler: p->addr=0xc01441d0, eflags=0x202 Jun 14 18:21:18 llm05 kernel: post_handler: p->addr=0xc01441d0, eflags=0x196
獲得偏移量
您可以在例程的開頭或者函數(shù)中的任意偏移位置插入 printk(偏移量必須在指令范圍之內(nèi))。 下面的代碼示例展示了如何來計算偏移量。首先,從對象文件中反匯編機器指令,并將它們 保存為一個文件:
$objdump -D /usr/src/linux/kernel/fork.o > fork.dis
其結(jié)果是: 清單 11. 反匯編的 fork
000022b0 : 22b0: 55 push %ebp 22b1: 89 e5 mov %esp,%ebp 22b3: 57 push %edi 22b4: 89 c7 mov %eax,%edi 22b6: 56 push %esi 22b7: 89 d6 mov %edx,%esi 22b9: 53 push %ebx 22ba: 83 ec 38 sub $0x38,%esp 22bd: c7 45 d0 00 00 00 00 movl $0x0,0xffffffd0(%ebp) 22c4: 89 cb mov %ecx,%ebx 22c6: 89 44 24 04 mov %eax,0x4(%esp) 22ca: c7 04 24 0a 00 00 00 movl $0xa,(%esp) 22d1: e8 fc ff ff ff call 22d2 0x22> 22d6: b8 00 e0 ff ff mov $0xffffe000,%eax 22db: 21 e0 and %esp,%eax 22dd: 8b 00 mov (%eax),%eax
要在偏移位置 0x22c4 插入探測器,先要得到與例程的開始處相對的偏移量 0x22c4 – 0x22b0 = 0x14 ,然后將這個偏移量添加到 do_fork 的地址 0xc01441d0 + 0x14 。(運行 $cat /proc/kallsyms | grep do_fork 命令以獲得 do_fork 的地址。) 您還可以將 do_fork 的相對偏移量 0x22c4 – 0x22b0 = 0x14 添加到 kallsyms_lookup_name(“do_fork”); 的輸入,即:0x14 + kallsyms_lookup_name(“do_fork”); 轉(zhuǎn)儲內(nèi)核數(shù)據(jù)結(jié)構(gòu)
現(xiàn)在,讓我們使用修改過的用來轉(zhuǎn)換數(shù)據(jù)結(jié)構(gòu)的 Kprobe post_handler 來轉(zhuǎn)儲運行在系統(tǒng)上的所有作業(yè)的一些組成部分: 清單 12. 用來轉(zhuǎn)儲數(shù)據(jù)結(jié)構(gòu)的修改過的 Kprope post_handler
void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) { struct task_struct *task; read_lock(&tasklist_lock); for_each_process(task) { printk(“pid =%x task-info_ptr=%lx “, task->pid, task->thread_info); printk(“thread-info element status=%lx,flags=%lx,cpu=%lx “, task->thread_info->status, task->thread_info->flags, task->thread_info->cpu); } read_unlock(&tasklist_lock);}
這個模塊應(yīng)該插入到 do_fork 的偏移位置。 清單 13. pid 1508 和 1509 的結(jié)構(gòu)體 thread_info 的輸出
$tail -10 /var/log/messagesJun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=0, cpu=1Jun 22 18:14:25 llm05 kernel: pid =5e4 task-info_ptr=f5948000Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=8, cpu=0Jun 22 18:14:25 llm05 kernel: pid =5e5 task-info_ptr=f5eca000
啟用奇妙的 SysRq 鍵
為了支持 SysRq 鍵,我們已經(jīng)進行了編譯。這樣來啟用它:
$echo 1 > /proc/sys/kernel/sysrq
現(xiàn)在,您可以使用 Alt+SysRq+W 在控制臺上或者到 /var/log/messages 中去查看所有插入的內(nèi)核探測器。 清單 14. /var/log/messages 顯示出在 do_fork 插入了一個 Kprobe
Jun 23 10:24:48 linux-udp4749545uds kernel: SysRq : Show kprobesJun 23 10:24:48 linux-udp4749545uds kernel:Jun 23 10:24:48 linux-udp4749545uds kernel: [] do_fork+0x0/0x1de
使用 Kprobes 更好地進行調(diào)試
由于探測器事件處理器是作為系統(tǒng)斷點中斷處理器的擴展來運行,所以它們很少或者根本不依賴于系統(tǒng) 工具 —— 這樣可以被植入到大部分不友好的環(huán)境中(從中斷時間和任務(wù)時間到禁用的上下文間切換和支持 SMP 的代碼路徑)—— 都不會對系統(tǒng)性能帶來什么負(fù)面影響。
使用 Kprobes 的好處有很多。不需要重新編譯和重新引導(dǎo)內(nèi)核就可以插入 printk。為了進行調(diào)試可以記錄 處理器寄存器的日志,甚至進行修改 —— 不會干擾系統(tǒng)。類似地,同樣可以無干擾地記錄 Linux 內(nèi)核數(shù)據(jù)結(jié)構(gòu)的日志,甚至是 進行修改。您甚至可以使用 Kprobes 調(diào)試 SMP 系統(tǒng)上的靜態(tài)條件 —— 避免了您自己重新編譯和重新引導(dǎo)的所有 麻煩。您將發(fā)現(xiàn)內(nèi)核調(diào)試比以往更為快速和簡單。