驱动程序开发的重点就在于驱动、系统的调试了,因此,掌握linux驱动的调试方法,是linux系统工程师必须掌握的技能。这里介绍驱动开发中常用的几种调试手段:
l
利用printk
l
查看OOP
消息
l
利用strace
l
利用内核内置的hacking
选项
l
利用ioctl
方法
l
利用/proc
文件系统
l
使用kgdb
1.1.1 利用printk
这是驱动开发中最朴实无华,同时也是最常用和有效的手段。printk函数会将内核信息输出到内核信息缓冲区中。为了避免缓冲区数据无限扩大,它使用了环形缓冲区的机制。当然,如果塞入的信息过多,之前的信息将会被冲刷掉。
在系统启动的过程中,我们可以使用串口终端监控打印信息,这些打印信息有助于我们分析系统的运行情况。进入系统之后,我们仍然可以使用dmesg命令查看。有时候我们需要打印更多的信息,有时候我们需要部分信息就可以了,在某种情况下,我们甚至不需要任何打印信息,但是设备要能正常工作。打印信息能够帮助分析系统运行情况的同时,它也能帮助“外人”查看设备信息,造成数据外泄。比如POS机,真正通过过银行标准认证的POS机是不允许有内核打印信息的。这时候,printk函数就大显身手了。
printk
的功能与我们经常在应用程序中使用的printf
是一样的,不同之处在于printk
可以在打印字符串前面加上内核定义的宏,这个宏定义了printk
的消息级别。printk
函数定义了8
个级别,依次对应级别0~7
,其数值越大,表示级别越低,表明对应的消息越不重要。第0
级为紧急事件级,第7
级为调试级,如下清单为printk
的级别定义。
- #define KERN _ EMERG "<0>" /* 紧急事件,一般是系统崩溃之前提示的消息 */
- #define KERN _ ALERT "<1>" /* 必须立即采取行动 */
- #define KERN _ CRIT "<2>" /* 临界状态,通常涉及严重的硬件或软件操作失败 */
- #define KERN _ ERR "<3>" /* 用于报告错误状态,设备驱动程序会经常使用KERN _ ERR来报告来自 硬件的问题 */
- #define KERN _ WARNING "<4>" /* 对可能出现问题的情况进行警告,这类情况通常不会对系统造成严重 问题 */
- #define KERN _ NOTICE "<5>" /* 有必要进行提示的正常情形,许多与安全相关的状况用这个级别进行 汇报 */
- #define KERN _ INFO "<6>" /* 内核提示性信息,很多驱动程序在启动的时候,以这个级别打印出它 们找到的硬件信息 */
- #define KERN _ DEBUG "<7>" /* 用于调试信息 */
复制代码
/proc/sys/kernel/printk文件表述了printk的输出等级,我们可以查看当前输出等级,如下图所示:
可见,该文件有四个数值,它们对应的属性如下:
l
控制台日志级别:优先级高于该值的消息将被打印至控制台。
l
默认的消息日志级别:将用该优先级来打印没有优先级的消息。
l
最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级)。
l
默认的控制台日志级别:控制台日志级别的默认值。
也就是说,第一个7
表示级别高于(小于)7
的消息才会被输出到控制台,第二个4
表示如果调用printk
时没有指定消息级别(宏)则消息的级别为4
,第三个1
表示接受的最高(最小)级别是1
,第四个7
表示系统启动时第一个7
原来的初值是7
。
通过如下命令可以使Linux内核的任何printk都被输出:
- echo 8 > /proc/sys/kernel/printk
复制代码
我们在复杂驱动的开发过程中,为了调试会在源码中加入成百上千的printk语句。而当调试完毕形成最终产品的时候必然会将这些printk语句删除,这势必会增加我们的工作量。可以通过封装更高级的宏来减轻我们的工作量。
- #define LEDS_DEBUG
- #undef PDEBUG /* undef it, just in case */
- #ifdef LEDS_DEBUG
- #ifdef __KERNEL__
- /* This one if debugging is on, and kernel space */
- #define PDEBUG(fmt, args…) printk( KERN_EMERG "leds: " fmt, ## args)
- #else
- /* This one for user space */
- #define PDEBUG(fmt, args…) fprintf(stderr, fmt, ## args)
- #endif
- #else
- #define PDEBUG(fmt, args…) /* not debugging: nothing */
- #endif
- #undef PDEBUGG
- #define PDEBUGG(fmt, args…) /* nothing: it’s a placeholder */
复制代码
这时如果想打印调试消息,可以用PDEBUG,如果不想看到该调试消息,只需要简单的将PDEBUG改为PDEBUGG即可。而当我们调试完毕形成最终产品时,只需要简单地将第1行注释掉即可。
上边那一段代码中的__KERNEL__是内核中定义的宏,当我们编译内核(包括模块)时,它会被定义。如果不明白代码中的…和##,请查阅gcc关于预处理部分的相关资料。
1.1.1 查看Oops消息
Oops是指当一段正常运行的内核程序执行一段不可预知的程序时,所产生的程序偏差。它会以错误的打印提示告知用户。比如当内核访问一个并不存在的虚拟地址,程序跑偏等。Oops信息会被打印到控制台,并写入系统打印的环形缓冲区。在移植驱动的过程中,经常会遇到内核打印出一长串莫名其妙的信息,我们可以分析这些信息来还原程序执行现场。
我们编写一个简单的字符设备驱动,让他产生Oops,让读写函数均访问0地址,程序如下:
- #include <linux/module.h>
- #include <linux/init.h>
- #include <linux/kernel.h>
- #include <linux/sched.h>
- #include <linux/delay.h>
- #include <linux/platform_device.h>
- #include <mach/hardware.h>
- #include <asm/mach-types.h>
-
- static ssize_t x4412_Oops_read(struct device *dev, struct device_attribute *attr, char *buf)
- {
- int *p = 0;
- *p = 1;
-
- return 0;
-
- static ssize_t x4412_Oops_write(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
- {
- int *p = 0;
- *p = 1;
-
- return 0;
- }
-
- static DEVICE_ATTR(Oops, 0666, x4412_Oops_read, x4412_Oops_write);
-
- static struct attribute * x4412_Oops_sysfs_entries[] = {
- &dev_attr_Oops.attr,
- NULL,
- };
-
- static struct attribute_group x4412_Oops_attr_group = {
- .name = NULL,
- .attrs = x4412_Oops_sysfs_entries,
- };
-
- static int x4412_Oops_probe(struct platform_device *pdev)
- {
- return sysfs_create_group(&pdev->dev.kobj, &x4412_Oops_attr_group);
- }
-
- static int x4412_Oops_remove(struct platform_device *pdev)
- {
- sysfs_remove_group(&pdev->dev.kobj, &x4412_Oops_attr_group);
- return 0;
- }
-
- #define x4412_Oops_suspend NULL
- #define x4412_Oops_resume NULL
-
- static struct platform_driver x4412_Oops_driver = {
- .probe = x4412_Oops_probe,
- .remove = x4412_Oops_remove,
- .suspend = x4412_Oops_suspend,
- .resume = x4412_Oops_resume,
- .driver = {
- .name = "x4412-Oops",
- },
- };
-
- static struct platform_device x4412_Oops_device = {
- .name = "x4412-Oops",
- .id = -1,
- };
-
- static int __devinit x4412_Oops_init(void)
- {
- int ret;
-
- printk("x4412 Oops driver\r\n");
-
- ret = platform_device_register(&x4412_Oops_device);
- if(ret)
- printk("failed to register x4412 Oops device\n");
-
- ret = platform_driver_register(&x4412_Oops_driver);
- if(ret)
- printk("failed to register x4412 Oops driver\n");
-
- return ret;
- }
-
- static void x4412_Oops_exit(void)
- {
- platform_driver_unregister(&x4412_Oops_driver);
- }
-
- module_init(x4412_Oops_init);
- module_exit(x4412_Oops_exit);
-
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("lqm");
- MODULE_DESCRIPTION("x4412 Oops driver");
复制代码
本段程序使用了
sys
文件系统,在加载后,将会在
/sys/devices/platform
下产生
x4412-Oops
目录,对应
x4412_Oops
目录下会创立
Oops
文件,如下图所示:
当我们使用cat指令查看Oops内容时,将会触发x4412_Oops_read函数,使用echo指令给Oops填写内容时,将会触发x4412_Oops_write函数。在读写函数中,前面的程序清单中加粗标红的内容,将会访问0地址。
执行如下指令:
可以看到有如下打印信息出来:
- [root@x4412 x4412-Oops]# cat Oops
- [ 1569.887046] Unable to handle kernel NULL pointer dereference at virtual address 00000000
- [ 1569.894523] pgd = d6a90000
- [ 1569.896813] [00000000] *pgd=56f67831, *pte=00000000, *ppte=00000000
- [ 1569.902652] Internal error: Oops: 817 [#2] PREEMPT SMP
- [ 1569.907759] Modules linked in:
- [ 1569.910809] CPU: 0 Tainted: G D (3.0.15-9tripod #26)
- [ 1569.916909] PC is at x4412_Oops_read+0x18/0x20
- [ 1569.921317] LR is at dev_attr_show+0x28/0x50
- [ 1569.925570] pc : [<c022c3a4>] lr : [<c022e9b4>] psr: a0000013
- [ 1569.925594] sp : d6c73ed8 ip : d6c73ee8 fp : d6c73ee4
- [ 1569.937016] r10: bebbb988 r9 : 00001000 r8 : c05c5ef8
- [ 1569.942226] r7 : d6c73f70 r6 : d6c3ca80 r5 : d6937ea0 r4 : c0753afc
- [ 1569.948735] r3 : 00000000 r2 : 00000001 r1 : c0753afc r0 : 00000000
- ……
- [ 1570.824538] Backtrace:
- [ 1570.827017] [<c022c38c>] (x4412_Oops_read+0x0/0x20) from [<c022e9b4>] (dev_attr_show+0x28/0x50)
- [ 1570.835706] [<c022e98c>] (dev_attr_show+0x0/0x50) from [<c014d808>] (sysfs_read_file+0xa4/0x154)
- [ 1570.844420] r5:d6937ea0 r4:d6c3ca98
- [ 1570.848011] [<c014d764>] (sysfs_read_file+0x0/0x154) from [<c00fbbec>] (vfs_read+0xb4/0x148)
- [ 1570.856421] [<c00fbb38>] (vfs_read+0x0/0x148) from [<c00fbd5c>] (sys_read+0x44/0x74)
- [ 1570.864122] r8:00000000 r7:00000003 r6:00001000 r5:bebbb988 r4:d6c5b6e0
- [ 1570.870852] [<c00fbd18>] (sys_read+0x0/0x74) from [<c0051b40>] (ret_fast_syscall+0x0/0x30)
- [ 1570.879053] r9:d6c72000 r8:c0051ce8 r6:bebbb988 r5:00000003 r4:000aea20
- [ 1570.885754] Code: e24cb004 e3a03000 e3a02001 e1a00003 (e5832000)
- [ 1570.901538] ---[ end trace 7ced72ad36929528 ]---
- Segmentation fault
- [root@x4412 x4412-Oops]#
复制代码
以上一堆莫名其妙的信息,就是Oops信息。从第一行可以看到,程序出错的原因是因为访问了“Null pointer”。第七行给出了事发现场,即x4412_Oops_read函数偏离0x18个字节处。
我们可以通过反汇编来查询x4412_Oops_read函数偏离0x18个字节处对应的C代码。在ubuntu下进入对应的驱动生成目录:
- lqm@ubuntu:~/samba/x4412_ics_rtm_v10/kernel/drivers/char/oops$ ls
- built-in.o Kconfig Makefile modules.builtin modules.order x4412-oops.c x4412-oops.o