原文链接:http://blog.csdn.net/hunhunzi/article/details/7052032
最近在搞Atmel 的SAM9x25平台,Linux系统,用于工业设备。这也是我首次参与工业设备的研发。在调试Atmel SAM9x25的Linux串口设备的时候,发现无论是读还是写,都会产生异常。相关的异常信息如下:
==================================================================================================================
Unable to handle kernel NULL pointer dereference at virtual address 00000000
pgd = c0004000
[00000000] *pgd=00000000
Internal error: Oops: 17 [#1]
last sysfs file: /sys/devices/virtual/vc/vcsa1/dev
Modules linked in:
CPU: 0 Not tainted (2.6.39 #1)
PC is at atmel_tasklet_func+0x110/0x69c
LR is at atmel_tasklet_func+0x10/0x69c
pc : [<c01a4f30>] lr : [<c01a4e30>] psr: 20000013
sp : c7825f50 ip : c045e0bc fp : 00000000
r10: c0456a80 r9 : 0000000a r8 : 00000000
r7 : c7874568 r6 : c045e0a8 r5 : 00000100 r4 : c045dfb4
r3 : 00000002 r2 : 00000ffc r1 : 00000001 r0 : 00000001
Flags: nzCv IRQs on FIQs on Mode SVC_32 ISA ARM Segment kernel
Control: 0005317f Table: 27aec000 DAC: 00000017
Process ksoftirqd/0 (pid: 3, stack limit = 0xc7824270)
Stack: (0xc7825f50 to 0xc7826000)
5f40: 00000100 c7824000 00000001 00000018
5f60: 0000000a c0456a80 c7825f84 00000000 00000100 c7824000 00000001 00000018
5f80: c0456a80 c0047b70 00000006 c0047650 c0432e50 00000000 c7824000 00000000
5fa0: 00000000 c0047938 00000000 00000000 00000000 c00479a0 c7825fd4 c7819f60
5fc0: 00000000 c0058c64 c00335f4 00000000 00000000 00000000 c7825fd8 c7825fd8
5fe0: 00000000 c7819f60 c0058be0 c00335f4 00000013 c00335f4 0c200050 fc3b9beb
[<c01a4f30>] (atmel_tasklet_func+0x110/0x69c) from [<c0047b70>] (tasklet_action+0x80/0xe4)
[<c0047b70>] (tasklet_action+0x80/0xe4) from [<c0047650>] (__do_softirq+0x74/0x104)
[<c0047650>] (__do_softirq+0x74/0x104) from [<c00479a0>] (run_ksoftirqd+0x68/0x108)
[<c00479a0>] (run_ksoftirqd+0x68/0x108) from [<c0058c64>] (kthread+0x84/0x8c)
[<c0058c64>] (kthread+0x84/0x8c) from [<c00335f4>] (kernel_thread_exit+0x0/0x8)
Code: 1a000002 e59f057c e59f157c ebfa416c (e5983000)
---[ end trace 6b8e1841ba3a56c9 ]---
Kernel panic - not syncing: Fatal exception in interrupt
[<c0037784>] (unwind_backtrace+0x0/0xf0) from [<c00429f4>] (panic+0x54/0x178)
[<c00429f4>] (panic+0x54/0x178) from [<c0035a18>] (die+0x17c/0x1bc)
[<c0035a18>] (die+0x17c/0x1bc) from [<c00386c4>] (__do_kernel_fault+0x64/0x84)
[<c00386c4>] (__do_kernel_fault+0x64/0x84) from [<c003889c>] (do_page_fault+0x1b8/0x1cc)
[<c003889c>] (do_page_fault+0x1b8/0x1cc) from [<c002c2f0>] (do_DataAbort+0x38/0x9c)
[<c002c2f0>] (do_DataAbort+0x38/0x9c) from [<c003234c>] (__dabt_svc+0x4c/0x60)
Exception stack(0xc7825f08 to 0xc7825f50)
5f00: 00000001 00000001 00000ffc 00000002 c045dfb4 00000100
5f20: c045e0a8 c7874568 00000000 0000000a c0456a80 00000000 c045e0bc c7825f50
5f40: c01a4e30 c01a4f30 20000013 ffffffff
[<c003234c>] (__dabt_svc+0x4c/0x60) from [<c01a4f30>] (atmel_tasklet_func+0x110/0x69c)
[<c01a4f30>] (atmel_tasklet_func+0x110/0x69c) from [<c0047b70>] (tasklet_action+0x80/0xe4)
[<c0047b70>] (tasklet_action+0x80/0xe4) from [<c0047650>] (__do_softirq+0x74/0x104)
[<c0047650>] (__do_softirq+0x74/0x104) from [<c00479a0>] (run_ksoftirqd+0x68/0x108)
[<c00479a0>] (run_ksoftirqd+0x68/0x108) from [<c0058c64>] (kthread+0x84/0x8c)
[<c0058c64>] (kthread+0x84/0x8c) from [<c00335f4>] (kernel_thread_exit+0x0/0x8)
==================================================================================================================
通常认为,产生异常的地址是lr寄存器的值,从上面的异常信息可以看到[lr]的值是c01a4e30。
接下来,我们可以通过内核镜像文件反汇编来找到这个地址。内核编译完成后,会在内核代码根目录下生成vmlinux文件,我们可以通过以下命令来反汇编:
arm-none-eabi-objdump -Dz -S vmlinux >linux.dump
值得注意的是,arm-none-eabi-objdump的参数-S表示尽可能的把原来的代码和反汇编出来的代码一起呈现出来,-S参数需要结合arm-linux-gcc编译参数-g,才能达到反汇编时同时输出原来的代码。所以,我在linux内核代码根目录的Makefile中增加-g编译参数:
KBUILD_CFLAGS := -g -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs \
-fno-strict-aliasing -fno-common \
-Werror-implicit-function-declaration \
-Wno-format-security \
-fno-delete-null-pointer-checks
修改Makefile后,重新编译内核,在根目录中生成的vmlinux文件就会包含了原来的代码信息,因此,该文件的大小也比原来大一倍!
最后执行“arm-none-eabi-objdump -Dz-S vmlinux >linux.dump”,由于加入了-g编译参数,执行这个反汇编命令需要很长时间(本人在虚拟机上执行,花了近6个小时!),反汇编出来的linux.dump文件也比原来的44MB增大到惊人的503MB。
接下来可以用UltraEdit打开linux.dump文件,查找“c01a4e30”字符串。
最后定位到的信息是:
==================================================================================================================
/*
* tasklet handling tty stuff outside the interrupt handler.
*/
static void atmel_tasklet_func(unsigned long data)
{
c01a4e20: e92d45f0 push {r4, r5, r6, r7, r8, sl, lr}
c01a4e24: e24dd01c sub sp, sp, #28 ; 0x1c
c01a4e28: e1a04000 mov r4, r0
/* The interrupt handler does not take the lock */
spin_lock(&port->lock);
if (atmel_use_pdc_tx(port))
atmel_tx_pdc(port);
else if (atmel_use_dma_tx(port))
c01a4e2c: ebfffda1 bl c01a44b8 <atmel_use_dma_tx>
c01a4e30: e3500000 cmp r0, #0 ; 0x0
c01a4e34: e5943034 ldr r3, [r4, #52]
c01a4e38: 0a00007b beq c01a502c <atmel_tasklet_func+0x20c>
==================================================================================================================
可以看出来,异常的产生位于atmel_tasklet_func函数的 else if (atmel_use_dma_tx(port))一行。
估计atmel_use_dma_tx(port)的“port”参数为空指针所致!
最后,我把串口的DMA功能去掉,改为直接传送,这样做虽然效率低了点,但产生异常的现象消失了。
到后面再仔细分析为什么会产生这个异常,彻底解决这个问题。
关键字:ARM atmel SAM9x25 Linux内核驱动异常 调试 反汇编 objdump
6.4 必修实验3--内核异常分析(3)
接下来的这些信息,和这个模块的调试没多大关系,它们是虚拟内存页目录、页表信息、oops错误号以及最后访问的sysfs文件等。
- pgd = c39d8000
- [00000000] *pgd=339cf031, *pte=00000000, *ppte=00000000
- Internal error: Oops: 817 [#1]
- last sysfs file: /sys/devices/platform/soc-audio/sound/card0/mixer/dev
- Modules linked in: oops(+)
再接下来是寄存器信息,这部分信息比较重要,其中最可能帮助定位错误的寄存器当然是PC。在这部分信息中,下面这句最为关键。
- PC is at func_D+0x1c/0x28 [oops]
它直接地告诉了我们,oops出错时,PC是位于func_D函数标号之后的0x1c处(怎么去寻找它?后面会进行分析。另外请思考后面的0x28代表什么?)。
寄存器信息之后是栈信息,但这里还用不上,先略过。
最后的部分,也就是Backtrace标号开始的地方,它是oops的精华。它表示回溯信息,也告诉调试者在oops出错之前,模块调用了那些函数。当然,在本实例中,可以看到模块调用了func_D后就出错了,显然错误就在func_D中了。
结尾部分还有一点信息请注意。
- Code: e59f0010 eb412fb6 e3a0200b e3a03000 (e5832000)
Code标号开始的字段记录了模块出错前最后几条机器码,其中被括号括起来的就是oops出错对应的机器码。
(4)根据上面的分析,可以使用反汇编来确定出错的位置。在RHEL5中,使用命令:arm-linux-objdump -D -S oops.ko >log,将模块文件反汇编到log中,使用vim打开该文件log,直接找到func_D标号处,如图6-17所示。
![]() |
(点击查看大图)图6-17 反汇编结果 |
根据前面的信息,出错位置应该在func_D+0x1c处,func_D在0x1c,所以出错地址应该是0x38。看看这句汇编代码,前面的语句将寄存器r3赋值为0,然后这句又试图将寄存器r2的值存入到r3指向的地址处,也就是向0地址写。因此出错。再来看看这句出错代码对应的机器码e5832000,显然就是之前在opps的Code字段中看到的被括起来的那个。
(5)通过反汇编程序,定位了汇编代码中的错误位置,但对于用C语言编写的内核模块而言,这样还不够。如何才能准确地定位到C语言中的语句呢?回忆一下在<<Linux应用程序开发班>>中学习gdb调试时,能够在调试中看到对应的源码。当时为了进行gdb调试,在编译时加入了-g选项,这样可以将调试信息加入到目标文件中。由此得到灵感,在这里也加入-g试试。进入实验代码目录2-3-1中的内核源码目录,修改其顶层Makefile,如图6-18所示。
![]() |
(点击查看大图)图6-18 内核调试信息开关 |
提示 可见编译模块时加入调试信息,需要定义CONFIG_DEBUG_INFO这个宏,由于它默认是关闭的,所以内核并没有启用-g选项,可以暂时把这个宏开关注释掉,使KBUILD_CFLAGS标识拥有-g选项。
(6)修改内核Makefile后,再次编译模块。然后将新得到的oops.ko反汇编,使用命令:arm-linux-objdump -D -S oops.ko >log。用vim打开log文件。这时,通过汇编混合C语言调试信息的结果,结合前面的分析,可以很轻松的定位到C语言的错误语句就出现在"*p = a + 5"处,如图6-19所示。
![]() |
(点击查看大图)图6-19 定位出错的C语言语句 |
5.总结
通过本节实验,可以学会oops信息的分析方法,掌握利用oops信息调试内核模块的基本方法。下面列出利用oops定位出错点的基本步骤。
(1)oops出错时,首先搜集到所有oops打印信息,如果模块中本身有很多printk打印语句,首先根据oops开头的打印信息分析出错点的大概位置。
(2)通过Backtrace字段,分析发生oops错误前模块程序的执行路径,将范围缩小到某个函数中。
(3)如果通过前面两步仍无法定位出错点,那么就直接通过PC来定位。查看出错时
oops信息中打印出的PC的值并记录下来。在内核Makefile中加入-g选项,重新编译模块。
(4)通过objdump反汇编该模块。在反汇编得出的汇编混合C语言调试信息的代码中,结合前3步的分析结论,精确定位出错点的位置。