驱动程序的调试
一. 打印: prink, 自制proc文件
UBOOT传入console=ttySAC0 console=tty1
1. 内核处理UBOOT传入的参数
console_setup
add_preferred_console // 我想用名为"ttySAC0"的控制台,先记录下来
2. 硬件驱动的入口函数里:
drivers/serial/s3c2410.c
register_console(&s3c24xx_serial_console);
3. printk
vprintk
/* Emit the output into the temporary buffer */
// 先把输出信息放入临时BUFFER
vscnprintf
// Copy the output into log_buf.
// 把临时BUFFER里的数据稍作处理,再写入log_buf
// 比如printk("abc")会得到"<4>abc", 再写入log_buf
// 可以用dmesg命令把log_buf里的数据打印出来重现内核的输出信息
// 调用硬件的write函数输出
release_console_sem();
call_console_drivers(_con_start, _log_end);
// 从log_buf得到数据,算出打印级别
_call_console_drivers(start_print, cur_index, msg_level);
// 如果可以级别够格打印
if ((msg_log_level < console_loglevel
__call_console_drivers
con->write(con, &LOG_BUF(start), end - start);
二. 根据内核打印的段错误信息分析
oops信息 : 单词oops的含义是“惊讶”,当内核出错时(比如访问非法地址),打印出来的信息被称为oops信息。
a). 作为模块:
1,根据PC值,找到导致错误的指令。
pc = 0x00000000 它属于什么的地址?是内核的地址,还是通过insmod加载的驱动程序的地址?
先判断是否属于内核的地址 : 看 内核编译makefile目录下的 System.map(编译完内核都会发现在内核根目录下面多出来一个System.map文件)
确定内核的函数的地址范围 : c0004000~c03faa94。
所以可以确定 : 导致错误的指令不在内核的地址范围,则它属于insmod加载的驱动程序的地址范围。
2,假设它的加载的驱动程序引入的错误。那又怎么确定是哪一个驱动程序?
有时候 Modules linked in: 会指明是哪个驱动程序,但是很多时候加载的驱动程序很多,是不会指明具体是哪个。
所以还是需要根据PC值来确定究竟是哪个驱动程序。
先看看加载的驱动程序的地址范围。
在开发板目录下 : cat /proc/kallsyms >> kallsyms.txt (内核函数的地址、加载的函数的地址)
kallsyms.txt文件中的内容介绍 //T : 表示全局函数 t : 表示静态函数
从这些信息里找到一个相近的地址, 这个地址<=0xbf000018
比如找到了:
bf000000 t first_drv_open [first_drv]
3. 找到了first_drv.ko
在PC上反汇编它: arm-linux-objdump -D lcd.ko > lcd.dis
在dis文件里找到first_drv_open
first_drv.dis文件里 insmod后
00000000 : bf000000 t first_drv_open [first_drv]
00000018 pc = bf000018
18: e5923000 ldr r3, [r2] //r2的值在下面可以找到,是56000050
此时,要通过汇编语言来找到对应的c语言的语句。考验汇编能力的时候。
./firstdrvtest on
//1,一段文本描述信息
Unable to handle kernel paging request at virtual address 56000050
内核使用56000050来访问时发生了错误
pgd = c3eb0000
[56000050] *pgd=00000000
//2,oops信息的序号,#1,表示是第1次。
Internal error: Oops: 5 [#1]
//3,内核中加载的模块的名称
Modules linked in: first_drv
//4,发送错误时,CPU的序号,对于单处理器系统,序号为0。
CPU: 0 Not tainted (2.6.22.6 #1)
//5,PC就是发生错误时,指令的地址。
//大多时候,PC值只会给出一个地址,不会指示说是在哪个函数里面。
PC is at first_drv_open+0x18(该指令的偏移)/0x3c(该函数的总大小) [first_drv]
PC就是发生错误的指令的地址
大多时候,PC值只会给出一个地址,不到指示说是在哪个函数里
//__init_begin = c0008000, PC=__init_begin+0x3fff8000=
//6,LR寄存器的值。
LR is at chrdev_open+0x14c/0x164
LR寄存器的值
//7,发送错误时,CPU各个寄存器的值。
pc = 0xbf000018
pc : [] lr : [] psr: a0000013
sp : c3c7be88 ip : c3c7be98 fp : c3c7be94
r10: 00000000 r9 : c3c7a000 r8 : c049abc0
r7 : 00000000 r6 : 00000000 r5 : c3e740c0 r4 : c06d41e0
r3 : bf000000 r2 : 56000050 r1 : bf000964 r0 : 00000000
执行这条导致错误的指令时各个寄存器的值
Flags: NzCv IRQs on FIQs on Mode SVC_32 Segment user
Control: c000717f Table: 33eb0000 DAC: 00000015
//8,发生错误时,当前进程是它,并不是说发生错误的是这个进程
Process firstdrvtest (pid: 777, stack limit = 0xc3c7a258)
//发生错误时当前进程的名称是firstdrvtest
//9,栈信息
Stack: (0xc3c7be88 to 0xc3c7c000)
be80: c3c7bebc c3c7be98 c008d888 bf000010 00000000 c049abc0
bea0: c3e740c0 c008d73c c0474e20 c3e766a8 c3c7bee4 c3c7bec0 c0089e48 c008d74c
bec0: c049abc0 c3c7bf04 00000003 ffffff9c c002c044 c3d10000 c3c7befc c3c7bee8
bee0: c0089f64 c0089d58 00000000 00000002 c3c7bf68 c3c7bf00 c0089fb8 c0089f40
bf00: c3c7bf04 c3e766a8 c0474e20 00000000 00000000 c3eb1000 00000101 00000001
bf20: 00000000 c3c7a000 c04a7468 c04a7460 ffffffe8 c3d10000 c3c7bf68 c3c7bf48
bf40: c008a16c c009fc70 00000003 00000000 c049abc0 00000002 bec1fee0 c3c7bf94
bf60: c3c7bf6c c008a2f4 c0089f88 00008520 bec1fed4 0000860c 00008670 00000005
bf80: c002c044 4013365c c3c7bfa4 c3c7bf98 c008a3a8 c008a2b0 00000000 c3c7bfa8
bfa0: c002bea0 c008a394 bec1fed4 0000860c 00008720 00000002 bec1fee0 00000001
bfc0: bec1fed4 0000860c 00008670 00000002 00008520 00000000 4013365c bec1fea8
bfe0: 00000000 bec1fe84 0000266c 400c98e0 60000010 00008720 00000000 00000000
//10,栈回溯信息,可以从中看出函数调用关系:从最后一个函数 sys_init_module 开始,向上可以找到函数调用的关系。
//可以通过内核配置信息 make menuconfig 来指定是否输出 栈回溯信息。
Backtrace: (回溯)
[] (first_drv_open+0x0/0x3c [first_drv]) from [] (chrdev_open+0x14c/0x164)
[] (chrdev_open+0x0/0x164) from [] (__dentry_open+0x100/0x1e8)
r8:c3e766a8 r7:c0474e20 r6:c008d73c r5:c3e740c0 r4:c049abc0
[] (__dentry_open+0x0/0x1e8) from [] (nameidata_to_filp+0x34/0x48)
[] (nameidata_to_filp+0x0/0x48) from [] (do_filp_open+0x40/0x48)
r4:00000002
[] (do_filp_open+0x0/0x48) from [] (do_sys_open+0x54/0xe4)
r5:bec1fee0 r4:00000002
[] (do_sys_open+0x0/0xe4) from [] (sys_open+0x24/0x28)
[] (sys_open+0x0/0x28) from [] (ret_fast_syscall+0x0/0x2c)
Code: e24cb004 e59f1024 e3a00000 e5912000 (e5923000)
Segmentation fault
#
b). 编入内核
Modules linked in:
CPU: 0 Not tainted (2.6.22.6 #2)
PC is at first_drv_open+0x18/0x3c
LR is at chrdev_open+0x14c/0x164
pc : [] lr : [] psr: a0000013
sp : c3a03e88 ip : c3a03e98 fp : c3a03e94
r10: 00000000 r9 : c3a02000 r8 : c03f3c60
r7 : 00000000 r6 : 00000000 r5 : c38a0c50 r4 : c3c1e780
r3 : c014e6a8 r2 : 56000050 r1 : c031a47c r0 : 00000000
Flags: NzCv IRQs on FIQs on Mode SVC_32 Segment user
Control: c000717f Table: 339f0000 DAC: 00000015
Process firstdrvtest (pid: 750, stack limit = 0xc3a02258)
1. 根据pc值确定该指令属于内核还是外加的模块
pc = c014e6c0 它属于什么的地址?是内核的地址,还是通过insmod加载的驱动程序的地址?
先判断是否属于内核的地址 : 看 内核编译makefile目录下的 System.map(编译完内核都会发现在内核根目录下面多出来一个System.map文件)
确定内核的函数的地址范围 : c0004000~c03faa94。
所以可以确定 : 导致错误的指令不在内核的地址范围,则它属于insmod加载的驱动程序的地址范围。
2. 反汇编内核: arm-linux-objdump -D vmlinux > vmlinux.dis
/*
vmlinux是未压缩的内核,vmlinux 是ELF文件,即编译出来的最原始的文件。用于kernel-debug,产生system.map符号表,不能用于直接加载,不可以作为启动内核。只是启动过程中的中间媒体
vmlinuz是可引导的、压缩的内核。“vm”代表“Virtual Memory”。Linux 支持虚拟内存,不像老的操作系统比如DOS有640KB内存的限制
*/
first_drv.dis文件中:搜c014e6c0
c014e6a8 :
c014e6a8: e1a0c00d mov ip, sp
c014e6ac: e92dd800 stmdb sp!, {fp, ip, lr, pc}
c014e6b0: e24cb004 sub fp, ip, #4 ; 0x4
c014e6b4: e59f1024 ldr r1, [pc, #36] ; c014e6e0 <.text+0x1276e0>
c014e6b8: e3a00000 mov r0, #0 ; 0x0
c014e6bc: e5912000 ldr r2, [r1]
c014e6c0: e5923000 ldr r3, [r2] // 在此出错 r2=56000050,在下面可以找到
c).根据栈信息分析函数调用过程(作为模块 或者 编入内核都可,下面的实验是作为模块)
# ./firstdrvtest on
Unable to handle kernel paging request at virtual address 56000050
pgd = c3e78000
[56000050] *pgd=00000000
Internal error: Oops: 5 [#1]
Modules linked in: first_drv
CPU: 0 Not tainted (2.6.22.6 #48)
PC is at first_drv_open+0x18/0x3c [first_drv]
LR is at chrdev_open+0x14c/0x164
pc : [] lr : [] psr: a0000013
1 根据PC确定出错位置,是在内核,还是在模块中,前面有详细的分析System.map
bf000018 属于 insmod的模块
bf000000 t first_drv_open [first_drv]
2 确定它属于哪个函数
有时候 Modules linked in: 会指明是哪个驱动程序,但是很多时候加载的驱动程序很多,是不会指明具体是哪个。
所以还是需要根据PC值来确定究竟是哪个驱动程序。
先看看加载的驱动程序的地址范围。
在开发板目录下 : cat /proc/kallsyms >> kallsyms.txt (内核函数的地址、加载的函数的地址)
kallsyms.txt文件中的内容介绍 //T : 表示全局函数 t : 表示静态函数
3 找到了first_drv.ko
在PC上反汇编它: arm-linux-objdump -D first_drv.ko > first_drv.dis
在dis文件里找到first_drv_open
first_drv.dis文件里 insmod后
00000000 : bf000000 t first_drv_open [first_drv]
00000018 pc = bf000018
first_drv.dis文件中:
c014e6a8 :
c014e6a8: e1a0c00d mov ip, sp
c014e6ac: e92dd800 stmdb sp!, {fp, ip, lr, pc}
//从这里可以看见 first_drv_open函数的栈信息,保存有4*4个字节
sp : c3e69e88 ip : c3e69e98 fp : c3e69e94
r10: 00000000 r9 : c3e68000 r8 : c0490620
r7 : 00000000 r6 : 00000000 r5 : c3e320a0 r4 : c06a8300
r3 : bf000000 r2 : 56000050 r1 : bf000964 r0 : 00000000
Flags: NzCv IRQs on FIQs on Mode SVC_32 Segment user
Control: c000717f Table: 33e78000 DAC: 00000015
Process firstdrvtest (pid: 752, stack limit = 0xc3e68258)
可以仅根据栈信息,来确定函数的调用关系。栈就是一块内存
分析如下 :
Stack: (0xc3e69e88 to 0xc3e6a000)
9e80: c3e69ebc c3e69e98 c008c888 bf000010 00000000 c0490620
fp ip 返回地址=lr pc
栈信息开始部分的4个数据 是first_drv_open 的 栈 chrdev_open is sp
first_drv_open执行完之后,返回地址lr=c008c888,再在 vmlinux.dis 中找到 调用者的函数。
4个数据后面的数据,就是它的调用者的函数的栈空间中的数据。
9ea0: c3e320a0 c008c73c c0465e20 c3e36cb4 c3e69ee4 c3e69ec0 c0088e48 c008c74c
lr=c0088e48
9ec0: c0490620 c3e69f04 00000003 ffffff9c c002b044 c06e0000 c3e69efc c3e69ee8
__dentry_open 的栈
9ee0: c0088f64 c0088d58 00000000 00000002 c3e69f68 c3e69f00 c0088fb8 c0088f40
lr=c0088f64 nameidata_to_filp的栈 lr=c0088fb8
9f00: c3e69f04 c3e36cb4 c0465e20 00000000 00000000 c3e79000 00000101 00000001
do_filp_open的栈
9f20: 00000000 c3e68000 c04c1468 c04c1460 ffffffe8 c06e0000 c3e69f68 c3e69f48
9f40: c008916c c009ec70 00000003 00000000 c0490620 00000002 be94eee0 c3e69f94
9f60: c3e69f6c c00892f4 c0088f88 00008520 be94eed4 0000860c 00008670 00000005
lr=c00892f4 do_sys_open的栈
9f80: c002b044 4013365c c3e69fa4 c3e69f98 c00893a8 c00892b0 00000000 c3e69fa8
lr=c00893a8 sys_open的栈
9fa0: c002aea0 c0089394 be94eed4 0000860c 00008720 00000002 be94eee0 00000001
lc=002aea0 ret_fast_syscall的栈
9fc0: be94eed4 0000860c 00008670 00000002 00008520 00000000 4013365c be94eea8
9fe0: 00000000 be94ee84 0000266c 400c98e0 60000010 00008720 00000000 00000000
ret_fast_syscall()函数是被谁调用的,我们这里还不需要详细了解,只要知道是应用程序是通过swi中断来加入内核的。
注意 : 上面的信息,从下往上,从大地址到小地址方向。是从栈底开始,往栈顶方向打印栈中的值。也是函数调用的方向。
三. 修改内核来定位系统僵死问题
1,\kernel-2.6.13\arch\arm\kernel\irq.c //修改这个内核文件
修改这个文件的原因 : 只要系统还在运行,即使某个驱动程序卡死了,系统时钟中断是绝对不会停歇的。
所以,可以在系统中断代码中加入一些调试代码,来帮助我们找到bug。
/*
* do_IRQ handles all hardware IRQ's. Decoded IRQs should not
* come via this function. Instead, they should provide their
* own 'handler'
*/
asmlinkage void asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct irqdesc *desc = irq_desc + irq;
static pid_t pre_pid;
static int count = 0;
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (irq >= NR_IRQS)
desc = &bad_irq_desc;
if(irq == 30) /*系统时钟中断*/
{
/*如果10秒之内,都是同一个进程在运行,就打印卡死信息。*/
/*静态局部变量:
只有在这个函数中能访问,但是生命周期是和全局变量差不多的,函数退出之后变量还在,
而且只在第一次进入的时候做初始化,以后会跳过初始化语句,保留原来的值*/
if(pre_pid == current->pid) /*当前进程 等于 之前记录下来的进程号*/
{
count++;
}
else /*当前进程 不等于 之前记录下来的进程号*/
{
count = 0;
pre_pid = current->pid; /*把新的当前进程记录下来。*/
}
if(count == 10*HZ) /*累计达到10秒的时候*/
{
count = 0;
/*明确是在哪个进程导致卡死的,明确PC值*/
printk("asm_do_IRQ==>s3c2410_timer_interrupt : pid=[%d], task_name=[%s], PC=[0x%08x]\n", current->pid, current->comm, regs->ARM_pc);
}
}
irq_enter();
spin_lock(&irq_controller_lock);
desc->handle(irq, desc, regs);
/*
* Now re-run any pending interrupts.
*/
if (!list_empty(&irq_pending))
do_pending_irqs(regs);
irq_finish(irq);
spin_unlock(&irq_controller_lock);
irq_exit();
}
重新编译内核 make clean;make
重新启动这个新的内核
2, 在这个驱动文件中加入死循环,来模拟系统僵死问题 \nfs_2.6.13\wxc\driver\chardriver\led\leddriver2.c
static int leddriver2_ioctl( struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)
{
//printk("DEVICE:mydriver2_ioctl Called!\n");
printk("DEVICE:cmd=[%d], arg=[%ld]\n", cmd, arg);
while(1); //故意加的
if(cmd == 0) //所有的灯全亮2s
{
....
}
return 0;
}
启动待测试的驱动程序
# insmod leddriver2.ko
[kernel/sys.c][notifier_call_chain][175]
[kernel/sys.c][notifier_call_chain][187]
DEVICE:leddriver2_init
DEVICE:register leddriver2 OK! Major = [253]
# mknod /dev/leddriver2 c 253 0
[root@EmbedSky ko]# ls -lrt /dev/led*
crw-r--r-- 1 root root 253, 0 Jan 1 00:03 /dev/leddriver2
启动应用程序
# ./leddrivertest2 0 3
APP:open fd=[3]
APP:led_no=[0] time=[3]
DEVICE:cmd=[0], arg=[3] //会发现驱动程序卡死在这里了,无论怎么也退出不了,下面打印出来了很多关键信息。
asm_do_IRQ==>s3c2410_timer_interrupt : pid=[806], task_name=[leddrivertest2], PC=[0xbf000044]
asm_do_IRQ==>s3c2410_timer_interrupt : pid=[806], task_name=[leddrivertest2], PC=[0xbf000044]
PC=0xbf000044
3,定位僵死发生的代码的具体位置
3.1), PC=0xbf000044
从这些信息里找到一个相近的地址, 这个地址<=0xbf000044
前面有分析 可知 地址 0xbf000044 不属于内核地址空间,属于外加的内核。
在开发板目录下 :
# cat /proc/kallsyms >> kallsyms.txt
在这个文件下面,查找 地址 0xbf000044 大概在什么函数
bf000000 t $a [leddriver2]
bf000048 t $d [leddriver2] // 对于中断, pc-4=0xbf000044 才是发生中断瞬间的地址,说明发生中断的驱动程序是 : leddriver2.ko。
bf002000 t leddriver2_init [leddriver2]
bf00004c t leddriver2_exit [leddriver2]
c481e178 ? __mod_license202 [leddriver2]
c481e184 ? __mod_description201 [leddriver2]
c481e1a4 ? __mod_author200 [leddriver2]
bf0008a0 b $d [leddriver2]
bf0008a4 b MYDRIVER_Major [leddriver2]
bf000698 d leddriver2_fops [leddriver2]
bf000698 d $d [leddriver2]
bf000028 t leddriver2_ioctl [leddriver2] //bf000028 是与 0xbf000044 最近的地址。说明 : 发生中断的驱动程序是 : leddriver2.ko。
bf000000 t leddriver2_open [leddriver2] //中断很有可能发生在 leddriver2_ioctl 函数中。
bf000014 t leddriver2_release [leddriver2]
bf000704 d GPBDAT_HIGH [leddriver2]
bf000714 d GPBDAT_LOW [leddriver2]
bf000724 d GPBUP_UPEN [leddriver2]
bf000734 d GPBCON_OUTP [leddriver2]
bf000744 d GPBCON_CLEAN [leddriver2]
bf000000 t $a [leddriver2]
bf000048 t $d [leddriver2]
bf002000 t $a [leddriver2]
bf002094 t $d [leddriver2]
bf00004c t $a [leddriver2]
bf000080 t $d [leddriver2]
3.2), 找到 leddriver2.ko
在PC上反汇编它:
在ubuntu目录下 :
# arm-linux-objdump -D leddriver2.ko > leddriver2.dis //PATH没有,就用下面一行
# /home/wangxc/linux/toolchain/crosstools_3.4.1_softfloat/arm-linux/gcc-3.4.1-glibc-2.3.3/bin/arm-linux-objdump -D leddriver2.ko > leddriver2.dis
在dis文件里找到 leddriver2_ioctl
00000028 : //insmod后 上面的地址 bf000028 对应在这里
28: e1a0c00d mov ip, sp
2c: e92dd800 stmdb sp!, {fp, ip, lr, pc}
30: e24cb004 sub fp, ip, #4 ; 0x4
34: e59f000c ldr r0, [pc, #12] ; 48 <.text+0x48>
38: e1a01002 mov r1, r2
3c: e1a02003 mov r2, r3
40: ebfffffe bl 40 // 对于中断, pc-4才是发生中断瞬间的地址,所以僵死的代码在这里
44: ea00000f b 88 //所以 0xbf000044 在这里
48: 00000000 andeq r0, r0, r0
Disassembly of section .init.text:
3.3), 分析汇编代码,找到对应的c语言代码
40: ebfffffe bl 40 //bl自己跳转到自己,就是一个死循环,
所以,c语言代码中的就是死循环的语句。
在驱动源码leddriver2_ioctl函数中寻找,可以找到 while(1);语句,说明僵死就发生在这里。