Linux內核調試器內幕4
——
1.下載您的 Linux 內核版本適用的補丁。
2.將組件構建到內核,因為這是使用 kgdb 最簡單的方法。(請注意,有兩種方法可以構建多數(shù)內核組件,比如作為模塊或直接構建到內核中。舉例來說,日志紀錄文件系統(tǒng)(Journaled File System,JFS)可以作為模塊構建,或直接構建到內核中。通過使用 gdb 補丁,我們就可以將 JFS 直接構建到內核中。)
3.應用內核補丁并重新構建內核。
4.創(chuàng)建一個名為 .gdbinit 的文件,并將其保存在內核源文件子目錄中(換句話說就是 /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 是用來在引導內核時選擇使用哪個內核的引導載入程序。
[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ā)機器上構建的內核和模塊引入測試機器。您需要修改下面幾項:
?best@sfb:用戶標識和機器名。
?/usr/src/linux-2.4.17:內核源代碼樹的目錄。
?bzImage-2.4.17:測試機器上將引導的內核名。
?rcp 和 rsync:必須允許它在構建內核的機器上運行。
清單 7. 引入測試機器的內核和模塊的腳本
[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)在我們可以通過改為使用內核源代碼樹開始的目錄來啟動開發(fā)機器上的 gdb 程序了。在本示例中,內核源代碼樹位于 /usr/src/linux-2.4.17。輸入 gdb 啟動程序。
如果一切正常,測試機器將在啟動過程中停止。輸入 gdb 命令 cont 以繼續(xù)啟動過程。一個常見的問題是,空調制解調器電纜可能會被連接到錯誤的串口。如果 gdb 不啟動,將端口改為第二個串口,這會使 gdb 啟動。
[color=darkblue:627becdd94]使用 kgdb 調試內核問題[/color:627becdd94]
清單 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ù)結構和變量值以及顯示系統(tǒng)中的所有任務處于什么狀態(tài)、它們駐留在何處、它們在哪些地方使用了 CPU 等等。清單 9 將顯示回溯跟蹤為該問題提供的信息;where 命令用來執(zhí)行反跟蹤,它將告訴被執(zhí)行的調用在代碼中的什么地方停止。
清單 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 段錯誤問題,但不設置調試器,如果您在非 kgdb 內核環(huán)境中執(zhí)行清單 8 中的代碼,那么它使用內核可能生成的 Oops 消息。
[color=darkblue:627becdd94]Oops 分析[/color:627becdd94]
Oops(也稱 panic,慌張)消息包含系統(tǒng)錯誤的細節(jié),如 CPU 寄存器的內容。在 Linux 中,調試系統(tǒng)崩潰的傳統(tǒng)方法是分析在發(fā)生崩潰時發(fā)送到系統(tǒng)控制臺的 Oops 消息。一旦您掌握了細節(jié),就可以將消息發(fā)送到 ksymoops 實用程序,它將試圖將代碼轉換為指令并將堆棧值映射到內核符號。在很多情況下,這些信息就足夠您確定錯誤的可能原因是什么了。請注意,Oops 消息并不包括核心文件。
讓我們假設系統(tǒng)剛剛創(chuàng)建了一條 Oops 消息。作為編寫代碼的人,您希望解決問題并確定什么導致了 Oops 消息的產(chǎn)生,或者您希望向顯示了 Oops 消息的代碼的開發(fā)者提供有關您的問題的大部分信息,從而及時地解決問題。Oops 消息是等式的一部分,但如果不通過 ksymoops 程序運行它也于事無補。下面的圖顯示了格式化 Oops 消息的過程。
[color=darkblue:627becdd94]格式化 Oops 消息[/color:627becdd94]見附圖
ksymoops 需要幾項內容:Oops 消息輸出、來自正在運行的內核的 System.map 文件,還有 /proc/ksyms、 vmlinux 和 /proc/modules。關于如何使用 ksymoops,內核源代碼 /usr/src/linux/Documentation/oops-tracing.txt 中或 ksymoops 手冊頁上有完整的說明可以參考。Ksymoops 反匯編代碼部分,指出發(fā)生錯誤的指令,并顯示一個跟蹤部分表明代碼如何被調用。
首先,將 Oops 消息保存在一個文件中以便通過 ksymoops 實用程序運行它。清單 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: [<c0155d4f>]...
... 15:59:37 sfb1 kernel: [<c0106e04 ...
... 15:59:37 sfb1 kernel: Code: 8b 2d 00 00 00 00 55 ...
>>EIP; c01588fc <jfs_mount+3c/2c0> <=====
...
Trace; c0106cf3 <system_call+33/40>
Code; c01588fc <jfs_mount+3c/2c0>
00000000 <_EIP>:
Code; c01588fc <jfs_mount+3c/2c0> <=====
0: 8b 2d 00 00 00 00 mov 0x0,%ebp <=====
Code; c0158902 <jfs_mount+42/2c0>
6: 55 push %ebp[/code:1:627becdd94]
接下來,您要確定 jfs_mount 中的哪一行代碼引起了這個問題。Oops 消息告訴我們問題是由位于偏移地址 3c 的指令引起的。做這件事的辦法之一是對 jfs_mount.o 文件使用 objdump 實用程序,然后查看偏移地址 3c。Objdump 用來反匯編模塊函數(shù),看看您的 C 源代碼會產(chǎn)生什么匯編指令。清單 11 顯示了使用 objdump 后您將看到的內容,接著,我們查看 jfs_mount 的 C 代碼,可以看到空值是第 109 行引起的。偏移地址 3c 之所以很重要,是因為 Oops 消息將該處標識為引起問題的位置。
清單 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 <jfs_mount>:
0:55 push %ebp
...
2c: e8 cf 03 00 00 call 400 <chkSuper>
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 <jfs_mount+0x291>
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 內核調試器(Linux kernel debugger,kdb)是 Linux 內核的補丁,它提供了一種在系統(tǒng)能運行時對內核內存和數(shù)據(jù)結構進行檢查的辦法。請注意,kdb 不需要兩臺機器,不過它也不允許您像 kgdb 那樣進行源代碼級別上的調試。您可以添加額外的命令,給出該數(shù)據(jù)結構的標識或地址,這些命令便可以格式化和顯示基本的系統(tǒng)數(shù)據(jù)結構。目前的命令集允許您控制包括以下操作在內的內核操作:
?處理器單步執(zhí)行
?執(zhí)行到某條特定指令時停止
?當存?。ɑ蛐薷模┠硞€特定的虛擬內存位置時停止
?當存取輸入/輸出地址空間中的寄存器時停止
?對當前活動的任務和所有其它任務進行堆?;厮莞櫍ㄍㄟ^進程 ID)
?對指令進行反匯編
[color=blue:627becdd94]追擊內存溢出[/color:627becdd94]
您肯定不想陷入類似在幾千次調用之后發(fā)生分配溢出這樣的情形。
我們的小組花了許許多多時間來跟蹤稀奇古怪的內存錯誤問題。應用程序在我們的開發(fā)工作站上能運行,但在新的產(chǎn)品工作站上,這個應用程序在調用 malloc() 兩百萬次之后就不能運行了。真正的問題是在大約一百萬次調用之后發(fā)生了溢出。新系統(tǒng)之所有存在這個問題,是因為被保留的 malloc() 區(qū)域的布局有所不同,從而這些零散內存被放置在了不同的地方,在發(fā)生溢出時破壞了一些不同的內容。
我們用多種不同技術來解決這個問題,其中一種是使用調試器,另一種是在源代碼中添加跟蹤功能。在我職業(yè)生涯的大概也是這個時候,我便開始關注內存調試工具,希望能更快更有效地解決這些類型的問題。在開始一個新項目時,我最先做的事情之一就是運行 MEMWATCH 和 YAMD,看看它們是不是會指出內存管理方面的問題。
評論