arm-linux驱动调试方法

  • Post author:
  • Post category:linux


工欲善其事必先利其器,写驱动掌握调试办法将事半功倍。本文参考韦东山和宋宝华驱动调试的办法做总结。




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值打印到屏幕。驱动调试暂且总结到这里,后期会补充其他调试办法。



版权声明:本文为yuanzhexu原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。