在普通的c应用程序中,我们经常使用printf来输出信息,或者使用gdb来调试程序,那么驱动程序如何调试呢?我们知道在调试程序时经常遇到的问题就是野指针或者数组越界带来的问题,在应用程序中运行这种程序就会报segmentation fault的错误,而由于驱动程序的特殊性,出现此类情况后往往会直接造成系统宕机,并会抛出oops信息。那么我们如何来分析oops信息呢,甚至根据oops信息来定位具体的出错的代码行呢?下面就根据一个简单的实例来说明如何调试驱动程序。
如何根据oops定位代码行
我们借用linux设备驱动第二篇:构造和运行模块里面的hello world程序来演示出错的情况,含有错误代码的hello world如下:
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
static int hello_init(void)
{
char *p = NULL;
memcpy(p,"test", 4);
printk(KERN_ALERT "hello , world\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "goodBye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
Makefile文件如下:
ifneq ($(KERNELRELEASE),)
obj-m := hello_world.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
很明显,以上代码的第8行是一个空指针错误。insmod后会出现下面的oops信息:
下面简单分析下oops信息的内容。
由BUG: unable to handle kernel NULL pointer dereference at (null)知道出错的原因是使用了空指针。标红的部分确定了具体出错的函数。Modules linked in: helloworld表明了引起oops问题的具体模块。call trace列出了函数的调用信息。这些信息中其中标红的部分是最有用的,我们可以根据其信息找到具体出错的代码行。下面就来说下,如何定位到具体出错的代码行。
第一步我们需要使用objdump把编译生成的bin文件反汇编,我们这里就是helloworld.o,如下命令把反汇编信息保存到err.txt文件中:
objdump helloworld.o -D > err.txt
err.txt内容如下:
hello_world.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <init_module>:
0: e8 00 00 00 00 callq 5 <init_module+0x5>
5: 55 push %rbp
6: 48 c7 c7 00 00 00 00 mov $0x0,%rdi
d: c7 04 25 00 00 00 00 movl $0x74736574,0x0
14: 74 65 73 74
18: 48 89 e5 mov %rsp,%rbp
1b: e8 00 00 00 00 callq 20 <init_module+0x20>
20: 31 c0 xor %eax,%eax
22: 5d pop %rbp
23: c3 retq
24: 66 90 xchg %ax,%ax
26: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
2d: 00 00 00
0000000000000030 <cleanup_module>:
30: e8 00 00 00 00 callq 35 <cleanup_module+0x5>
35: 55 push %rbp
36: 48 c7 c7 00 00 00 00 mov $0x0,%rdi
3d: 48 89 e5 mov %rsp,%rbp
40: e8 00 00 00 00 callq 45 <cleanup_module+0x15>
45: 5d pop %rbp
46: c3 retq
Disassembly of section .rodata.str1.1:
0000000000000000 <.rodata.str1.1>:
0: 01 31 add %esi,(%rcx)
2: 68 65 6c 6c 6f pushq $0x6f6c6c65
7: 20 2c 20 and %ch,(%rax,%riz,1)
a: 77 6f ja 7b <cleanup_module+0x4b>
c: 72 6c jb 7a <cleanup_module+0x4a>
e: 64 0a 00 or %fs:(%rax),%al
11: 01 31 add %esi,(%rcx)
13: 67 6f outsl %ds:(%esi),(%dx)
15: 6f outsl %ds:(%rsi),(%dx)
16: 64 42 79 65 fs rex.X jns 7f <cleanup_module+0x4f>
1a: 2c 20 sub $0x20,%al
1c: 63 72 75 movslq 0x75(%rdx),%esi
1f: 65 6c gs insb (%dx),%es:(%rdi)
21: 20 77 6f and %dh,0x6f(%rdi)
24: 72 6c jb 92 <cleanup_module+0x62>
26: 64 0a 00 or %fs:(%rax),%al
Disassembly of section .modinfo:
0000000000000000 <__UNIQUE_ID_license0>:
0: 6c insb (%dx),%es:(%rdi)
1: 69 63 65 6e 73 65 3d imul $0x3d65736e,0x65(%rbx),%esp
8: 44 75 61 rex.R jne 6c <cleanup_module+0x3c>
b: 6c insb (%dx),%es:(%rdi)
c: 20 42 53 and %al,0x53(%rdx)
f: 44 2f rex.R (bad)
11: 47 50 rex.RXB push %r8
13: 4c rex.WR
...
Disassembly of section .comment:
0000000000000000 <.comment>:
0: 00 47 43 add %al,0x43(%rdi)
3: 43 3a 20 rex.XB cmp (%r8),%spl
6: 28 55 62 sub %dl,0x62(%rbp)
9: 75 6e jne 79 <cleanup_module+0x49>
b: 74 75 je 82 <cleanup_module+0x52>
d: 20 35 2e 34 2e 30 and %dh,0x302e342e(%rip) # 302e3441 <cleanup_module+0x302e3411>
13: 2d 36 75 62 75 sub $0x75627536,%eax
18: 6e outsb %ds:(%rsi),(%dx)
19: 74 75 je 90 <cleanup_module+0x60>
1b: 31 7e 31 xor %edi,0x31(%rsi)
1e: 36 2e 30 34 2e ss xor %dh,%cs:(%rsi,%rbp,1)
23: 39 29 cmp %ebp,(%rcx)
25: 20 35 2e 34 2e 30 and %dh,0x302e342e(%rip) # 302e3459 <cleanup_module+0x302e3429>
2b: 20 32 and %dh,(%rdx)
2d: 30 31 xor %dh,(%rcx)
2f: 36 30 36 xor %dh,%ss:(%rsi)
32: 30 39 xor %bh,(%rcx)
...
Disassembly of section __mcount_loc:
0000000000000000 <__mcount_loc>:
由oops信息我们知道出错的地方是hello_init的地址偏移0xd。而有dump信息知道,hello_init的地址即init_module的地址,因为hello_init即本模块的初始化入口,如果在其他函数中出错,dump信息中就会有相应符号的地址。由此我们得到出错的地址是0xd,下一步我们就可以使用addr2line来定位具体的代码行:
addr2line -C -f -e helloworld.o d
此命令就可以得到行号了。以上就是通过oops信息来定位驱动崩溃的行号。
其他调试手段
以上就是通过oops信息来获取具体的导致崩溃的代码行,这种情况都是用在遇到比较严重的错误导致内核挂掉的情况下使用的,另外比较常用的调试手段就是使用printk来输出打印信息。printk的使用方法类似printf,只是要注意一下打印级别,详细介绍在linux设备驱动第二篇:构造和运行模块中已有描述,另外需要注意的是大量使用printk会严重拖慢系统,所以使用过程中也要注意。
以上两种调试手段是我工作中最常用的,还有一些其他的调试手段,例如使用/proc文件系统,使用trace等用户空间程序,使用gdb,kgdb等,这些调试手段一般不太容易使用或者不太方便使用,所以这里就不在介绍了。