今天同事在處理一個陌生的模組時遇到kernel panic,Linux印出了backtrace,同事大致上可以知道是在哪個function中,但該function的長度頗長,短時間無法定位在哪個位置,在這種情況下,要如何收斂除錯範圍呢?更糟的是,由於加入printk會改變模組行為,所以printk基本上無法拿來檢查參數的值是否正常。一般這樣的問題會backtrace的資訊來著手。從這個資訊我們可以知道在function中的多少offset發生錯誤,以x86為例(從LDD3借來的例子):
Unable to handle kernel NULL pointer dereference at virtual address 00000000
printing eip:
d083a064
Oops: 0002 [#1]
SMP
CPU: 0
EIP: 0060:[<d083a064>] Not tainted
EFLAGS: 00010246 (2.6.6)
EIP is at faulty_write+0x4/0x10 [faulty]
eax: 00000000 ebx: 00000000 ecx: 00000000 edx: 00000000
esi: cf8b2460 edi: cf8b2480 ebp: 00000005 esp: c31c5f74
ds: 007b es: 007b ss: 0068
Process bash (pid: 2086, threadinfo=c31c4000 task=cfa0a6c0)
Stack: c0150558 cf8b2460 080e9408 00000005 cf8b2480 00000000 cf8b2460 cf8b2460
fffffff7 080e9408 c31c4000 c0150682 cf8b2460 080e9408 00000005 cf8b2480
00000000 00000001 00000005 c0103f8f 00000001 080e9408 00000005 00000005
Call Trace:
[<c0150558>] vfs_write+0xb8/0x130
[<c0150682>] sys_write+0x42/0x70
[<c0103f8f>] syscall_call+0x7/0xb
我們便知道在faulty_write的offset為0x4的位址發生了錯誤,此時用objdump -d,將該模組反組譯,觀察該位址應該就可以猜出來是怎麼一回事。
真的這麼簡單嗎?同事的困難就在於該function很長,並且offset的位址處於約function中間的位址,前後所用的其他function一堆,在kernel有開較高層級的最佳化情況下,部份function被inline進來,使得連是不是在backtrace所指名的function中都是問題。
面對這個困難,我們可以用幾個絕對不會被inline的function去標示function中的不同位址,藉此收斂定位錯誤的位置。在gcc中,只要在function的signature給予__attribute__((noinline))提示即可,而Linux也有再包裝過一層:
//you should include linux/compiler.h to use this
#define noinline __attribute__((noinline))
然後在發生錯誤的function裡的不同位置插入此類function,便可以在asm中看到:
...
bl not_inline_f1
...
str r0 [r3] <-- address of panic!!
...
bl not_inline_f2
...
由於function call的影響較printk低,於是,便幾乎可以在不改變程式行為的情況下知道是在哪段程式碼中出包啦~這時再逐一檢查該小範圍中的記憶體使用,就抓到了錯誤的來源。(或許你會想看一下include/linux/compiler.h,其中有不少有趣的compiler extensions可用,有的適合除錯,有的則可以改善效能,部份extensions運作的基本原理可以在"淺談GCC編譯技術"看到一點概念)。
Unable to handle kernel NULL pointer dereference at virtual address 00000000
printing eip:
d083a064
Oops: 0002 [#1]
SMP
CPU: 0
EIP: 0060:[<d083a064>] Not tainted
EFLAGS: 00010246 (2.6.6)
EIP is at faulty_write+0x4/0x10 [faulty]
eax: 00000000 ebx: 00000000 ecx: 00000000 edx: 00000000
esi: cf8b2460 edi: cf8b2480 ebp: 00000005 esp: c31c5f74
ds: 007b es: 007b ss: 0068
Process bash (pid: 2086, threadinfo=c31c4000 task=cfa0a6c0)
Stack: c0150558 cf8b2460 080e9408 00000005 cf8b2480 00000000 cf8b2460 cf8b2460
fffffff7 080e9408 c31c4000 c0150682 cf8b2460 080e9408 00000005 cf8b2480
00000000 00000001 00000005 c0103f8f 00000001 080e9408 00000005 00000005
Call Trace:
[<c0150558>] vfs_write+0xb8/0x130
[<c0150682>] sys_write+0x42/0x70
[<c0103f8f>] syscall_call+0x7/0xb
我們便知道在faulty_write的offset為0x4的位址發生了錯誤,此時用objdump -d,將該模組反組譯,觀察該位址應該就可以猜出來是怎麼一回事。
真的這麼簡單嗎?同事的困難就在於該function很長,並且offset的位址處於約function中間的位址,前後所用的其他function一堆,在kernel有開較高層級的最佳化情況下,部份function被inline進來,使得連是不是在backtrace所指名的function中都是問題。
面對這個困難,我們可以用幾個絕對不會被inline的function去標示function中的不同位址,藉此收斂定位錯誤的位置。在gcc中,只要在function的signature給予__attribute__((noinline))提示即可,而Linux也有再包裝過一層:
//you should include linux/compiler.h to use this
#define noinline __attribute__((noinline))
然後在發生錯誤的function裡的不同位置插入此類function,便可以在asm中看到:
...
bl not_inline_f1
...
str r0 [r3] <-- address of panic!!
...
bl not_inline_f2
...
由於function call的影響較printk低,於是,便幾乎可以在不改變程式行為的情況下知道是在哪段程式碼中出包啦~這時再逐一檢查該小範圍中的記憶體使用,就抓到了錯誤的來源。(或許你會想看一下include/linux/compiler.h,其中有不少有趣的compiler extensions可用,有的適合除錯,有的則可以改善效能,部份extensions運作的基本原理可以在"淺談GCC編譯技術"看到一點概念)。
留言
張貼留言