Oops信息解析
内核的Oops信息对于做嵌入式Linux开发的来说非常常见,在此单独划作一章。
Oops信息及栈回溯
Oops信息来源及格式
Oops这个单词含义为“惊讶”,当内核出错时(比如访问非法地址)打印出来的信息被称为Oops信息。
Oops信息包含以下几部分内容。
- 一段文本描述信息
比如类似“Unable to handle kernel NULL pointer dereference at virtual address 00000000”
的信息,它说明了发生的是哪类错误。 - Oops信息的序号。
比如是第1次、第2次等。这些信息与下面类似,中括号内的数据表示序号。1
Internal error: Oops:805 [#1]
- 内核中加载的模块名称,也可能没有,以下面字样开头。
1
Modules linked in:
- 发生错误的CPU序号,对于单处理器的系统,序号为0,比如:
1
CPU: 0 Not tainted (2.6.22.6 #36)
- 发生错误时CPU的各个寄存器的值。
- 当前进程的名字及进程ID,比如:并不是说发生错误的是这个进程,而是表示发生错误时,当前进程是它。错误可能发生在内核代码、驱动程序,也可能就是这个进程的错误。
1
Process swapper (pid:1,stack limit = 0xc0480258)
- 栈信息
- 栈回溯信息,可以从中看出函数调用关系,形式如下:
1
2
3Backstrace:
[<c001a6f4>] (s3c2410fb_probe + 0x0/0x560) from [<c01bf4e8>] (platform_drv_probe + 0x20/0x24)
... - 出错指令附近的指令的机器码,比如(出错指令在小括号里):
1
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有可能已经被其他配置项自动选上。
使用Oops信息调试内核的实例
获得Oops信息
刻意修改LCD驱动程序drivers/video/s3c2410fb.c
,加入错误代码:在s3c2410fb_probe
函数开头增加如下两行代码:
1 | int *ptest = NULL; |
重新编译内核,启动后会出错并打印如下Oops信息:
分析Oops信息
明确出错原因
由出错信息“Unable to handle kernel NULL pointer dereference at virtual address 00000000”
可知内核是因为非法地址访问出错,使用了空指针
。根据栈回溯信息找出函数调用关系
内核崩溃时,可以从pc寄存器得知崩溃发生时的函数、出错指令。但是很多情况下,错误有可能是它的调用者引入的,所以找出函数的调用关系也很重要。
部分栈回溯信息如下:1
[<c001a6f4>] (s3c2410fb_probe + 0x0/0x560) from [<c01bf4e8>] (platform_drv_probe + 0x20/0x24)
这行信息分为两部分,表示后面的
platform_drv_probe
函数调用了前面的s3c2410fb_probe
函数。
前半部分含义为:“c001a6f4”是s3c2410fb_probe
函数首地址偏移0的地址,这个函数大小为0x560。
后半部分含义为:“c01bf4e8”是paltform_drv_probe
函数首地址偏移0x20的地址,这个函数大小为0x24。
另外,后半部的“[]”表示 s3c2410fb_probe
执行后的返回地址。
对于类似下面的栈回溯信息,其中r8-r4表示driver_probe_device
函数刚被调用时这些寄存器的值。1
2[<c01bd4c0>] (driver_probe_device + 0x0/0x18c) from [<c01bd788>] (__driver_attach + 0x80/0xe0)
r8:00000000 r7:c0389a3c r6:c01bd708 r5:c036256c r4:c0362644从上面的栈回溯信息可以知道内核出错时的函数调用关系如下,最后在
s3c2410fb_probe
函数内部崩溃。1
2
3
4
5
6
7
8
9
10
11
12do_exit ->
kernel_init ->
s3c2410fb_init ->
platform_driver_register ->
driver_register ->
bus_add_driver ->
driver_attch ->
bus_for_each_dev ->
__driver_attach ->
driver_probe_device ->
platform_drv_probe ->
s3c2410fb_probe根据pc寄存器的值确定出错位置
上述Oops信息中出错时的寄存器值如下:1
2
3
4PC is at s3c2410fb_probe+0x18/0x560
LR is at platform_drv_probe+0x20/0x24
pc : [<c001a70c>] lr : [<c01bf4e8>] psr : a0000013
...“PC is at s3c2410fb_probe+0x18/0x560”表示出错指令为s3c2410fb_probe函数中偏移为0x18的指令。
“pc : []”表示出错指令的地址为c001a70c(十六进制)。 结合内核源代码和反汇编代码定位问题
先生成内核的反汇编代码vmlinux.dis
,执行以下命令:1
2cd /work/system/linux-2.6.22.6
arm-linux-objdump -D vmlinux > vmlinux.dis出错地址c001a70c附近的部分汇编代码如下:
1
2
3
4
5
6
7
8
9c001a6f4: <s3c2410fb_probe>:
c001a6f4: e1a0c00d mov ip, sp
c001a6f8: e92ddff0 stmdb sp!, {r4,r5,r6,r7,r8,r9,sl,fp,ip,lr,pc}
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; 0x0
c001a70c: e5873000 str r3, [r7] <=========出错指令
c001a710: e59030fc ldr r3, [r0, #252]出错指令为
“str r3, [r7]”
,它把r3寄存器的值放到内存中,内存地址为r7寄存器的值。根据Oops信息中的寄存器值可知:r3为0x00001234,r7为0。0地址不可访问,所以出错
。s3c2410fb_probe
函数的部分c代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15static int __init s3c2410fb_probe(struct platform_device *pdev)
{
struct s3c2410fb_info *info;
struct fb_info *fb_info;
struct s3c2410fb_hw *mregs;
int ret;
int irq;
int i;
u32 lcdcon1;
int *ptest = NULL;
*ptest = 0x1234;
mach_info = pdev->dev.paltform_data;
}结合反汇编代码,很容易知道是“*ptest = 0x1234;”导致错误,其中的ptest为空。
对于大多数情况,从反汇编代码定位到C代码并不容易,这需要较强的汇编程序阅读能力。通过栈回溯信息知道函数的调用关系,这已经可以帮助定位很多问题了。
使用Oops的栈信息手工进行栈回溯
前面说过,从Oops信息的pc寄存器值可得知崩溃发生时的函数、出错指令。但是错误有可能是它的调用者引入的,所以还是要找出函数的调用关系。
由于内核配置了CONFIG_FRAME_POINTER,当出现Oops信息时,会打印栈回溯信息。如果内核没有配置CONFIG_FRAME_POINTER,这时可以自己分析栈信息,找到函数的调用关系。
栈的作用
一个程序包含代码段、数据段、BSS段、堆、栈;其中数据段用来存储初始值不为0的全局数据,BSS段用来存储初始值为0的全局数据,堆用于动态内存分配,栈用于实现函数调用、存储局部变量。
被调用函数在执行之前,它会将一些寄存器的值保存在栈中,其中包括返回地址寄存器lr。如果知道了所保存的lr寄存器的值,那就可以知道它的调用者是谁。在栈信息中,一个函数一个函数地往上找出所有保存的lr值,就可以知道各个调用函数,这就是栈回溯的原理。
栈回溯实例分析
仍以前面的LCD驱动程序为例,使用上面的Oops信息的栈信息进行分析,栈信息如下:
- 根据pc寄存器值找到第一个函数,确定它的栈大小,确定调用函数。
从Oops信息可知pc值为c001a70c,使用它在内核反汇编程序vmlinux.dis
中可以知道它位于s3c2410fb_probe
函数内。
根据这个函数开始部分的汇编代码可以在知道栈的大小、lr返回值在栈中保存的位置,代码如下:
{r4,r5,r6,r7,r8,r9,sl,fp,ip,lr,pc}这11个寄存器都保存在栈中,指令“sub sp, sp, #16”又使得栈向下扩展了16字节,所以本函数的大小为(11 x 4 + 16)字节,即15个双字。
栈信息开始部分的15个数据就是本函数的栈内容,下面列出了它们所保存的寄存器。其中lr值为c01bf4e8,表示函数s3c2410fb_probe执行完后的返回地址,它是调用函数中的地址。下面使用lr值再次重复本步骤的回溯过程。1
2
3
41e60: 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寄存器值找到调用函数,确定它的栈大小,确定上一级调用函数。
根据上步得到的lr值(c01bf4e8), 在内核反汇编程序vmlinux.dis中可以知道它位于platform_drv_probe
函数内。
根据这个函数开始部分的反汇编代码可以知道栈的大小、lr返回值在栈中保存的位置。代码如下:{fp,ip,lr,pc}这4个寄存器都保存在栈中,本函数的栈大小为4个双字。Oops栈信息中,前一个函数s3c2410fb_probe的栈下面的4个数据就是函数platform_drv_probe的栈内容,如下所示:1
2
3
4
5c01bf4c8 <platform_drv_probe>:
c01bf4c8: e1a0c00d mov ip, sp
c01bf4cc: e92dd800 stmdb sp!, {fp,ip,lr,pc}
...
c01bf4e8: e89da800 ldmia sp!, {fp,sp,pc} //lr值(c01bf4e8)对应的指令其中lr值为c01bd5a8,表示函数1
2lea0: c0481ed0 c0481eb0 c01bd5a8 c01bf4d8
fp ip lr pcplatform_drv_probe
执行完后的返回地址,它是上一级调用函数中的地址。使用lr值,重复本步骤的查找过程,知道栈信息分析完毕或者再也无法分析,这样就可以找出所有的函数调用关系。
有些函数很简单,没有使用栈(sp在这个函数中没有吧变化),或者没有在栈中保存lr值。