揭開Linux系統(tǒng)內(nèi)核調(diào)試器神秘面紗
Linux 內(nèi)核調(diào)試器(KDB)允許您調(diào)試 Linux 內(nèi)核。kgdb 程序(使用 gdb 的遠(yuǎn)程主機(jī) Linux 內(nèi)核調(diào)試器)提供了一種使用 gdb 調(diào)試 Linux 內(nèi)核的機(jī)制。kgdb 程序是內(nèi)核的擴(kuò)展,它讓您能夠在遠(yuǎn)程主機(jī)上運(yùn)行 gdb 時連接到運(yùn)行
多數(shù) Linux 分發(fā)版包含一個 Electric Fence 包,不過您也可以選擇下載它。Electric Fence 是一個由 Bruce Perens 編寫的 malloc() 調(diào)試庫。它就在您分配內(nèi)存后分配受保護(hù)的內(nèi)存。如果存在 fencepost 錯誤(超過數(shù)組末尾運(yùn)行),程序就會產(chǎn)生保護(hù)錯誤,并立即結(jié)束。通過結(jié)合 Electric Fence 和 gdb,您可以精確地跟蹤到哪一行試圖訪問受保護(hù)內(nèi)存。 Electric Fence 的另一個功能就是能夠檢測內(nèi)存泄漏。
strace 命令是一種強(qiáng)大的工具,它能夠顯示所有由用戶空間程序發(fā)出的系統(tǒng)調(diào)用。strace 顯示這些調(diào)用的參數(shù)并返回符號形式的值。 strace 從內(nèi)核接收信息,而且不需要以任何特殊的方式來構(gòu)建內(nèi)核。將跟蹤信息發(fā)送到應(yīng)用程序及內(nèi)核開發(fā)者都很有用。在清單 6 中,分區(qū)的一種格式有錯誤,清單顯示了 strace 的開頭部分,內(nèi)容是關(guān)于調(diào)出創(chuàng)建文件系統(tǒng)操作(mkfs)的。strace 確定哪個調(diào)用導(dǎo)致問題出現(xiàn)。
清單 6. mkfs 上 strace 的開頭部分
[code:1:95b8e28830]execve("/sbin/mkfs.jfs", ["mkfs.jfs", "-f", "/dev/test1"], &
...
open("/dev/test1", O_RDWR|O_LARGEFILE) = 4
stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0
ioctl(4, 0x40041271, 0xbfffe128) = -1 EINVAL (Invalid argument)
write(2, "mkfs.jfs: warning - cannot setb" ..., 98mkfs.jfs: warning -
cannot set blocksize on block device /dev/test1: Invalid argument )
= 98
stat64("/dev/test1", {st_mode=&, st_rdev=makedev(63, 255), ...}) = 0
open("/dev/test1", O_RDONLY|O_LARGEFILE) = 5
ioctl(5, 0x80041272, 0xbfffe124) = -1 EINVAL (Invalid argument)
write(2, "mkfs.jfs: can''t determine device"..., ..._exit(1)
= ?[/code:1:95b8e28830]
清單 6 顯示 ioctl 調(diào)用導(dǎo)致用來格式化分區(qū)的 mkfs 程序失敗。ioctl BLKGETSIZE64 失敗。(BLKGET- SIZE64 在調(diào)用 ioctl 的源代碼中定義。) BLKGETSIZE64 ioctl 將被添加到 Linux 中所有的設(shè)備,而在這里,邏輯卷管理器還不支持它。因此,如果 BLKGETSIZE64 ioctl 調(diào)用失敗,mkfs 代碼將改為調(diào)用較早的 ioctl 調(diào)用;這使得 mkfs 適用于邏輯卷管理器。
[b:627becdd94][size=18:627becdd94] 第 3 種情況:使用 gdb 和 Oops[/size:627becdd94][/b:627becdd94]
您可以從命令行使用 gdb 程序(Free Software Foundation 的調(diào)試器)來找出錯誤,也可以從諸如 Data Display Debugger(DDD)這樣的幾個圖形工具之一使用 gdb 程序來找出錯誤。您可以使用 gdb 來調(diào)試用戶空間程序或 Linux 內(nèi)核。這一部分只討論從命令行運(yùn)行 gdb 的情況。
使用 gdb program name 命令啟動 gdb。gdb 將載入可執(zhí)行程序符號并顯示輸入提示符,讓您可以開始使用調(diào)試器。您可以通過三種方式用 gdb 查看進(jìn)程:
使用 attach 命令開始查看一個已經(jīng)運(yùn)行的進(jìn)程;attach 將停止進(jìn)程。
使用 run 命令執(zhí)行程序并從頭開始調(diào)試程序。
查看已有的核心文件來確定進(jìn)程終止時的狀態(tài)。要查看核心文件,請用下面的命令啟動 gdb。
gdb programname corefilename
要用核心文件進(jìn)行調(diào)試,您不僅需要程序的可執(zhí)行文件和源文件,還需要核心文件本身。要用核心文件啟動 gdb,請使用 -c 選項:
gdb -c core programname
gdb 顯示哪行代碼導(dǎo)致程序發(fā)生核心轉(zhuǎn)儲。
在運(yùn)行程序或連接到已經(jīng)運(yùn)行的程序之前,請列出您覺得有錯誤的源代碼,設(shè)置斷點(diǎn),然后開始調(diào)試程序。您可以使用 help 命令查看全面的 gdb 在線幫助和詳細(xì)的教程。
kgdb 程序(使用 gdb 的遠(yuǎn)程主機(jī) Linux 內(nèi)核調(diào)試器)提供了一種使用 gdb 調(diào)試 Linux 內(nèi)核的機(jī)制。kgdb 程序是內(nèi)核的擴(kuò)展,它讓您能夠在遠(yuǎn)程主機(jī)上運(yùn)行 gdb 時連接到運(yùn)行用 kgdb 擴(kuò)展的內(nèi)核機(jī)器。您可以接著深入到內(nèi)核中、設(shè)置斷點(diǎn)、檢查數(shù)據(jù)并進(jìn)行其它操作(類似于您在應(yīng)用程序上使用 gdb 的方式)。這個補(bǔ)丁的主要特點(diǎn)之一就是運(yùn)行 gdb 的主機(jī)在引導(dǎo)過程中連接到目標(biāo)機(jī)器(運(yùn)行要被調(diào)試的內(nèi)核)。這讓您能夠盡早開始調(diào)試。請注意,補(bǔ)丁為 Linux 內(nèi)核添加了功能,所以 gdb 可以用來調(diào)試 Linux 內(nèi)核。
使用 kgdb 需要兩臺機(jī)器:一臺是開發(fā)機(jī)器,另一臺是測試機(jī)器。一條串行線(空調(diào)制解調(diào)器電纜)將通過機(jī)器的串口連接它們。您希望調(diào)試的內(nèi)核在測試機(jī)器上運(yùn)行;gdb 在開發(fā)機(jī)器上運(yùn)行。gdb 使用串行線與您要調(diào)試的內(nèi)核通信。
請遵循下面的步驟來設(shè)置 kgdb 調(diào)試環(huán)境:
1.下載您的 Linux 內(nèi)核版本適用的補(bǔ)丁。
2.將組件構(gòu)建到內(nèi)核,因為這是使用 kgdb 最簡單的方法。(請注意,有兩種方法可以構(gòu)建多數(shù)內(nèi)核組件,比如作為模塊或直接構(gòu)建到內(nèi)核中。舉例來說,日志紀(jì)錄文件系統(tǒng)(Journaled File System,JFS)可以作為模塊構(gòu)建,或直接構(gòu)建到內(nèi)核中。通過使用 gdb 補(bǔ)丁,我們就可以將 JFS 直接構(gòu)建到內(nèi)核中。)
3.應(yīng)用內(nèi)核補(bǔ)丁并重新構(gòu)建內(nèi)核。
{{分頁}}
4.創(chuàng)建一個名為 .gdbinit 的文件,并將其保存在內(nèi)核源文件子目錄中(換句話說就是 /usr/src/linux)。文件 .gdbinit 中有下面四行代碼:
[code:1:627becdd94]oset remotebaud 115200
osymbol-file vmlinux
otarget remote /dev/ttyS0
oset output-radix 16 [/code:1:627becdd94]
5.將 append=gdb 這一行添加到 lilo,lilo 是用來在引導(dǎo)內(nèi)核時選擇使用哪個內(nèi)核的引導(dǎo)載入程序。
[code:1:627becdd94]oimage=/boot/bzImage-2.4.17
olabel=gdb2417
oread-only
oroot=/dev/sda8
oappend="gdb gdbttyS=1 gdb-baud=115200 nmi_watchdog=0" [/code:1:627becdd94]
清單 7 是一個腳本示例,它將您在開發(fā)機(jī)器上構(gòu)建的內(nèi)核和模塊引入測試機(jī)器。您需要修改下面幾項:
.best@sfb:用戶標(biāo)識和機(jī)器名。
./usr/src/linux-2.4.17:內(nèi)核源代碼樹的目錄。
.bzImage-2.4.17:測試機(jī)器上將引導(dǎo)的內(nèi)核名。
.rcp 和 rsync:必須允許它在構(gòu)建內(nèi)核的機(jī)器上運(yùn)行。
清單 7. 引入測試機(jī)器的內(nèi)核和模塊的腳本
[code:1:627becdd94]set -x
rcp best@sfb: /usr/src/linux-2.4.17/arch/i386/boot/bzImage /boot/bzImage-2.4.17
rcp best@sfb:/usr/src/linux-2.4.17/System.map /boot/System.map-2.4.17
rm -rf /lib/modules/2.4.17
rsync -a best@sfb:/lib/modules/2.4.17 /lib/modules
chown -R root /lib/modules/2.4.17
lilo[/code:1:627becdd94]
現(xiàn)在我們可以通過改為使用內(nèi)核源代碼樹開始的目錄來啟動開發(fā)機(jī)器上的 gdb 程序了。在本示例中,內(nèi)核源代碼樹位于 /usr/src/linux-2.4.17。輸入 gdb 啟動程序。
如果一切正常,測試機(jī)器將在啟動過程中停止。輸入 gdb 命令 cont 以繼續(xù)啟動過程。一個常見的問題是,空調(diào)制解調(diào)器電纜可能會被連接到錯誤的串口。如果 gdb 不啟動,將端口改為第二個串口,這會使 gdb 啟動。
使用 kgdb 調(diào)試內(nèi)核問題
清單 8 列出了 jfs_mount.c 文件的源代碼中被修改過的代碼,我們在代碼中創(chuàng)建了一個空指針異常,從而使代碼在第 109 行產(chǎn)生段錯誤。
清單 8. 修改過后的 jfs_mount.c 代碼
[code:1:627becdd94]int jfs_mount(struct super_block *sb)
{
...
int ptr; /* line 1 added */
jFYI(1, ("
Mount JFS
"));
/ *
* read/validate superblock
* (initialize mount inode from the superblock)
* /
if ((rc = chkSuper(sb))) {
goto errout20;
}
108 ptr=0; /* line 2 added */
109 printk("%d
",*ptr); /* line 3 added */[/code:1:627becdd94]
{{分頁}}
清單 9 在向文件系統(tǒng)發(fā)出 mount 命令之后顯示一個 gdb 異常。kgdb 提供了幾條命令,如顯示數(shù)據(jù)結(jié)構(gòu)和變量值以及顯示系統(tǒng)中的所有任務(wù)處于什么狀態(tài)、它們駐留在何處、它們在哪些地方使用了 CPU 等等。清單 9 將顯示回溯跟蹤為該問題提供的信息;where 命令用來執(zhí)行反跟蹤,它將告訴被執(zhí)行的調(diào)用在代碼中的什么地方停止。
清單 9. gdb 異常和反跟蹤
[code:1:627becdd94]mount -t jfs /dev/sdb /jfs
Program received signal SIGSEGV, Segmentation fault.
jfs_mount (sb=0xf78a3800) at jfs_mount.c:109
109 printk("%d
",*ptr);
(gdb)where
#0 jfs_mount (sb=0xf78a3800) at jfs_mount.c:109
#1 0xc01a0dbb in jfs_read_super ... at super.c:280
#2 0xc0149ff5 in get_sb_bdev ... at super.c:620
#3 0xc014a89f in do_kern_mount ... at super.c:849
#4 0xc0160e66 in do_add_mount ... at namespace.c:569
#5 0xc01610f4 in do_mount ... at namespace.c:683
#6 0xc01611ea in sys_mount ... at namespace.c:716
#7 0xc01074a7 in system_call () at af_packet.c:1891
#8 0x0 in ?? ()
(gdb)[/code:1:627becdd94]
下一部分還將討論這個相同的 JFS 段錯誤問題,但不設(shè)置調(diào)試器,如果您在非 kgdb 內(nèi)核環(huán)境中執(zhí)行清單 8 中的代碼,那么它使用內(nèi)核可能生成的 Oops 消息。
[color=darkblue:627becdd94]Oops 分析[/color:627becdd94]
Oops(也稱 panic,慌張)消息包含系統(tǒng)錯誤的細(xì)節(jié),如 CPU 寄存器的內(nèi)容。在 Linux 中,調(diào)試系統(tǒng)崩潰的傳統(tǒng)方法是分析在發(fā)生崩潰時發(fā)送到系統(tǒng)控制臺的 Oops 消息。一旦您掌握了細(xì)節(jié),就可以將消息發(fā)送到 ksymoops 實用程序,它將試圖將代碼轉(zhuǎn)換為指令并將堆棧值映射到內(nèi)核符號。在很多情況下,這些信息就足夠您確定錯誤的可能原因是什么了。請注意,Oops 消息并不包括核心文件。
讓我們假設(shè)系統(tǒng)剛剛創(chuàng)建了一條 Oops 消息。作為編寫代碼的人,您希望解決問題并確定什么導(dǎo)致了 Oops 消息的產(chǎn)生,或者您希望向顯示了 Oops 消息的代碼的開發(fā)者提供有關(guān)您的問題的大部分信息,從而及時地解決問題。Oops 消息是等式的一部分,但如果不通過 ksymoops 程序運(yùn)行它也于事無補(bǔ)。下面的圖顯示了格式化 Oops 消息的過程。
[color=darkblue:627becdd94]格式化 Oops 消息[/color:627becdd94]
ksymoops 需要幾項內(nèi)容:Oops 消息輸出、來自正在運(yùn)行的內(nèi)核的 System.map 文件,還有 /proc/ksyms、 vmlinux 和 /proc/modules。關(guān)于如何使用 ksymoops,內(nèi)核源代碼 /usr/src/linux/Documentation/oops-tracing.txt 中或 ksymoops 手冊頁上有完整的說明可以參考。Ksymoops 反匯編代碼部分,指出發(fā)生錯誤的指令,并顯示一個跟蹤部分表明代碼如何被調(diào)用。
首先,將 Oops 消息保存在一個文件中以便通過 ksymoops 實用程序運(yùn)行它。清單 10 顯示了由安裝 JFS 文件系統(tǒng)的 mount 命令創(chuàng)建的 Oops 消息,問題是由清單 8 中添加到 JFS 安裝代碼的那三行代碼產(chǎn)生的。
清單 10. ksymoops 處理后的 Oops 消息
[code:1:627becdd94]ksymoops 2.4.0 on i686 2.4.17. Options used
... 15:59:37 sfb1 kernel: Unable to handle kernel NULL pointer dereference at
virtual address 0000000
... 15:59:37 sfb1 kernel: c01588fc
... 15:59:37 sfb1 kernel: *pde = 0000000
... 15:59:37 sfb1 kernel: Oops: 0000
... 15:59:37 sfb1 kernel: CPU: 0
... 15:59:37 sfb1 kernel: EIP: 0010:[jfs_mount+60/704]
... 15:59:37 sfb1 kernel: Call Trace: [jfs_read_super+287/688]
[get_sb_bdev+563/736] [do_kern_mount+189/336] [do_add_mount+35/208]
[do_page_fault+0/1264]
... 15:59:37 sfb1 kernel: Call Trace: []...
... 15:59:37 sfb1 kernel: [
... 15:59:37 sfb1 kernel: Code: 8b 2d 00 00 00 00 55 ...
>>EIP; c01588fc <=====
...
Trace; c0106cf3
Code; c01588fc
00000000 <_EIP>:
Code; c01588fc <=====
0: 8b 2d 00 00 00 00 mov 0x0,%ebp <=====
Code; c0158902
6: 55 push %ebp[/code:1:627becdd94]
{{分頁}}
接下來,您要確定 jfs_mount 中的哪一行代碼引起了這個問題。Oops 消息告訴我們問題是由位于偏移地址 3c 的指令引起的。做這件事的辦法之一是對 jfs_mount.o 文件使用 objdump 實用程序,然后查看偏移地址 3c。Objdump 用來反匯編模塊函數(shù),看看您的 C 源代碼會產(chǎn)生什么匯編指令。清單 11 顯示了使用 objdump 后您將看到的內(nèi)容,接著,我們查看 jfs_mount 的 C 代碼,可以看到空值是第 109 行引起的。偏移地址 3c 之所以很重要,是因為 Oops 消息將該處標(biāo)識為引起問題的位置。
清單 11. jfs_mount 的匯編程序清單
[code:1:627becdd94]109 printk("%d
",*ptr);
objdump jfs_mount.o
jfs_mount.o: file format elf32-i386
Disassembly of section .text:
00000000 :
0:55 push %ebp
...
2c: e8 cf 03 00 00 call 400
31: 89 c3 mov %eax,%ebx
33: 58 pop %eax
34: 85 db test %ebx,%ebx
36: 0f 85 55 02 00 00 jne 291
3c: 8b 2d 00 00 00 00 mov 0x0,%ebp << problem line above
42: 55 push %ebp[/code:1:627becdd94]
[color=darkblue:627becdd94]kdb[/color:627becdd94]
Linux 內(nèi)核調(diào)試器(Linux kernel debugger,kdb)是 Linux 內(nèi)核的補(bǔ)丁,它提供了一種在系統(tǒng)能運(yùn)行時對內(nèi)核內(nèi)存和數(shù)據(jù)結(jié)構(gòu)進(jìn)行檢查的辦法。請注意,kdb 不需要兩臺機(jī)器,不過它也不允許您像 kgdb 那樣進(jìn)行源代碼級別上的調(diào)試。您可以添加額外的命令,給出該數(shù)據(jù)結(jié)構(gòu)的標(biāo)識或地址,這些命令便可以格式化和顯示基本的系統(tǒng)數(shù)據(jù)結(jié)構(gòu)。目前的命令集允許您控制包括以下操作在內(nèi)的內(nèi)核操作:
處理器單步執(zhí)行
執(zhí)行到某條特定指令時停止
當(dāng)存取(或修改)某個特定的虛擬內(nèi)存位置時停止
當(dāng)存取輸入/輸出地址空間中的寄存器時停止
對當(dāng)前活動的任務(wù)和所有其它任務(wù)進(jìn)行堆?;厮莞?通過進(jìn)程 ID)
對指令進(jìn)行反匯編
[color=blue:627becdd94]追擊內(nèi)存溢出[/color:627becdd94]
您肯定不想陷入類似在幾千次調(diào)用之后發(fā)生分配溢出這樣的情形。
我們的小組花了許許多多時間來跟蹤稀奇古怪的內(nèi)存錯誤問題。應(yīng)用程序在我們的開發(fā)工作站上能運(yùn)行,但在新的產(chǎn)品工作站上,這個應(yīng)用程序在調(diào)用 malloc() 兩百萬次之后就不能運(yùn)行了。真正的問題是在大約一百萬次調(diào)用之后發(fā)生了溢出。新系統(tǒng)之所有存在這個問題,是因為被保留的 malloc() 區(qū)域的布局有所不同,從而這些零散內(nèi)存被放置在了不同的地方,在發(fā)生溢出時破壞了一些不同的內(nèi)容。
我們用多種不同技術(shù)來解決這個問題,其中一種是使用調(diào)試器,另一種是在源代碼中添加跟蹤功能。在我職業(yè)生涯的大概也是這個時候,我便開始關(guān)注內(nèi)存調(diào)試工具,希望能更快更有效地解決這些類型的問題。在開始一個新項目時,我最先做的事情之一就是運(yùn)行 MEMWATCH 和 YAMD,看看它們是不是會指出內(nèi)存管理方面的問題。
內(nèi)存泄漏是應(yīng)用程序中常見的問題,不過您可以使用本文所講述的工具來解決這些問題。
評論