0%

一个oops分析实例

最近出现了比较多的内核崩溃的问题,之前也曾转发过网上一篇关于内核崩溃问题的跟踪方法,我在这里借xxx提供的这个死机现场,再给大家描述一下这类问题的分析方法和debug过程,也希望能让大家真正掌握这类问题的调试方法。让大家明白,内核崩溃问题的调试方法其实也就是这样一些固定的套路。问题的分析过程如下:

确认死机现场

1
2
3
4
5
6
7
8
9
10
11
12
13
[56400.437450] Unable to handle kernel paging request at virtual address f7fe44a1
[56400.440040] pgd = d112c000
[56400.440040] [f7fe44a1] *pgd=00000000
[56400.440040] Internal error: Oops: 5 [#1] PREEMPT SMP ARM
[56400.440040] Modules linked in: 8188eu gt82x mma8452 rtl8150 mcs7830 qf9700 asix sunxi_keyboard sw_device vfe_v4l2 gc2035 gc0307 vfe_subdev vfe_os cci videobuf_dma_contig videobuf_core mali(O) lcd disp nand(O)
[56400.440040] CPU: 0 Tainted: G O (3.4.39 #428)
[56400.440040] PC is at cpuacct_charge+0x68/0xa8
[56400.440040] LR is at cpuacct_charge+0x30/0xa8
[56400.440040] pc : [<c004b93c>] lr : [<c004b904>] psr: a0000093
[56400.440040] sp : ca25bda0 ip : 0061b000 fp : ca25bdb4
[56400.440040] r10: 00000000 r9 : 00000000 r8 : 000b64fe
[56400.440040] r7 : 00000000 r6 : 000b64fe r5 : 00000000 r4 : c06e4480
[56400.440040] r3 : f7fe4479 r2 : db506884 r1 : c07312c8 r0 : c06c8a00

根据死机现场的打印分析,死机的原因是因为执行0xc004b93c处的代码访问数据时,取到一个非法地址:0xf7fe44a1,访问该地址的时候,会导致系统异常;

反汇编内核代码,分析原因

通过命令"arm-none-linux-gnueabi-objdump -d vmlinux > vmlinux.lst ",可以反汇编出内核的代码。 固件不是在我的机器上编出来的,所以反汇编得到的代码和在机器上实际运行的不是完全一致的,但是,没关系,这不影响我们分析问题。 由于本地的反汇编代码和设备上的不一致,所以不能用PC值0xc004b93c去定位代码,而是用"PC is at cpuacct_charge+0x68/0xa8"这个信息来确认死机时的代码位置。 "cpuacct_charge+0x68/0xa8"的含义是:函数"cpuacct_charge"的代码为0xa8个字节,出错时,PC位于"cpuacct_charge"偏移0x68字节处。 OK,根据此信息,我们来查看反汇编得到的代码。cpuacct_charge位于0xc004bab4处,偏移0x68字节,即0xc004bab4 + 0x68 = 0xc004bb1c处出现异常。

1
2
3
4
5
6
7
8
c004bab4 <cpuacct_charge>:
c004bab4: e92d48f0 push {r4, r5, r6, r7, fp, lr}
。。。
c004bb0c: e5913000 ldr r3, [r1]
c004bb10: e5933018 ldr r3, [r3, #24]
c004bb14: e3530000 cmp r3, #0
c004bb18: 0a000002 beq c004bb28 <cpuacct_charge+0x74>
c004bb1c: e5931028 ldr r1, [r3, #40] ; 0x28

再回头看异常现场:r3 : f7fe4479,发现r3 + 0x28 = f7fe44a1,代码和异常现场相吻合。 接下来就是尝试分析出错的代码执行过程,这个有一定的难度,需要结合C源代码,和对汇编代码的理解来进行分析。

定位出错位置的源代码

通过反汇编得知,出错时的PC位置在我编出来的vmlinux的0xc004bb1c地址处,采用命令:

1
arm-none-linux-gnueabi-addr2line -e vmlinux -f c004bb1c

来行源码定位:

1
2
3
kevin@Exdroid6:~/aw1650-47/lichee/linux-3.4$ arm-none-linux-gnueabi-addr2line -e vmlinux -f c004bb1c
cgroup_subsys_state
/home/kevin/aw1650-47/lichee/linux-3.4/include/linux/cgroup.h:508

发现出错的源代码是在操作一个链表上的指针,这些数据不是全局变量,很难分析出其出错的本质原因,所以源码分析一步在这时可以放弃;

分析出错的汇编代码

结合"cpuacct_charge"函数的C代码实现,再回头看出错时的汇编代码,发现出错的代码其实是位于一个循环体中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c004baf0: ea00000a  b c004bb20 <cpuacct_charge+0x6c>
c004baf4: e794c105 ldr ip, [r4, r5, lsl #2]
c004baf8: e5910010 ldr r0, [r1, #16]
c004bafc: e18020dc ldrd r2, [r0, ip]
c004bb00: e0922006 adds r2, r2, r6
c004bb04: e0a33007 adc r3, r3, r7
c004bb08: e18020fc strd r2, [r0, ip]
c004bb0c: e5913000 ldr r3, [r1]
c004bb10: e5933018 ldr r3, [r3, #24]
c004bb14: e3530000 cmp r3, #0
c004bb18: 0a000002 beq c004bb28 <cpuacct_charge+0x74>
c004bb1c: e5931028 ldr r1, [r3, #40] ; 0x28
c004bb20: e3510000 cmp r1, #0
c004bb24: 1afffff2 bne c004baf4 <cpuacct_charge+0x40>

出错的代码是:"c004bb1c: e5931028 ldr r1, [r3, #40] ; 0x28",即r3的值是错的,我们来尝试分析r3这个数据的来源。 从:"c004bb1c: e5931028 ldr r1, [r3, #40] ; 0x28"这句代码向前找,发现r3数据的直接来源是r1,

1
2
c004bb0c: e5913000  ldr r3, [r1]
c004bb10: e5933018 ldr r3, [r3, #24]

再回头看异常现场的r1值为"r1 : c07312c8",以及r1所指向的数据:

1
2
3
4
5
6
[56400.440040] R1: 0xc0731248:
[56400.440040] 1248 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[56400.440040] 1268 00000000 00000000 00000000 00000000 d9833700 00989680 00000000 00000000
[56400.440040] 1288 00000000 00000000 00000000 d9818e40 00000000 00000000 00000000 d9847380
[56400.440040] 12a8 d9800dc0 00000000 00000000 00000000 00000000 00000000 00000000 00000000
[56400.440040] 12c8 d8dd2018 00000001 00000001 00000000 c06c8a00 c06c5768 00000000 00000000

可以看到"c004bb0c: e5913000 ldr r3, [r1]"取到的r3值为0xd8dd2018,再看接下来的一条汇编: "c004bb10: e5933018 ldr r3, [r3, #24]",即错误的数据是从0xd8dd2018 + 24 = 0xd8dd2030处取到的。

此时如果想再往下分析,就只能借助Jtag了。

Jtag分析

当前死机的这一台,DS-5无法打开,所以没有办法直接通过DS-5来查看0xd8dd2030处的数据是否真的错了。 此时,可以先推断CPU模块本身已经出错了,导致jtag无法通过CPU访问数据。 我们再来尝试一下用JTAG的CSAT方式,访问一下,发现CSAT可以连接上,而且可以读取物理内存的内容。 由于CSAT是绕过CPU来访问内存的数据,所以无法直接访问"0xd8dd2030"处的内容。但是,我们知道,内核 空间有近700Mbyte的是直接映射的,即0x40000000 -> 0xc0000000,那么0xd8dd2030所对应的物理地址应该 为0x58dd2030,通过CSAT查看一下0x58dd2030处的数据:

1
2
3
4
5
%>dmr 0 0x58dc6010 16
0x58DC6010 : 0xC06ED7EC 0xC06ED7EC 0x00000002 0x00000001
0x58DC6020 : 0xD8DC6020 0xD8DC6020 0xD9A32088 0xD9A32088
0x58DC6030 : 0x00000000 0xD9432F70 0x00000000 0x00000000
0x58DC6040 : 0xC07312C8 0x00000000 0x00000000 0x00000000

我们发现,0x58dd2030处的数据是0x00000000,即:0xd8dd2030处的数据应该是0x00000000。

再回头看把汇编出来的代码:

1
2
3
4
c004bb10: e5933018  ldr r3, [r3, #24]
c004bb14: e3530000 cmp r3, #0
c004bb18: 0a000002 beq c004bb28 <cpuacct_charge+0x74>
c004bb1c: e5931028 ldr r1, [r3, #40] ; 0x28

如果r3是0,应该是退出循环,而不是再去取值,因此,可以先推断,出错的原因可能是:

  • cpu本身出错,导致指令执行不正常,从而导致r3中的数据出错
  • DRAM访问出错,导致取回到r3的数据是错的

再结合我们之前的推断,CPU模块本身可能已经出错了,因此,我们更环疑该问题是CPU工作不稳定导致的。 我们再来确认一下此时CPU的工作频率和电压:

1
2
%>dmr 0 0x1c20000 16
0x01C20000 : 0x90001410 0x00000000 0x90034E14 0x00000000

根据寄存器的值可以推出CPU当前工作频率为1008Mhz;测得当前的CPU工作电压为:1.17v;

分析下来,并没有真正查明问题的原因,只是初步推测了一个问题可能的方向,后续的实验可以重点观注该方向上的一些实验。

其他常用命令

根据函数名拿到函数的地址:

1
2
arm-linux-gnueabi-nm vmlinux | grep __cpuinfo_store_cpu
ffffff80100919c0 t __cpuinfo_store_cpu

根据出错的位置计算起始和结束地址

1
2
3
4
$ echo "obase=16;ibase=10;$((0x80100919c0+0x80))" | bc -l
8010091A40
$ echo "obase=16;ibase=10;$((0x80100919c0+0x1d0))" | bc -l
8010091B90

根据起始地址和结束地址dump出出错的汇编和源码,结合oops现场进行复盘分析

1
arm-linux-gnueabi-objdump -dS vmlinux --start-address=0xffffff80100919c0 --stop-address=0xffffff8010091b90