工欲善其事必先利其器,写驱动掌握调试办法将事半功倍。本文参考韦东山和宋宝华驱动调试的办法做总结。
1. 利用内核打印函数printk()
在linux中, printk()会将内核信息输出到内核信息缓冲区中。内核信息缓冲区是一个环形缓冲区(ring buffer),因此,如果塞入的消息过多,就会将之前的消息冲刷掉;环形缓冲区的数据,兵分两路,一路输出到控制台,另一路通过/proc/kmsg文件读取缓冲区。用户可以通过cat /proc/kmsg或者mesg显示内核信息;printk常用到的格式
printk("funtion=%s line=%d",__FUNTION__,__LINE__);
其中FUNTION是printk被函数引用的函数名,LINE是printk所在行号,这两个参数对初学者来说非常友好。
2.利用虚拟文件系统/proc
在linux系统中,可以用下面函数创建/proc文件节点
struct proc_dir_entry *create_proc_entry(const char *name, mode_t mode,struct proc_dir_entry *parent);
create_proc_entry()函数用于创建/proc 节点,参数 name 为/proc 节点的名称,parent/base为父目录的节点,如果为 NULL,则指/proc 目录。
下面函数函数用于创建/proc目录。
struct proc_dir_entry *proc_mkdir(const char *name, struct proc_dir_entry *parent);
有时候我们希望驱动打印信息不要和其他驱动混在/proc/dmsg上,这时候我们可以仿造/proc/dmsg创建一个/proc节点。定义一个类似printk函数使信息被自己创建的节点读出。
代码清单1
/*以下代码是创建文件节点*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
#include <linux/proc_fs.h>
#define LOGBUF_LEN 30
char mylog_buf[LOGBUF_LEN];//定义环形缓冲区
char tmp_buf[LOGBUF_LEN];
static int log_r=0;
static int log_w=0;
static int log_r_start=0;
struct proc_dir_entry *entry;//定义文件节点
DECLARE_WAIT_QUEUE_HEAD( mylog_wait);
static int log_isempty_start(void)
{
if(log_r_start==log_w)
return 1;
else
return 0;
}
static int log_isfull(void)
{
if((log_w+1)%LOGBUF_LEN==log_r)
{
return 1;
}
else
return 0;
}
static int log_read_start(char *p)
{
if(log_isempty_start())
return 0;
*p=mylog_buf[log_r_start];
log_r_start=(log_r_start+1)%LOGBUF_LEN;
return 1;
}
static void log_write(char c)
{
if(log_isfull())
{
log_r=(log_r+1)%LOGBUF_LEN;
if((log_r_start+1)%LOGBUF_LEN==log_r)
log_r_start=log_r;
}
mylog_buf[log_w]=c;
log_w=(log_w+1)%LOGBUF_LEN;
wake_up_interruptible(&mylog_wait);
}
/*参考内核输出函数定义类似printk函数,写数据到环形环形缓冲区上*/
int my_printk(const char *fmt, ...)
{
va_list args;
int i,j;
va_start(args, fmt);
i=vsprintf(tmp_buf,fmt,args);
va_end(args);
for(j=0;j<i;j++)
{
log_write(tmp_buf[j]);
}
return i;
}
static int my_msg_open(struct inode * inode, struct file * file)
{
log_r_start=log_r;
return 0;
}
static ssize_t my_msg_read(struct file *file, char __user *buf,
size_t count, loff_t *ppos)
{
size_t i=0;
int error = 0;
char c;
if ((file->f_flags & O_NONBLOCK) && log_isempty_start() )
return -EAGAIN;
error = wait_event_interruptible(mylog_wait,!log_isempty_start());
while (!error && i<count && log_read_start(&c)) {
error = __put_user(c,buf);
buf++;
i++;
}
if(!error)
error=i;
return error;
}
const struct file_operations my_msg_operations = {
.open = my_msg_open,
.read = my_msg_read,
};
static int my_msg_init(void)
{
entry = create_proc_entry("my_msg", S_IRUSR, &proc_root);
if (entry)
entry->proc_fops = &my_msg_operations;
return 0;
}
static void my_msg_exit(void)
{
remove_proc_entry("mymsg",&proc_root);
}
module_init(my_msg_init);
module_exit(my_msg_exit);
EXPORT_SYMBOL(my_printk);//输出函数别的驱动才可以使my_printk函数
MODULE_LICENSE("GPL");
以下代码是对my_printk()函数的使用,忽略其他部分,重点看button_drv_init和button_drv_exit。
代码清单2
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
static struct class *button_drv_class;
static struct class_device *button_drv_class_dev;
volatile unsigned long *gpfcon=NULL;
volatile unsigned long *gpfdat=NULL;
volatile unsigned long *gpgcon=NULL;
volatile unsigned long *gpgdat=NULL;
static int button_drv_open(struct inode *inode, struct file *file)
{
*gpfcon &=~((0x3<<0*2)|(0x3<<2*2));
*gpgcon &=~((0x3<<3*2)|(0x3<<11*2));
return 0;
}
ssize_t button_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
/*返回四个引脚的电平*/
unsigned char key_val[4];
int regval;
if(size!=sizeof(key_val))
return -EINVAL;
regval=*gpfdat;
key_val[0]=(regval&(1<<0)) ? 1:0;
key_val[1]=(regval&(1<<2)) ? 1:0;
regval=*gpgdat;
key_val[2]=(regval&(1<<3)) ? 1:0;
key_val[3]=(regval&(1<<11)) ? 1:0;
copy_to_user(buf,key_val,sizeof(key_val));/*内核向用户空间传递值*/
return sizeof(key_val);
}
static struct file_operations button_drv_fops =
{
.owner = THIS_MODULE, /* 这是一个宏,推向编译模块时自动创建的__this_module变量 */
.open = button_drv_open,
.read = button_drv_read,
};
int major;
int button_drv_init(void)/*驱动的入口函数*/
{
my_printk("button_drv_init\n");
major=register_chrdev(0,"button_drv",&button_drv_fops);/*注册函数,告诉内核*/
button_drv_class = class_create(THIS_MODULE, "button_drv");
button_drv_class_dev = class_device_create(button_drv_class, NULL, MKDEV(major, 0), NULL, "button"); /* /dev/xyz */
gpfcon=(volatile unsigned long*)ioremap(0x56000050,16);
gpfdat=gpfcon+1;
gpgcon=volatile unsigned long*)ioremap(0x56000060,16);
gpgdat=gpgcon+1;
return 0;
}
void button_drv_exit(void)/*驱动的出口函数*/
{
my_printk("button_drv_exit\n");
unregister_chrdev(major,"button_drv");
class_device_unregister(button_drv_class_dev);
class_destroy(button_drv_class);
iounmap(gpfcon);
iounmap(gpgcon);
}
module_init(button_drv_init);
module_exit(button_drv_exit);
MODULE_LICENSE("GPL");
输出效果
结果分析:insmod加载文件节点模块my_msg.ko,ls命令,显示/proc/my_msg;接着insmod加载使用例子模块button_drv.ko,利用cat命令查看/proc/my_msg内容,刚好与上面代码button_init函数中调用my_printk(“button_drv_init\n”)符合。
3.Oops错误
当内核出现 Segmentation Fault 时(例如内核访问一个并不存在的虚拟地址),Oops会被打印到控制台和写入系统 ring buffer。我们编写一个字符设备驱动,对代码清单2修改,使让它产生 Oops。
分析Oops错误:
Unable to handle kernel paging request at virtual address 56000050
pgd = c3c90000
[56000050] *pgd=00000000
Internal error: Oops: 5 [#1]
Modules linked in: button_drv
CPU: 0 Not tainted (2.6.22.6 #1)
PC is at button_drv_open+0x18/0x48 [button_drv]
(发生错误指令的地址)
LR is at chrdev_open+0x14c(指定的偏移)/0x164(函数的总大小)
pc : [<bf000018>] lr : [<c008d888>] psr: a0000013
sp : c0739e88 ip : c0739e98 fp : c0739e94
r10: 00000000 r9 : c0738000 r8 : c04de960
r7 : 00000000 r6 : 00000000 r5 : c3e9d0c0 r4 : c06dc5a0
r3 : bf0009ec r2 : 56000050 r1 : c04de960 r0 : 00000000
Flags: NzCv IRQs on FIQs on Mode SVC_32 Segment user
Control: c000717f Table: 33c90000 DAC: 00000015
Process button_drv_test (pid: 778, stack limit = 0xc0738258)
(当前进程·的·名称)
Stack: (0xc0739e88 to 0xc073a000)
(栈)
9e80: c0739ebc c0739e98 c008d888 bf000010 00000000 c04de960
9ea0: c3e9d0c0 c008d73c c0474da0 c3d06b40 c0739ee4 c0739ec0 c0089e48 c008d74c
9ec0: c04de960 c0739f04 00000003 ffffff9c c002c044 c3ed9000 c0739efc c0739ee8
9ee0: c0089f64 c0089d58 00000000 00000002 c0739f68 c0739f00 c0089fb8 c0089f40
9f00: c0739f04 c3d06b40 c0474da0 00000000 00000000 c3c91000 00000101 00000001
9f20: 00000000 c0738000 c046d8c8 c046d8c0 ffffffe8 c3ed9000 c0739f68 c0739f48
9f40: c008a16c c009fc70 00000003 00000000 c04de960 00000002 becb2ecc c0739f94
9f60: c0739f6c c008a2f4 c0089f88 000084f0 becb2ec4 000085c4 00008628 00000005
9f80: c002c044 4013365c c0739fa4 c0739f98 c008a3a8 c008a2b0 00000000 c0739fa8
9fa0: c002bea0 c008a394 becb2ec4 000085c4 000086d8 00000002 becb2ecc 00000000
9fc0: becb2ec4 000085c4 00008628 00000001 000084f0 00000000 4013365c becb2e98
9fe0: 00000000 becb2e6c 0000266c 400c98e0 60000010 000086d8 00000000 00000000
Backtrace:(回溯信息)
[<bf000000>] (button_drv_open+0x0/0x48 [button_drv]) from [<c008d888>] (chrdev_open+0x14c/0x164)
[<c008d73c>] (chrdev_open+0x0/0x164) from [<c0089e48>] (__dentry_open+0x100/0x1e8)
r8:c3d06b40 r7:c0474da0 r6:c008d73c r5:c3e9d0c0 r4:c04de960
[<c0089d48>] (__dentry_open+0x0/0x1e8) from [<c0089f64>] (nameidata_to_filp+0x34/0x48)
[<c0089f30>] (nameidata_to_filp+0x0/0x48) from [<c0089fb8>] (do_filp_open+0x40/0x48)
r4:00000002
[<c0089f78>] (do_filp_open+0x0/0x48) from [<c008a2f4>] (do_sys_open+0x54/0xe4)
r5:becb2ecc r4:00000002
[<c008a2a0>] (do_sys_open+0x0/0xe4) from [<c008a3a8>] (sys_open+0x24/0x28)
[<c008a384>] (sys_open+0x0/0x28) from [<c002bea0>] (ret_fast_syscall+0x0/0x2c)
Code: e24cb004 e59f302c e3a00000 e5932000 (e5923000)
Segmentation fault
解决步骤:
1.根据错误信息找到发生段错误的pc值 2.判断发生错误的pc地址是加载的模块还是内核。查看内核文件System.map可以查看所有内核函数的地址范围,查看/proc/Kallsyms可以查看加载模块函数和内核函数的地址范围。3.根据步骤2反汇编内核或者加载模块。4.结合出错信息和反汇编文件找出错误根源。
以下示例是把出错代码放进内核编译出错的情况。pc值是c019ad84
在System.map文件中pc值查找结果如下
由此断定发生错误在内核函数,反汇编内核函数,查看反汇编文件如下,找到c019ad84的位置。
由上面Oops错误信息可知,r2=56000050,r3=c03b3114。ldr r3,[r2]就是把地址为r2的值读入到r3,可判断出错的地方为修改的地方1。
4.修改系统时钟
驱动程序进入死循环会导致卡死现象。系统时钟就像人的心脏,一定时间就会引发中断,进入中断处理函数,只要在中断函数中打印出当前进程的pid和pc值,就可找到导致死循环的地方。下面示例在内核文件./arch/asm/kernel/irq.c中断处理函数处添加代码
这样,即使出现卡死现象,控制台也会定时将pid值和当前pc值打印到屏幕。驱动调试暂且总结到这里,后期会补充其他调试办法。