Oops信息解析

内核的Oops信息对于做嵌入式Linux开发的来说非常常见,在此单独划作一章。

Oops信息及栈回溯

Oops信息来源及格式

Oops这个单词含义为“惊讶”,当内核出错时(比如访问非法地址)打印出来的信息被称为Oops信息。
Oops信息包含以下几部分内容。

  1. 一段文本描述信息
    比如类似“Unable to handle kernel NULL pointer dereference at virtual address 00000000”的信息,它说明了发生的是哪类错误。
  2. Oops信息的序号。
    比如是第1次、第2次等。这些信息与下面类似,中括号内的数据表示序号。
    1
    Internal error: Oops:805 [#1]
  3. 内核中加载的模块名称,也可能没有,以下面字样开头。
    1
    Modules linked in:
  4. 发生错误的CPU序号,对于单处理器的系统,序号为0,比如:
    1
    CPU: 0 Not tainted (2.6.22.6 #36)
  5. 发生错误时CPU的各个寄存器的值。
  6. 当前进程的名字及进程ID,比如:
    1
    Process swapper (pid:1,stack limit = 0xc0480258)
    并不是说发生错误的是这个进程,而是表示发生错误时,当前进程是它。错误可能发生在内核代码、驱动程序,也可能就是这个进程的错误。
  7. 栈信息
  8. 栈回溯信息,可以从中看出函数调用关系,形式如下:
    1
    2
    3
    Backstrace:
    [<c001a6f4>] (s3c2410fb_probe + 0x0/0x560) from [<c01bf4e8>] (platform_drv_probe + 0x20/0x24)
    ...
  9. 出错指令附近的指令的机器码,比如(出错指令在小括号里):
    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
2
int *ptest = NULL;
*ptest = 0x1234;

重新编译内核,启动后会出错并打印如下Oops信息:
img not found
img not found

分析Oops信息

  1. 明确出错原因
    由出错信息“Unable to handle kernel NULL pointer dereference at virtual address 00000000”可知内核是因为非法地址访问出错,使用了空指针

  2. 根据栈回溯信息找出函数调用关系
    内核崩溃时,可以从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
    12
    do_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
  3. 根据pc寄存器的值确定出错位置
    上述Oops信息中出错时的寄存器值如下:

    1
    2
    3
    4
    PC 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(十六进制)。

  4. 结合内核源代码和反汇编代码定位问题
    先生成内核的反汇编代码vmlinux.dis,执行以下命令:

    1
    2
    cd /work/system/linux-2.6.22.6
    arm-linux-objdump -D vmlinux > vmlinux.dis

    出错地址c001a70c附近的部分汇编代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    c001a6f4: <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
    15
    static 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信息的栈信息进行分析,栈信息如下:
img not found

  1. 根据pc寄存器值找到第一个函数,确定它的栈大小,确定调用函数。
    从Oops信息可知pc值为c001a70c,使用它在内核反汇编程序vmlinux.dis中可以知道它位于s3c2410fb_probe函数内。
    根据这个函数开始部分的汇编代码可以在知道栈的大小、lr返回值在栈中保存的位置,代码如下:
    img not found
    {r4,r5,r6,r7,r8,r9,sl,fp,ip,lr,pc}这11个寄存器都保存在栈中,指令“sub sp, sp, #16”又使得栈向下扩展了16字节,所以本函数的大小为(11 x 4 + 16)字节,即15个双字。
    栈信息开始部分的15个数据就是本函数的栈内容,下面列出了它们所保存的寄存器。
    1
    2
    3
    4
    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返回值在栈中保存的位置。代码如下:
    1
    2
    3
    4
    5
    c01bf4c8  <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)对应的指令
    {fp,ip,lr,pc}这4个寄存器都保存在栈中,本函数的栈大小为4个双字。Oops栈信息中,前一个函数s3c2410fb_probe的栈下面的4个数据就是函数platform_drv_probe的栈内容,如下所示:
    1
    2
    lea0:   c0481ed0  c0481eb0  c01bd5a8  c01bf4d8
    fp ip lr pc
    其中lr值为c01bd5a8,表示函数platform_drv_probe执行完后的返回地址,它是上一级调用函数中的地址。使用lr值,重复本步骤的查找过程,知道栈信息分析完毕或者再也无法分析,这样就可以找出所有的函数调用关系。
    有些函数很简单,没有使用栈(sp在这个函数中没有吧变化),或者没有在栈中保存lr值。