您的位置:首页 > 运维架构 > Linux

linux中Oops信息的调试及栈回溯

2013-10-31 10:19 267 查看
直接通过交叉编译器定位代码:http://blog.csdn.net/hellowxwworld/article/details/10731653

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:

[<c001a6f4>] (s3c2410fb_probe+0×0/0×560) from [<c01bf4e8>] (platform_drv_

probe+0×20/0×24)



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 = 0×1234;

重新编译内核,启动后会出错并打印出如下 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+0×18/0×560

LR is at platform_drv_probe+0×20/0×24

pc : [<c001a70c>]

lr : [<c01bf4e8>]

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:

[<c001a6f4>] (s3c2410fb_probe+0×0/0×560) from [<c01bf4e8>] (platform_drv_

probe+0×20/0×24)

[<c01bf4c8>] (platform_drv_probe+0×0/0×24) from [<c01bd5a8>] (driver_probe_

device+0xe8/0x18c)

[<c01bd4c0>] (driver_probe_device+0×0/0x18c) from [<c01bd788>] (__driver_

attach+0×80/0xe0)

r8:00000000 r7:c0389a3c r6:c01bd708 r5:c036256c r4:c0362644

[<c01bd708>] (_ _driver_attach+0×0/0xe0) from [<c01bc5a8>] (bus_for_each_

dev+0×50/0×84)

r5:c0481eec r4:00000000

[<c01bc558>] (bus_for_each_dev+0×0/0×84) from [<c01bd808>] (driver_attach+

0×20/0×28)

r7:c038d9dc r6:c0389a44 r5:c0389a3c r4:00000000

[<c01bd7e8>] (driver_attach+0×0/0×28) from [<c01bcd78>] (bus_add_driver+

0x7c/0x1b4)

[<c01bccfc>] (bus_add_driver+0×0/0x1b4) from [<c01bdc84>] (driver_register+

0×80/0×88)

[<c01bdc04>] (driver_register+0×0/0×88) from [<c01bf5fc>] (platform_driver_

register+0x6c/0×88)

r4:00000000

[<c01bf590>] (platform_driver_register+0×0/0×88) from [<c019479c>] (s3c2410fb_

init+0×14/0x1c)

[<c0194788>] (s3c2410fb_init+0×0/0x1c) from [<c0008c14>] (kernel_init+0xd4/

0x28c)

[<c0008b40>] (kernel_init+0×0/0x28c) from [<c00476ec>] (do_exit+0×0/0×760)

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 寄存器得知崩溃发生时的函数、出错指令。但是很多情况下,错

误有可能是它的调用者引入的,所以找出函数的调用关系也很重要。

部分栈回溯信息如下:

[<c001a6f4>] (s3c2410fb_probe+0×0/0×560) from [<c01bf4e8>] (platform_drv_

probe+0×20/0×24)

这行信息分为两部分,

表示后面的 platform_drv_probe 函数调用了前面的 s3c2410fb_probe

函数。

前半部含义为:

“c001a6f4”是 s3c2410fb_probe 函数首地址偏移 0 的地址,这个函数大

小为 0×560。

后半部含义为:

“c01bf4e8”是 platform_drv_probe 函数首地址偏移 0×20 的地址,这个函

数大小为 0×24。

另外,后半部的“[<c01bf4e8>]”表示 s3c2410fb_probe 执行后的返回地址。

对于类似下面的栈回溯信息,其中是 r8~r4 表示 driver_probe_device 函数刚被调用时这

些寄存器的值。

[<c01bd4c0>] (driver_probe_device+0×0/0x18c) from [<c01bd788>] (__driver_

attach+0×80/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+0×18/0×560

LR is at platform_drv_probe+0×20/0×24

pc : [<c001a70c>]

lr : [<c01bf4e8>]

psr: a0000013



“PC is at s3c2410fb_probe+0×18/0×560”表示出错指令为 s3c2410fb_probe 函数中偏移为

0×18 的指令。

“pc : [<c001a70c>]”表示出错指令的地址为 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 ; 0×4

c001a700: e24dd010 sub sp, sp, #16 ; 0×10

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}

; 0×0

<===========出错指令

出错指令为“str r3, [r7]”

,它把 r3 寄存器的值放到内存中,内存地址为 r7 寄存器的值。

根据 Oops 信息中的寄存器值可知:r3 为 0×00001234,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 = 0×1234;

mach_info = pdev->dev.platform_data;

结合反汇编代码,很容易知道是“*ptest = 0×1234;”导致错误,其中的 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 ; 0×4

sp!, {r4, r5, r6, r7, r8, r9, sl, fp, ip, lr, pc}

c001a700: e24dd010 sub sp, sp, #16 ; 0×10

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

值。这些情况需要读者灵活处理,较强的汇编程序阅读能力是关键。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: