一个 oops 分析实例
最近出现了比较多的内核崩溃的问题,之前也曾转发过网上一篇关于内核崩溃问题的跟踪方法,我在这里借 xxx 提供的这个死机现场,再给大家描述一下这类问题的分析方法和 debug 过程,也希望能让大家真正掌握这类问题的调试方法。让大家明白,内核崩溃问题的调试方法其实也就是这样一些固定的套路。问题的分析过程如下:
确认死机现场
1 | [56400.437450] Unable to handle kernel paging request at virtual address f7fe44a1 |
根据死机现场的打印分析,死机的原因是因为执行 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 | c004bab4 <cpuacct_charge>: |
再回头看异常现场:r3 : f7fe4479,发现 r3 + 0x28 = f7fe44a1,代码和异常现场相吻合。 接下来就是尝试分析出错的代码执行过程,这个有一定的难度,需要结合 C 源代码,和对汇编代码的理解来进行分析。
定位出错位置的源代码
通过反汇编得知,出错时的 PC 位置在我编出来的 vmlinux 的 0xc004bb1c 地址处,采用命令:
1 | arm-none-linux-gnueabi-addr2line -e vmlinux -f c004bb1c |
来行源码定位:
1 | kevin@Exdroid6:~/aw1650-47/lichee/linux-3.4$ arm-none-linux-gnueabi-addr2line -e vmlinux -f c004bb1c |
发现出错的源代码是在操作一个链表上的指针,这些数据不是全局变量,很难分析出其出错的本质原因,所以源码分析一步在这时可以放弃;
分析出错的汇编代码
结合"cpuacct_charge"函数的 C 代码实现,再回头看出错时的汇编代码,发现出错的代码其实是位于一个循环体中:
1 | c004baf0: ea00000a b c004bb20 <cpuacct_charge+0x6c> |
出错的代码是:"c004bb1c: e5931028 ldr r1, [r3, #40] ; 0x28",即 r3 的值是错的,我们来尝试分析 r3 这个数据的来源。 从:"c004bb1c: e5931028 ldr r1, [r3, #40] ; 0x28"这句代码向前找,发现 r3 数据的直接来源是 r1,
1 | c004bb0c: e5913000 ldr r3, [r1] |
再回头看异常现场的 r1 值为"r1 : c07312c8",以及 r1 所指向的数据:
1 | [56400.440040] R1: 0xc0731248: |
可以看到"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 | %>dmr 0 0x58dc6010 16 |
我们发现,0x58dd2030 处的数据是 0x00000000,即:0xd8dd2030 处的数据应该是 0x00000000。
再回头看把汇编出来的代码:
1 | c004bb10: e5933018 ldr r3, [r3, #24] |
如果 r3 是 0,应该是退出循环,而不是再去取值,因此,可以先推断,出错的原因可能是:
- cpu 本身出错,导致指令执行不正常,从而导致 r3 中的数据出错
- DRAM 访问出错,导致取回到 r3 的数据是错的
再结合我们之前的推断,CPU 模块本身可能已经出错了,因此,我们更环疑该问题是 CPU 工作不稳定导致的。 我们再来确认一下此时 CPU 的工作频率和电压:
1 | %>dmr 0 0x1c20000 16 |
根据寄存器的值可以推出 CPU 当前工作频率为 1008Mhz;测得当前的 CPU 工作电压为:1.17v;
分析下来,并没有真正查明问题的原因,只是初步推测了一个问题可能的方向,后续的实验可以重点观注该方向上的一些实验。
其他常用命令
根据函数名拿到函数的地址:
1 | arm-linux-gnueabi-nm vmlinux | grep __cpuinfo_store_cpu |
根据出错的位置计算起始和结束地址
1 | $ echo "obase=16;ibase=10;$((0x80100919c0+0x80))" | bc -l |
根据起始地址和结束地址 dump 出出错的汇编和源码,结合 oops 现场进行复盘分析
1 | arm-linux-gnueabi-objdump -dS vmlinux --start-address=0xffffff80100919c0 --stop-address=0xffffff8010091b90 |