Linux kernel OOPS解析:

=============================================================================

原文地址:http://blog.micro-studios.com/?p=615#comment-1069

看後感想:這點比 ldd3上講的都仔細

2012年11月29日11:24:17:有BUG_ON就不用反彙編了。。。

2012年11月30日11:14:13:回調函數跟丟了

我遇到的情況:http://my.csdn.net/my/code/detail/28858

=============================================================================

Oops 信息來源及格式
Oops 這個單詞含義爲“驚訝”
,當內核出錯時(比如訪問非法地址)打印出來的信息被
稱爲 Oops 信息。
Oops 信息包含以下幾部分內容。
1 一段文本描述信息。
比如類似“Unable to handle kernel NULL pointer dereference at virtual address 00000000”
的信息,它說明了發生的是哪類錯誤。
2 Oops 信息的序號。
比如是第 1 次、第 2 次等。這些信息與下面類似,中括號內的數據表示序號。
Internal error: Oops: 805 [#1]
3 內核中加載的模塊名稱,也可能沒有,以下面字樣開頭。
Modules linked in:
4 發生錯誤的 CPU 的序號,對於單處理器的系統,序號爲 0,比如:
CPU: 0
Not tainted (2.6.22.6 #36)
5 發生錯誤時 CPU 的各個寄存器值。
6 當前進程的名字及進程 ID,比如:
Process swapper (pid: 1, stack limit = 0xc0480258)
這並不是說發生錯誤的是這個進程,而是表示發生錯誤時,當前進程是它。錯誤可能發
生在內核代碼、驅動程序,也可能就是這個進程的錯誤。
7 棧信息。
8 棧回溯信息,可以從中看出函數調用關係,形式如下:
Backtrace:
[] (s3c2410fb_probe+0x0/0x560) from [] (platform_drv_
probe+0x20/0x24)

9 出錯指令附近的指令的機器碼,比如(出錯指令在小括號裏)
:
Code: e24cb004 e24dd010 e59f34e0 e3a07000 (e5873000)

配置內核使 Oops 信息的棧回溯信息更直觀
Linux 2.6.22 自身具備的調試功能,可以使得打印出的 Oops 信息更直觀。通過 Oops 信
息中 PC 寄存器的值可以知道出錯指令的地址,通過棧回溯信息可以知道出錯時的函數調用
關係,根據這兩點可以很快定位錯誤。
要讓內核出錯時能夠打印棧回溯信息,編譯內核時要增加“-fno-omit-frame-pointer”選
項,這可以通過配置 CONFIG_FRAME_POINTER 來實現。查看內核目錄下的配置文件.config,
確保 CONFIG_FRAME_POINTER 已經被定義,如果沒有,執行“make menuconfig”命令重
新配置內核。CONFIG_FRAME_POINTER 有可能被其他配置項自動選上。
18.3.3
使用 Oops 信息調試內核的實例
1.獲得 Oops 信息
本小節故意修改 LCD 驅動程序 drivers/video/s3c2410fb.c,加入錯誤代碼:在 s3c2410fb_
probe 函數的開頭增加下面兩條代碼:
int *ptest = NULL;
*ptest = 0x1234;
重新編譯內核,啓動後會出錯並打印出如下 Oops 信息:
Unable to handle kernel NULL pointer dereference at virtual address 00000000
pgd = c0004000
[00000000] *pgd=00000000
Internal error: Oops: 805 [#1]
Modules linked in:
CPU: 0
Not tainted (2.6.22.6 #36)
PC is at s3c2410fb_probe+0x18/0x560
LR is at platform_drv_probe+0x20/0x24
pc : []
lr : []
psr: a0000013
sp : c0481e64 ip : c0481ea0 fp : c0481e9c
r10: 00000000 r9 : c0024864 r8 : c03c420c
r7 : 00000000 r6 : c0389a3c r5 : 00000000 r4 : c036256c
r3 : 00001234 r2 : 00000001 r1 : c04c0fc4 r0 : c0362564
Flags: NzCv IRQs on FIQs on Mode SVC_32 Segment kernel
Control: c000717f Table: 30004000 DAC: 00000017
Process swapper (pid: 1, stack limit = 0xc0480258)
Stack: (0xc0481e64 to 0xc0482000)
1e60:c02b1f70 00000020 c03625d4 c036256c c036256c 00000000 c0389a3c
1e80: c0389a3c c03c420c c0024864 00000000 c0481eac c0481ea0 c01bf4e8 c001a704
1ea0: c0481ed0 c0481eb0 c01bd5a8 c01bf4d8 c0362644 c036256c c01bd708 c0389a3c
1ec0: 00000000 c0481ee8 c0481ed4 c01bd788 c01bd4d0 00000000 c0481eec c0481f14
1ee0: c0481eec c01bc5a8 c01bd718 c038dac8 c038dac8 c03625b4 00000000 c0389a3c
1f00: c0389a44 c038d9dc c0481f24 c0481f18 c01bd808 c01bc568 c0481f4c c0481f28
1f20: c01bcd78 c01bd7f8 c0389a3c 00000000 00000000 c0480000 c0023ac8 00000000
1f40: c0481f60 c0481f50 c01bdc84 c01bcd0c 00000000 c0481f70 c0481f64 c01bf5fc
1f60: c01bdc14 c0481f80 c0481f74 c019479c c01bf5a0 c0481ff4 c0481f84 c0008c14
1f80: c0194798 e3c338ff e0222423 00000000 00000001 e2844004 00000000 00000000
1fa0: 00000000 c0481fb0 c002bf24 c0041328 00000000 00000000 c0008b40 c00476ec
1fc0: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
1fe0: 00000000 00000000 00000000 c0481ff8 c00476ec c0008b50 c03cdf50 c0344178
Backtrace:
[] (s3c2410fb_probe+0x0/0x560) from [] (platform_drv_
probe+0x20/0x24)
[] (platform_drv_probe+0x0/0x24) from [] (driver_probe_
device+0xe8/0x18c)
[] (driver_probe_device+0x0/0x18c) from [] (driver
attach+0x80/0xe0)
r8:00000000 r7:c0389a3c r6:c01bd708 r5:c036256c r4:c0362644
[] (
driver_attach+0x0/0xe0) from [] (bus_for_each
dev+0x50/0x84)
r5:c0481eec r4:00000000
[] (bus_for_each_dev+0x0/0x84) from [] (driver_attach+
0x20/0x28)
r7:c038d9dc r6:c0389a44 r5:c0389a3c r4:00000000
[] (driver_attach+0x0/0x28) from [] (bus_add_driver+
0x7c/0x1b4)
[] (bus_add_driver+0x0/0x1b4) from [] (driver_register+
0x80/0x88)
[] (driver_register+0x0/0x88) from [] (platform_driver_
register+0x6c/0x88)
r4:00000000
[] (platform_driver_register+0x0/0x88) from [] (s3c2410fb_
init+0x14/0x1c)
[] (s3c2410fb_init+0x0/0x1c) from [] (kernel_init+0xd4/
0x28c)
[] (kernel_init+0x0/0x28c) from [] (do_exit+0x0/0x760)
Code: e24cb004 e24dd010 e59f34e0 e3a07000 (e5873000)
Kernel panic - not syncing: Attempted to kill init!
分析 Oops 信息
(1)明確出錯原因。
由出錯信息“Unable to handle kernel NULL pointer dereference at virtual address 00000000”
可知內核是因爲非法地址訪問出錯,使用了空指針。
(2)根據棧回溯信息找出函數調用關係。
內核崩潰時,可以從 pc 寄存器得知崩潰發生時的函數、出錯指令。但是很多情況下,錯
誤有可能是它的調用者引入的,所以找出函數的調用關係也很重要。
部分棧回溯信息如下:
[] (s3c2410fb_probe+0x0/0x560) from [] (platform_drv_
probe+0x20/0x24)
這行信息分爲兩部分,
表示後面的 platform_drv_probe 函數調用了前面的 s3c2410fb_probe
函數。
前半部含義爲:
“c001a6f4”是 s3c2410fb_probe 函數首地址偏移 0 的地址,這個函數大
小爲 0x560。
後半部含義爲:
“c01bf4e8”是 platform_drv_probe 函數首地址偏移 0x20 的地址,這個函
數大小爲 0x24。
另外,後半部的“[]”表示 s3c2410fb_probe 執行後的返回地址。
對於類似下面的棧回溯信息,其中是 r8~r4 表示 driver_probe_device 函數剛被調用時這
些寄存器的值。
[] (driver_probe_device+0x0/0x18c) from [] (_driver
attach+0x80/0xe0)
r8:00000000 r7:c0389a3c r6:c01bd708 r5:c036256c r4:c0362644
從上面的棧回溯信息可以知道內核出錯時的函數調用關係如下,
最後在 s3c2410fb_probe
函數內部崩潰。
do_exit ->
kernel_init ->
s3c2410fb_init ->
platform_driver_register ->
driver_register ->
bus_add_driver ->
driver_attach ->
bus_for_each_dev ->
__driver_attach ->
driver_probe_device ->
platform_drv_probe ->
s3c2410fb_probe
(3)根據 pc 寄存器的值確定出錯位置。
上述 Oops 信息中出錯時的寄存器值如下:
PC is at s3c2410fb_probe+0x18/0x560
LR is at platform_drv_probe+0x20/0x24
pc : []
lr : []
psr: a0000013

“PC is at s3c2410fb_probe+0x18/0x560”表示出錯指令爲 s3c2410fb_probe 函數中偏移爲
0x18 的指令。
“pc : []”表示出錯指令的地址爲 c001a70c(十六進制)。
(4)結合內核源代碼和反彙編代碼定位問題。
先生成內核的反彙編代碼 vmlinux.dis,執行以下命令:
$ cd /work/system/linux-2.6.22.6
$ arm-linux-objdump -D vmlinux > vmlinux.dis
出錯地址 c001a70c 附近的部分彙編代碼如下:
c001a6f4 <s3c2410fb_probe>:
c001a6f4: e1a0c00d mov ip, sp
c001a6f8: e92ddff0 stmdb
c001a6fc: e24cb004 sub fp, ip, #4 ; 0x4
c001a700: e24dd010 sub sp, sp, #16 ; 0x10
c001a704: e59f34e0 ldr r3, [pc, #1248] ; c001abec <.init+0x1284c>
c001a708: e3a07000 mov r7, #0
c001a70c: e5873000 str r3, [r7]
c001a710: e59030fc ldr r3, [r0, #252]
sp!, {r4, r5, r6, r7, r8, r9, sl, fp, ip, lr, pc}
; 0x0
<===========出錯指令
出錯指令爲“str r3, [r7]”
,它把 r3 寄存器的值放到內存中,內存地址爲 r7 寄存器的值。
根據 Oops 信息中的寄存器值可知:r3 爲 0x00001234,r7 爲 0。0 地址不可訪問,所以出錯。
s3c2410fb_probe 函數的部分 C 代碼如下:
static int __init s3c2410fb_probe(struct platform_device *pdev)
{
struct s3c2410fb_info *info;
struct fb_info
*fbinfo;
struct s3c2410fb_hw *mregs;
int ret;
int irq;
int i;
u32 lcdcon1;
int *ptest = NULL;
*ptest = 0x1234;
mach_info = pdev->dev.platform_data;
結合反彙編代碼,很容易知道是“*ptest = 0x1234;”導致錯誤,其中的 ptest 爲空。
對於大多數情況,從反彙編代碼定位到 C 代碼並不會如此容易,這需要較強的閱讀彙編
程序的能力。通過棧回溯信息知道函數的調用關係,這已經可以幫助定位很多問題了。

使用 Oops 的棧信息手工進行棧回溯
前面說過,從 Oops 信息的 pc 寄存器值可知得知崩潰發生時的函數、出錯指令。但是錯
誤有可能是它的調用者引入的,所以還要找出函數的調用關係。
由於內核配置了 CONFIG_FRAME_POINTER,當出現 Oops 信息時,會打印棧回溯信息。如
果內核沒有配置 CONFIG_FRAME_POINTER,這時可以自己分析棧信息,找到函數的調用關係。
1.棧的作用
一個程序包含代碼段、數據段、BSS 段、堆、棧;其中數據段用來中存儲初始值不爲 0
的全局數據,BSS 段用來存儲初始值爲 0 的全局數據,堆用於動態內存分配,棧用於實現函
數調用、存儲局部變量。
被調用函數在執行之前,它會將一些寄存器的值保存在棧中,其中包括返回地址寄存器
lr。如果知道了所保存的 lr 寄存的值,那麼就可以知道它的調用者是誰。在棧信息中,一個
函數一個函數地往上找出所有保存的 lr 值,
就可以知道各個調用函數,
這就是棧回溯的原理。
2.棧回溯實例分析
仍以前面的 LCD 驅動程序爲例,
使用上面的 Oops 信息的棧信息進行分析,
棧信息如下:
Stack: (0xc0481e64 to 0xc0482000)
1e60:c02b1f70 00000020 c03625d4 c036256c c036256c 00000000 c0389a3c
1e80: c0389a3c c03c420c c0024864 00000000 c0481eac c0481ea0 c01bf4e8 c001a704
1ea0: c0481ed0 c0481eb0 c01bd5a8 c01bf4d8 c0362644 c036256c c01bd708 c0389a3c
1ec0: 00000000 c0481ee8 c0481ed4 c01bd788 c01bd4d0 00000000 c0481eec c0481f14
1ee0: c0481eec c01bc5a8 c01bd718 c038dac8 c038dac8 c03625b4 00000000 c0389a3c

1 根據 pc 寄存器值找到第一個函數,確定它的棧大小,確定調用函數。
從 Oops 信息可知 pc 值爲 c001a70c,
使用它在內核反彙編程序 vmlinux.dis 中可以知道它
位於 s3c2410fb_probe 函數內。
根據這個函數開始部分的彙編代碼可以知道棧的大小、lr 返回值在棧中保存的位置,代
碼如下:
c001a6f4 <s3c2410fb_probe>:
c001a6f4:
e1a0c00d
mov ip, sp
c001a6f8: e92ddff0 stmdb
c001a6fc: e24cb004 sub fp, ip, #4 ; 0x4
sp!, {r4, r5, r6, r7, r8, r9, sl, fp, ip, lr, pc}
c001a700: e24dd010 sub sp, sp, #16 ; 0x10
e5873000 str r3, [r7]

c001a70c:
// pc 值 c001a70c 對應的指令

{r4, r5, r6, r7, r8, r9, sl, fp, ip, lr, pc}這 11 個寄存器都保存在棧中,指令“sub sp, sp, #16”
又使得棧向下擴展了 16 字節,所以本函數的棧大小爲(11 × 4+16)字節,即 15 個雙字。
棧信息開始部分的 15 個數據就是本函數的棧內容,下面列出了它們所保存的寄存器。
1e60:
c02b1f70 00000020 c03625d4 c036256c c036256c 00000000 c0389a3c
r4
r5
r6
1e80: c0389a3c c03c420c c0024864 00000000 c0481eac c0481ea0 c01bf4e8 c001a704
r7
r8
r9
sl
fp
ip
lr
pc
其中 lr 值爲 c01bf4e8,表示函數 s3c2410fb_probe 執行完後的返回地址,它是調用函數
中的地址。下面使用 lr 值再次重複本步驟的回溯過程。
2 根據 lr 寄存器值找到調用函數,確定它的棧大小,確定上一級調用函數。
根據上步得到的 lr 值(c01bf4e8)在內核反彙編程序 vmlinux.dis 中可以知道它位於
platform_drv_probe 函數內。
根據這個函數開始部分的反彙編代碼可以知道棧的大小、lr 返回值在棧中保存的位置。
代碼如下:
c01bf4c8 <platform_drv_probe>:
c01bf4c8: e1a0c00d mov ip, sp
c01bf4cc: e92dd800 stmdb sp!, {fp, ip, lr, pc}
e89da800 ldmia sp, {fp, sp, pc}

c01bf4e8:
// lr 值(c01bf4e8)對應的指令
{fp, ip, lr, pc}這 4 寄存器都保存在棧中,本函數的棧大小爲 4 個雙字。Oops 棧信息中,
前一個函數 s3c2410fb_probe 的棧下面的 4 個數據就是函數 platform_drv_probe 的棧內容,如
下所示:
1ea0: c0481ed0 c0481eb0 c01bd5a8 c01bf4d8
fp
ip
lr
pc
其中 lr 值爲 c01bd5a8,表示函數 platform_drv_probe 執行完後的返回地址,它是上一級
調用函數中的地址。使用 lr 值,重複本步驟的查找過程,直到棧信息分析完畢或者再也無法
分析,這樣就可以找出所有的函數調用關係。
有些函數很簡單,沒有使用棧(sp 值在這個函數中沒有變化)
,或者沒有在棧中保存 lr
值。這些情況需要讀者靈活處理,較強的彙編程序閱讀能力是關鍵。

通常是使用objdump對文件反彙編,然後去查找對應錯誤,下面介紹兩種更簡單的方法:

方法一:

假如我們要查看0xffffffff8124d1f9地址對應的是內核中哪個文件哪一條代碼,可以按照如下方法來做:

~/crosstools/x86_64_gcc6.2.0_glibc2.24.0/bin/x86_64-pc-linux-gnu-addr2line -e vmlinux -a ffffffff8124d1f9

方法二:
啓動一個完好的內核,然後在啓動的內核中導入 上次發生oops的內核鏡像vmlinux 和 gdb 工具到當前目錄下。

假設上次發生oops時pc指針在0x8025a898,然後執行如下命令:

gdb vmlinux

(gdb) b*0x8025a898

這樣就可以看到出現oops時出現在內核源碼的哪個文件、哪一行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章