linux裝置驅動:從如何定位oops的程式碼行談驅動除錯方法
在普通的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等,這些除錯手段一般不太容易使用或者不太方便使用,所以這裡就不在介紹了。