基于ARM的Linux驱动调试技术研究
2016-09-26巩琛蔡文
巩 琛 蔡 文
(上海师范大学信息与机电工程学院 上海 200234)
基于ARM的Linux驱动调试技术研究
巩琛蔡文
(上海师范大学信息与机电工程学院上海 200234)
在ARM上进行Linux驱动移植时,要对Linux内核代码进行修改、删减或添加,但这样做在运行时可能会遇到很多意想不到的错误,这时就需要去调试代码以找到出错的原因和位置。针对这一需要,提出并实现两种新的调试技术:第一种构造一个打印函数,把添加的打印信息单独存储,然后借助proc文件系统将其输出,实现了外加打印信息与内核自身打印信息的分离,使查找更加方便;其次利用系统时钟中断永不停息的特性确定系统僵死的位置。通过实验表明,该技术能快速有效地找到死循环的位置,省去了大量查找和分析代码的工作。
Linux调试proc文件系统系统时钟中断
0 引 言
如今,以ARM为核心处理器,搭载Linux操作系统的嵌入式产品越来越多,应用场景越来越广泛[1]。但由于硬件平台的多样性,并没有一套通用的Linux操作系统。开发相应的嵌入式产品时,需要工程师根据硬件平台外围设备的特性对Linux内核源码进行增减。这样一个移植过程中会出现各种各样的bug,此时就需要开发者运用各种调试手段去找到出错的地方和原因。本文基于Linux+ARM的实验平台(Linux选取2.6.31.14的版本;ARM架构芯片采用三星公司的s3c2440;BootLoader采用U-boot,根文件系统用Busybox创建[2,3]),提出并实现两种新的驱动调试技术。文中涉及到的交叉开发方面的知识不作详述。
1 构建proc文件输出私有调试信息
1.1整体思想概述
在调试Linux驱动时在程序里用printk()添加打印语句是最常用的方法[4]。首先,Linux内核会在内核空间分配一段静态缓冲区,作为显示用的空间。然后调用sprintf,格式化显示字符串,最后调用tty_write向终端进行信息的显示。执行dmesg命令,就可把缓冲区里面的打印信息重新显示在硬件终端上。dmesg命令的实质是打开/proc/kmsg文件。
Linux内核源码多用printk()打印信息,而开发人员在调试驱动时也常常会用printk()添加调试信息。当开发者想回头查看自己添加的打印信息时,用简单的cat命令去打开 /proc/kmsg文件即可。但此时显示的是与内核打印信息混杂在一起的结果,查找起来很不方便。cat命令的原理是先执行open系统调用打开某个文件,然后再在一个while()里用read系统调用循环地读取该文件里面的内容,然后把内容显示到硬件输出端。cat命令的机制原理不是本文的重点,这里只是简单介绍一下,便于后面代码的理解。
基于此问题,本文构造出一个私有打印函数pvt_printk(),把自己添加的打印语句单独存储到一段缓冲区pvt_buf里面去。然后利用内核的proc机制创建一个与kmsg类似的文件pvt_kmsg。当去执行cat/proc/pvt_kmsg时,可把pvt_buf缓冲区里面的数据显示出来。
1.2proc文件系统
本调试技术用到了Linux内核的proc虚拟文件系统,它是一种在用户态检查内核状态的机制。里面的内容是内核动态生成的。它的存在主要是想通过这样一种渠道把内核的一些状态信息反馈给用户,让用户能够了解到内核运行的一些状况[5]。
Linux内核当中使用structproc_dir_entry结构体来表示和描述/proc目录下的一个文件或者目录。如下:
structproc_dir_entry
{
…… ……
conststructfile_operations*proc_fops;
…… ……
}
这个结构里面成员比较多,其他的不用太关心,这里着重介绍file_operations这个结构体,它与对proc文件的具体操作相对应[5]。Linux内核当中是用create_proc_entry这个函数去创建一个proc文件,返回值为一个proc_dir_entry结构体指针。
structproc_dir_entry*create_proc_entry(constchar
*name,mode_tmode,structproc_dir_entry*parent)
name: 要创建的文件名。
mode: 要创建的文件属性。
parent: 这个文件的父目录。如果为NULL,便直接在proc的根目录下面去创建文件。
file_operations这个结构体的成员也很多,本文只用到open、read。
structfile_operations
{
……;
ssize_t(*read) (structfile*,char__user*,size_t,loff_t*);
ssize_t(*write) (structfile*,constchar__user*,size_t,loff_t*);
int(*open) (structinode*,structfile*);
……;
}
当应用程序(本实验就是cat命令)对一个proc文件执行open、read、write系统调用时[6],程序会通过SWI指令,最终调用到与此文件相对应的file_operations结构体里面的open、read、write函数。整个过程类似于一个典型的字符型设备驱动[7],/proc目录下的文件有点像字符型驱动里面的设备节点。
1.3实现过程及步骤
创建proc-printk.c文件作为该实验的驱动源码文件。经前面分析,用到proc文件的描述结构体proc_dir_entry和创建函数create_proc_entry(),所以程序除了添加字符型驱动所需的头文件,还需要包含Linux/proc_fs.h。
在驱动初始化函数里利用create_proc_entry()去创建一个proc_dir_entry结构体指针pvt_entry,并将其设为全局变量。如果创建成功,把file_operations结构体proc_pvt_entry_operations的指针赋给pvt_entry所指结构体里面的proc_fops成员。
structproc_dir_entry*pvt_entry;
staticintpvt_kmsg_init(void)
{
pvt_entry=create_proc_entry("pvt_kmsg",S_IRUSR,NULL);
if(pvt_entry)
pvt_entry->proc_fops=&proc_pvt_kmsg_operations;
return0;
}
S_IRUSR是Linux内核定义的宏,作用是让创建的文件的属性为只读。
staticvoidpvt_kmsg_exit(void)
{
remove_proc_entry("pvt_kmsg",NULL);/*在出口函数做相应的移除*/
}
module_init(pvt_kmsg_init);
module_exit(pvt_kmsg_exit);
MODULE_LICENSE("GPL");
接着构造并填充file_operations结构体proc_pvt_kmsg_operations。
structfile_operationsproc_pvt_kmsg_operations=
{
.read=pvt_kmsg_read,
.open=pvt_kmsg_open,
};
先实现pvt_kmsg_read函数,pvt_kmsg_open后面再说。
staticssize_tpvt_kmsg_read(structfile*file,char__user*buf,size_tcount,loff_t*ppos)
{
inti,ret;
charc;
//文件以非阻塞方式打开并且pvt_buf为空,立刻返回错误
if((file->f_flags&O_NONBLOCK) &&empty())
return-EAGAIN;
//如果文件以阻塞方式打开又为空,用wait_event_interruptible()让此进程在queue等待队列上睡眠[8]
//如果文件以阻塞方式打开有数据,用__put_user把数据读到用户空间
if(!wait_event_interruptible(queue,!empty()))
{
for(i=0;i //count是要读的字节数 { if(read_pvt_buf(&c)) { ret=__put_user(c,buf); //返回值:0表成功,-EFAULT表错误 buf++; } } } returnret; } 上述代码用等待队列来实现阻塞操作,所以需在文件开头用宏DECLARE_QUEUEEUE_HEAD(queue)去定义并初始化一个等待队列queue。 定义一个私有的存储区域staticcharpvt_buf[2048],对它的存贮读取方式采用数组环形缓冲区。在本设计里只有自定义的打印函数pvt_printk()往里写数据,只有cat应用程序从里读数据。在这样仅有一个读用户和一个写用户的情况下,使用环形缓冲区的好处是可以不用添加互斥[9]保护机制就能保证数据的正确性。下面是对环形缓冲区空、满判断函数和读写函数的实现。 staticcharpvt_buf[2048]; staticintread_p; staticintwrite_p= 0; staticintreadstart_p=0; //每次读的起始位置 staticintempty() { if(read_p==write_p)return1; elsereturn0; } staticintfull() { if((write_p+ 1)%2048 ==readstart_p)return1; elsereturn0; } staticvoidwrite_pvt_buf(charc) { /*如果数据满了移动一位读的起始位,丢弃一个数据 */ if(full()) readstart_p= (readstart_p+ 1) % 2048; pvt_buf[write_p] =c; write_p= (write_p+ 1) % 2048; wake_up_interruptible(&queue); //唤醒进程 } staticintread_pvt_buf(char*p) { if(empty())return0; *p=pvt_buf[read_p]; read_p= (read_p+1) % 2048; return1; } 接着实现私有打印函数pvt_printk。这里借助于内核提供的vsnprintf函数的存储功能进行改写。intvsnprintf(char*buf,size_tsize,constchar*fmt,va_listargs)把fmt指向的打印内容拷贝到*buf里面。 但为了实现环形缓冲区的顺序写入,这里不能直接将打印信息拷贝到pvt_buf。而是需先把打印信息存储到这个临时缓冲区temp[2048],再把临时缓冲区的信息拷贝到pvt_buf。 staticchartemp[2048]; intpvt_printk(constchar*fmt,…) { va_listargs; intj,k= 0; va_start(args,fmt); /*把打印信息放到临时缓冲区*/ j=vsnprintf(temp,INT_MAX,fmt,args); va_end(args); /*把临时缓冲区的数据放到环形缓冲区pvt_buf里 */ while(k write_pvt_buf(temp[k]); k++; } returnj; } 到此最初的构想已经可以实现了,但是这样做只能cat一次。因为在cat一次后读位置已经移动到最后面没有数据的地方。所以为了实现多次可读,得在每次cat调用open的时候把读位置调整到起始的地方。 staticintpvt_kmsg_open(structinode*inode,structfile*file) { read_p=readstart_p; return0; } 用EXPORT_SYMBOL(pvt_printk)将pvt_printk函数导出,供全部内核文件使用。 简单测试:在xx驱动源码里先用externintpvt_printk(constchar*fmt,…);声明,然后在合适位置添加pvt_printk(“abcdefg”),将此驱动和proc-printk.c都编译进内核,把新内核烧写到ARM开发板并启动。在开发板的串口终端下先运行与xx驱动对应的应用程序test1,再去cat创建的proc文件pvt_kmsg,就可以看到添加的信息。如图1所示。 图1 第一种调试方法实验结果 2.1原理 当驱动程序某个地方不小心写入了死循环的代码。那么在运行相应app的时候Linux系统就会出现僵死的状态。如果在多进程的状况话,直接追查代码找寻僵死点那将显得非常困难。 但Linux时钟中断确是永远都在发生的,进程的僵死是不会屏蔽掉系统时钟中断的。就像人一样,即便是在睡觉,心脏总是在跳动的。 Linux系统下的中断处理过程如图2所示。t是时间轴,一个进程在运行着,发生中断时,先保存现场,再执行中断函数,最后回复现场[10],只不过系统时钟中断是周而复始的这样执行。 图2 中断发生过程 保存现场其实就是保存各个寄存器的值。其中pc寄存器(ARM里由r15寄存器充当)就保存了执行中的驱动程序被打断处的地址。僵死状态时,进程会重复执行同一段代码,这样pc值会在一个很小的范围里变动。 本调试技术就是借助系统时钟中断永不停息这一特性,在中断入口函数里添加一些打印语句,把保存的pc值打印出来,再经过分析判断定位出僵死的大致位置了。 在本实验平台的Linux系统当中,一发生中断,CPU就强制跳到0xffff0018处(此地址根据CPU的架构不同而不同)执行异常向量表里面的bvector_irq+stubs_offset。其中“stubs_offset”用来重定位跳转的位置[11],这条汇编指令是去跳转执行vector_irq这个函数,在这个函数里面保存了一些现场,并经过复杂的汇编代码后会调用到asm_do_IRQ(archarmkernelIrq.c)这个函数。它是中断处理函数的总入口[12],在它内部经过层层的函数调用后,会最终执行到时钟中断处理函数s3c2410_timer_interrupt()(archarmplat-s3c24xx ime.c)。选择在s3c2410_timer_interrupt()或asm_do_IRQ()里加打印信息都可以,这里选择asm_do_IRQ(),因为在asm_do_IRQ()的传入参数中有一个pt_regs结构体,它作用就是保存发生中断时的现场。pt_regs结构体里面定义了一个长度为18的数组uregs,其中uregs[15]保存的就是pc寄存器的值,正好可以利用它把pc值打印出来。但这里需注意一点,中断保存的pc值其实是实际指令地址+4。 asmlinkagevoid__exceptionasm_do_IRQ(unsignedintirq,structpt_regs*regs) structpt_regs{ longuregs[18]; }; …… #defineARM_pcuregs[15] …… 2.2实现步骤 先在一个正确的驱动程序里加入一段死循环代码。本文在mydrive.c这个驱动源码的读函数mydrive_read()里加一句for(;;)。 staticssize_tmydrive_read(structfile*file,constchar__user*buf,size_tcount,loff_t*ppos) {…… for(;;); …… } 然后把mydrive.c编译成模块下载到开发板(这里把驱动程序编译成模块,直接编译进内核的情况比较简单,后面会叙述)。insmodmydrive.ko后,执行于此驱动相对应的测试程序mytest,就会看到系统完全卡死了。 在asm_do_IRQ()函数里添加如下一段代码,把僵死进程号和pc值打印出来。 staticpid_tpid; //之前进程号 staticintcount=0; If(irq== 30) //系统时钟中断用的是定时器4,对应的中断号是30 { //若当前进程不等于之前进程,计数值清零 if(pid!=current->pid) { pid=current->pid; count= 0; } //当前进程等于之前的进程,计数值累加 else count++; if(count==15*HZ)若15秒内都是同一个进程 { count= 0; printk(“PID=%d,name=%s,pc=%08x
”,current->pid,current->comm,regs->uregs[15]); } } 在Linux内核里面每一个进程都由一个task_struct结构体来表示[13,14],里面包含了进程的相关属性和信息。其中pid_tpid表示进程号;charcomm表示进程的名字。 current是一个全局宏,用来获取表示当前进程的task_struct结构体指针。HZ也是一个宏定义,表示时钟中断发生的频率,即一秒发生中断的次数。 把修改的内核源码在Linux服务器下编译后下载到开发板并启动。然后装载驱动模块mydrive.ko并运行与之相应的测试程序mytest,如图3所示。 图3执行步骤 系统僵死,等待15秒左右之后打印出僵死进程号、僵死进程名、 pc 值。如图4所示。 图4 僵死进程相关信息 从打印信息可知是763号进程mytest出现了僵死,从而可以知道问题出在与mytest对应的驱动程序mydrive。在开发板的串口终端下打开system.map(里面是内核的地址空间),发现bf000068不在其中,说明僵死驱动mydrive是个外加驱动模块,这也与实际操作相符。 至于死循环代码的具体位置还得用pc值去反推。在Linux2.6版本内核中引入了kallsyms,kallsyms抽取了内核用到的所有函数地址(全局的、静态的)和非栈数据变量地址,生成一个数据块,作为只读数据链接进kernelimage。当然外加驱动模块的地址也在其中重启开发板,在终端下执行: insmodmydrive.ko cat/proc/kallsyms 在里面寻找地址bf000068,就找到与bf000068相近的一条bf000000。 bf000000tmydrive_open[mydrive] 在Linux服务器下把mydrive.ko模块反汇编arm-Linux-cbjdump-Dmydrive.ko>mydrive.dis 打开mydrv.dis,找到有mydrive_open的那一行00000000 00000000 …… 64:ebfffffebl64 68:ea00001fbf8 …… 前面说过,中断保存的pc值其实是实际指令地址+4,所以0x00000064才是中断函数执行前保存的真正地址,也即僵死的位置。再看对应的名字mydrive_read+0x3c,可知具体代码在mydrive_read()函数入口地址偏移0x3c。至此回到源码文件mydrive.c的mydrive_read()函数便可找到具体的僵死处。这正好与之前故意添加的死循环for(;;)的位置相一致。比照着相应的汇编指令[16]bl64:永远跳转到64。这也刚好和死循环的C语言是相吻合的。 这里的僵死点在外加的驱动模块中,如果僵死点在内核里,就会发现打印出来的pc值在system.map文件所列出的地址范围里面。这时只需要把使用的Linux内核文件反汇编,在里面找到pc-4地址所在那一行代码,自然就是僵死点的位置。 第一种调试方法中将驱动源码文件proc-printk.c编译进了内核。当然为了装卸载方便,也可以将其编译成模块。在第二种调试方法中,为了测试需要,故意在驱动文件里添加for(;;)语句,造成程序僵死在一点,只打印出一个pc值。而在实际中,死循环的代码可能是一段,这时用上述方法在一段时间内打印出来的pc值可能会不同。但这不要紧,因为此时的pc值虽不同,但分布密集。程序员只须分析这几个pc值指定的汇编代码就可确定僵死的大致位置。 [1] 霍玲玲,王世君,徐晓卉,等.嵌入式Linux系统的设计与实现[J]. 计算机技术与发展,2014,24(5):87-89. [2] 冯开林,刘春艳,韩东旭.基于S3C2440平台搭建Linux环境[J]. 通信技术,2013,46(11):120-124. [3] 付阳.基于ARM9的嵌入式Linux移植和驱动程序设计[D].武汉:华中科技大学,2012. [4] 韦东山.嵌入式Linux应用开发完全手册[M].北京:人民邮电出版社,2008. [5] 赵付强,李允俊,宫彦磊.Proc文件系统的研究与应用[J]. 计算机系统应用,2013,22(1):87-90. [6] 郭锐.基于覆盖测试的Linux内核裁剪[D].太原:中北大学,2014. [7] 宋宝华.Linux设备驱动开发详解[M].北京:人民邮电出版社,2010. [8] 王维,李涛,韩俊刚. 一种多线程轻核机器中进程管理的硬件实现[J].电子技术应用,2013,29(3):40-43. [9] 唐富强,于鸿洋,张萍.Linux下通用线程池的改进与实现[J].计算机工程与应用,2012,48(28):77-83. [10] 周峰,胡军山,朱宗玖.基于CK810LINUX3.0内核的移植实现[J].计算机应用与软件,2014,31(1):252-255,267. [11] 郑强.Linux驱动开发入门与实战[M].北京:清华大学出版社,2011. [12] 毛德操,胡希明.Linux内核源代码情景分析[M].杭州:浙江大学出版社,2001. [13] 杨兴强,刘翔鹏,刘毅.Linux进程状态演化过程的图形学表示[J].系统仿真学报,2013,25(10):2444-2448. [14] 龙飞.嵌入式Linux系统内核实时性研究[D].沈阳:沈阳工业大学,2012. [15] 奚琪,曾勇军,王清贤,等.一种动静结合的代码反汇编框架[J].小型微型计算机系统,2013(10):2251-2255. [16] 黄奉孝,高艳华,张学军.基于嵌入式构件的编程语言融合技术研究[J].计算机工程与设计,2012,33(11):4138-4141. RESEARCHONARM-BASEDLINUXDRIVERDEBUGGINGTECHNOLOGY GongChenCaiWen (School of Information,Mechanical and Electrical Engineering,Shanghai Normal University,Shanghai 200234,China) WhenperformingLinuxdrivertransplantationonARM,itisneededtomodify,deleteoraddLinuxkernelcodes,butwhichmayresultinmanyunexpectederrorsatruntime.Atthistimetodebugcodessoastofindthecauseandpositionoftheerrorarenecessary.Forthisrequirement,weproposeandimplementtwonewdebuggingtechniques.Thefirstoneistoconstructaprintingfunctiontostoreadditionalprintinformationinabufferseparately,andtooutputitwiththehelpofprocfilesystem.Itachievestheseparationbetweentheadditionalprintinformationandtheprintinformationofthekernelitself,andmakesthesearchmoreconvenient.Thesecondoneistodeterminethepositionofsystemdeadbymakinguseoftheunceasingcharacteristicofsystemclockinterrupt.Itisshownthroughexperimentthatthistechniquefindsthepositionofendlessloopquickly,andsavesalargeamountofcodesearchandanalysiswork. LinuxDebugProcfilesystemSystemclockinterrupt 2014-08-26。巩琛,硕士生,主研领域:嵌入式系统与通信控制系统。蔡文,副教授。 TP314 ADOI:10.3969/j.issn.1000-386x.2016.03.0542 利用内核时钟中断确定僵死位置
3 结 语