驱动程序调试方法之printk——自制proc文件(二)

上一节的程序很振奋人心,我们自己实现了一个myprintk打印函数。但是这个函数存在一个致命的缺陷,那就是只能使用一次cat /proc/mymsg命令来读取mylog_buf的值。这是因为读到最后会出现:mylog_r == mylog_w,表示缓冲区为空,下一次就不能在读到数据了。在本节里面我们就着手来解决这个问题,我们要实现的就是每次使用  cat /proc/mymsg  时,都会从头打印。那么我们就需要将入口做一个拷贝,一个保存起来,一个进行变换。这样的话,当下一次读的时候,我们可以将保存的入口重新做个拷贝,然后让拷贝进行变化。具体程序如下:

#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 MYLOG_BUF_LEN 1024

struct proc_dir_entry *myentry;

static char mylog_buf[MYLOG_BUF_LEN];
static char tmp_buf[MYLOG_BUF_LEN];
static int mylog_r = 0;
static int mylog_r_for_read = 0;//这个用来拷贝mylog_r ,它将改变,但是mylog_r 不变
static int mylog_w = 0;

static DECLARE_WAIT_QUEUE_HEAD(mymsg_waitq);

static int is_mylog_empty(void)
{
return (mylog_r == mylog_w);
}

static int is_mylog_empty_for_read(void)
{
return (mylog_r_for_read == mylog_w);
}

static int is_mylog_full(void)
{
return ((mylog_w + 1)% MYLOG_BUF_LEN == mylog_r);
}

//这个函数是被myprintk函数调用的
static void mylog_putc(char c)
{
if (is_mylog_full())
{
mylog_r = (mylog_r + 1) % MYLOG_BUF_LEN;
                 /*加上下面三行的原因是:如果读的时候,也一直在调用printk写的话,
                *当写的速度比较快的时候,可能会导致mylog_w超过mylog_r_for_read,
                *这时就需要更新mylog_r_for_read,使mylog_r_for_read 指向新的入口
                *当mylog_w超过入口mylog_r时,mylog_r会一直跟着更新的!        
               */
if ((mylog_r_for_read + 1) % MYLOG_BUF_LEN == mylog_r)
{
mylog_r_for_read = mylog_r;
}
}

mylog_buf[mylog_w] = c;
mylog_w = (mylog_w + 1) % MYLOG_BUF_LEN;

    wake_up_interruptible(&mymsg_waitq); 
}

static int  mylog_getc_for_read (char *p)
{
if ( is_mylog_empty_for_read ())
{
return 0;
}
*p = mylog_buf[ mylog_r_for_read ];
mylog_r_for_read  = ( mylog_r_for_read  + 1) % MYLOG_BUF_LEN;
return 1;
}


int myprintk(const char *fmt, ...)
{
va_list args;
int i;
int j;

va_start(args, fmt);
i = vsnprintf(tmp_buf, INT_MAX, fmt, args);
va_end(args);
for (j = 0; j < i; j++)
mylog_putc (tmp_buf[j]);
return i;
}

static ssize_t mymsg_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
int error = 0;
int i = 0;
char c;

if ((file->f_flags & O_NONBLOCK) && is_mylog_empty_for_read())
return -EAGAIN;

error = wait_event_interruptible(mymsg_waitq, !is_mylog_empty_for_read());

/* copy_to_user */
while (!error && ( mylog_getc_for_read (&c)) && i < count) {
error = __put_user(c, buf);
buf++;
i++;
}
if (!error)
error = i;
return error;
}

static int mymsg_open(struct inode *inode, struct file *file)
{
mylog_r_for_read  = mylog_r;
return 0;
}

const struct file_operations proc_mymsg_operations = {
.open = mymsg_open,
.read = mymsg_read,
};

static int mymsg_init(void)
{
myentry = create_proc_entry("mymsg", S_IRUSR, &proc_root);
if (myentry)
myentry->proc_fops = &proc_mymsg_operations;
return 0;
}

static void mymsg_exit(void)
{
remove_proc_entry("mymsg", &proc_root);
}

module_init(mymsg_init);
module_exit(mymsg_exit);

EXPORT_SYMBOL(myprintk);

MODULE_LICENSE("GPL");

总结,关于这个函数,当我们在用户空间,使用命令:cat /proc/mymsg时,首先会调用open函数,在open函数里面会将入口做一个拷贝,然后拿出一份来作为变化量,另外一个作为入口不改变。这样,每次cat /proc/mymsg时,都会从入口处开始打印!


根据内核打印的段错误信息分析驱动程序——根据出错PC来分析  

我们知道在内核里面不能够处理指向0地址的指针,我们故意引入这样一个指针,并根据打印的出错信息来进行分析,下面是我们的程序:
#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>

int *i;

static int first_drv_init(void)
{
         i=0;
        *i=2;
        return 0;
}

static void first_drv_exit(void)
{
}
module_init(first_drv_init);
module_exit(first_drv_exit);
MODULE_LICENSE("GPL");

当我们加载的时候会打印出错信息,出错信息如下:
 
//第一行就指出了出错的原因是由于无法处理指向0地址的指针
Unable to handle kernel NULL pointer dereference at virtual address 00000000
pgd = c3e4c000
[00000000] *pgd=33c90031, *pte=00000000, *ppte=00000000
Internal error: Oops: 817 [#1]
Modules linked in: test   
CPU: 0    Not tainted  (2.6.22.6 #12)

//PC指明发生错误的指令的地址
//0x1c:该指令的偏移,0x28 :该函数的大小
//大多时候PC值只会是一个地址,不会指示说在哪个函数里面
//当然这里是给出了函数了
PC is at first_drv_init+0x1c/0x28 [test]

//指明LR寄存器的值
LR is at sys_init_module+0x1424/0x1514

//执行这条导致错误的指令时各个寄存器的值
pc : [<bf00001c>]    lr : [<c006285c>]    psr: a0000013
sp : c3e25ec8  ip : c3e25ed8  fp : c3e25ed4
r10: c486e000  r9 : c3e29d14  r8 : 00000018
r7 : c48795b8  r6 : bf000360  r5 : bf000360  r4 : 00000000
r3 : 00000002  r2 : c0357028  r1 : 00000001  r0 : 00000000


Flags: NzCv  IRQs on  FIQs on  Mode SVC_32  Segment user
Control: c000717f  Table: 33e4c000  DAC: 00000015
Process insmod (pid: 751, stack limit = 0xc3e24258)
//发生错误时当前进程的名称是insmod

Stack: (0xc3e25ec8 to 0xc3e26000)
5ec0:                   c3e25fa4 c3e25ed8 c006285c bf000010 00000000 00000398 
5ee0: c02a9f60 c02a9f60 000000fc 0000001c 00000018 c3c13370 c3e25f18 00000000 
5f00: 00000028 00000028 0000005c 00000058 00000014 c3e24000 00000000 00000000 
5f20: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 
5f40: 00000003 00000000 00000005 00000000 00000000 00000000 00000017 00000016 
5f60: c487cd48 c3e0db80 c48794e4 000cb060 beb46918 000c7f7c c3e25f9c 0000eddb 
5f80: 000ca1b0 000cb050 00000080 c002b044 c3e24000 000c7f7c 00000000 c3e25fa8 
5fa0: c002aea0 c0061448 000ca1b0 000cb050 00900080 000cb248 0000eddb 000cb070 
5fc0: 0000eddb 000ca1b0 000cb050 00000000 000cb060 beb46918 000c7f7c 00000002 
5fe0: beb44fb8 beb44fac 00052354 401b7a00 60000010 00900080 00000000 00000000 

//这里从出错位置回溯出了函数的调用关系
Backtrace: 
[<bf000000>] (first_drv_init+0x0/0x28 [test]) from [<c006285c>] (sys_init_module+0x1424/0x1514)
[<c0061438>] (sys_init_module+0x0/0x1514) from [<c002aea0>] (ret_fast_syscall+0x0/0x2c)
Code: e59f3010 e3a00000 e5830000 e3a03002 (e5803000) 
Segmentation fault

分析:
1、根据pc值确定该指令属于内核还是外加的模块
先判断是否属于内核的地址: 看System.map确定内核的函数的地址范围:c0004000~c03be254
而本PC值为bf00001c,并不属于内核模块,而是属于加载模块
该System.map文件位于编译内核后的根目录

2、根据pc确定错误出在哪一个加载的内核模块
cat /proc/kallsyms  (内核函数、加载的函数的地址)
从这些信息中找出一个跟bf00001c相近的地址
我们找到如下信息:
bf000000 t first_drv_init[test]
这行表示first_drv_init地址是bf000000 ,显然出错位置就在0xbf000000 + 0x1c处,且对应的加载模块式test.ko
我们根据打印信息: PC is at first_drv_init+0x1c/0x28 [test]
看出确实是如此!

3、找到了test.ko
在PC上反汇编它: arm-linux-objdump -D test.ko > test.dis
在dis文件里找到地址: bf00001c,即:   
1c: e5803000 str r3, [r0]
根据对应的反汇编找到对应的c函数对应的位置

总结一下就是:
1、首先我们在出错信息中找到出错的地址
2、然后查看: System.map,判断出错位置是属于内核模块还是加载模块
3、如果是在加载模块的话,我们可以用命令: cat /proc/kallsyms > /kallsyms.txt 来查看距离出错pc最近的一个函数是什么,并查看它属于哪一个模块。
最后我们反汇编该模块,并找到我们上面找到的那个函数,根据偏移地址找到出错pc的位置,根据该位置的汇编代码来判断出错位置对应c语言的位置。

上面的错误位置是出现在加载模块,那么如果出现在内核模块又怎么做呢?其实方法是完全一样的:
比如我们将上面的文件命名为test.c,然后将其放入内核的drivers/char/目录下面,然后将 drivers/char/目录下的Makefile里面加上这么一句:
obj-y    +=test.o
重新编译内核:make uImage
用新内核启动
启动过程中崩溃了,打印出如下错误信息:

//看!无法处理0指针处的操作
Unable to handle kernel NULL pointer dereference at virtual address 00000000
pgd = c0004000
[00000000] *pgd=00000000
Internal error: Oops: 805 [#1]
Modules linked in:
CPU: 0    Not tainted  (2.6.22.6 #15)
PC is at first_drv_init+0x1c/0x28
LR is at kernel_init+0xd4/0x28c
pc : [<c019ad88>]    lr : [<c0008a94>]    psr: 60000013
sp : c0471f74  ip : c0471f84  fp : c0471f80
r10: 00000000  r9 : c0023864  r8 : c0022838
r7 : c0470000  r6 : 00000000  r5 : 00000000  r4 : 00000000
r3 : 00000002  r2 : c002292c  r1 : 00000000  r0 : 00000000
Flags: nZCv  IRQs on  FIQs on  Mode SVC_32  Segment kernel
Control: c000717f  Table: 30004000  DAC: 00000017
Process swapper (pid: 1, stack limit = 0xc0470258)
Stack: (0xc0471f74 to 0xc0472000)
1f60:                                              c0471ff4 c0471f84 c0008a94 
1f80: c019ad7c 0a00002e e5943000 00000000 00000001 e59f10fc 00000000 00000000 
1fa0: 00000000 c0471fb0 c002af24 c0040330 00000000 00000000 c00089c0 c00466f0 
1fc0: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 
1fe0: 00000000 00000000 00000000 c0471ff8 c00466f0 c00089d0 e5840058 15803024 
Backtrace: 
[<c019ad6c>] (first_drv_init+0x0/0x28) from [<c0008a94>] (kernel_init+0xd4/0x28c)
[<c00089c0>] (kernel_init+0x0/0x28c) from [<c00466f0>] (do_exit+0x0/0x760)
Code: e59f3010 e3a00000 e5830000 e3a03002 (e5803000) 
Kernel panic - not syncing: Attempted to kill init!

分析:
1、我们找到pc值:c019ad88
2、然后:cat System.map,查看错误位置是否在内核模块
     我们发现内核模块函数的地址范围是:c0004000~c03bd168
     所以是属于内核模块的。
3、接下来我们反汇编内核:arm-linux-objdump -D vmlinux > vmlinux.dis
4、在dis文件里搜c014e6c0
      vi  vmlinux.dis

我们找到如下信息:
c019ad6c <first_drv_init>:
c019ad6c:       e1a0c00d        mov     ip, sp
c019ad70:       e92dd800        stmdb   sp!, {fp, ip, lr, pc}
c019ad74:       e24cb004        sub     fp, ip, #4      ; 0x4
c019ad78:       e59f3010        ldr     r3, [pc, #16]   ; c019ad90 <.text+0x170d90>
c019ad7c:       e3a00000        mov     r0, #0  ; 0x0
c019ad80:       e5830000        str     r0, [r3]
c019ad84:       e3a03002        mov     r3, #2  ; 0x2
c019ad88:       e5803000        str     r3, [r0]
c019ad8c:       e89da800        ldmia   sp, {fp, sp, pc}
c019ad90:       c03b33d8        ldrgtsb r3, [fp], -r8

OK!根据  函数名  以及  c019ad88:       e5803000        str     r3, [r0]就可以找到对应的c程序的位置了!

根据内核打印的段错误信息分析驱动程序——根据栈信息来分析  

上一节里面我们看到出错信息还包括回溯信息,通过回溯信息我们可以知道是哪一条调用路径出现了错误。但是要想显示回溯信息的话,在内核配置文件.config文件里面必须要有这么一句:CONFIG_FRAME_POINTER=y
但是如果我们的内核没有配置 CONFIG_FRAME_POINTER=y 这句话,是不是就意味着我们无法知道回调关系了呢!其实并非如此,根据上一节里面的内容我们还知道,出错信息里面还包括栈信息,其实从栈信息里面我们就可以分析出回调关系,它的原理也很简单,我们知道当A调用B的时候,会将A的返回地址入栈,B在调用C的时候,又会将B的返回地址入栈,这样栈中的信息就表示了一种调用关系。这样当调用函数返回时,就可以从栈中取出返回地址,一次返回B、A。同样,在出错的时候,也可以通过一次打印栈中的信息,来显示调用关系。
下面我们就来具体地分析一下:
这里是我们故意写的一个出错的模块:
#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>

int *i;

void a()
{
    *i=1;
}

static int first_drv_init(void)
{
       i=0;
       a();
return 0;
}

static void first_drv_exit(void)
{
    
}

module_init(first_drv_init);
module_exit(first_drv_exit);
MODULE_LICENSE("GPL");

加载的时候出现了下面的打印信息:

Unable to handle kernel NULL pointer dereference at virtual address 00000000
pgd = c3c44000
[00000000] *pgd=3001b031, *pte=00000000, *ppte=00000000
Internal error: Oops: 817 [#1]
Modules linked in: test
CPU: 0    Not tainted  (2.6.22.6 #16)
PC is at a+0x18/0x24 [test]
LR is at first_drv_init+0x1c/0x28 [test]
pc : [<bf000018>]    lr : [<bf000040>]    psr: a0000013
sp : c3c27eb4  ip : c3c27ec4  fp : c3c27ec0
r10: c486e000  r9 : c06d6f14  r8 : 00000018
r7 : c4879624  r6 : bf0003c0  r5 : bf0003c0  r4 : 00000000
r3 : 00000001  r2 : 00000000  r1 : 00000001  r0 : 00000000
Flags: NzCv  IRQs on  FIQs on  Mode SVC_32  Segment user
Control: c000717f  Table: 33c44000  DAC: 00000015
Process insmod (pid: 747, stack limit = 0xc3c26258)
Stack: (0xc3c27eb4 to 0xc3c28000)
7ea0:                                              c3c27ed4 c3c27ec4 bf000040 
7ec0: bf000010 00000000 c3c27fa4 c3c27ed8 c006285c bf000034 00000000 00000398 
7ee0: c02a9f20 c02a9f20 000000fc 0000001c 00000018 c3cce128 c3c27f18 00000000 
7f00: 0000002b 0000002b 0000005c 00000058 00000014 c3c26000 00000000 00000000 
7f20: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 
7f40: 00000003 00000000 00000005 00000000 00000000 00000000 00000017 00000016 
7f60: c487ce14 c3e16240 c4879550 000cb060 be8a0918 000c7f7c c3c27f9c 0000eea9 
7f80: 000ca1b0 000cb050 00000080 c002b044 c3c26000 000c7f7c 00000000 c3c27fa8 
7fa0: c002aea0 c0061448 000ca1b0 000cb050 00900080 000cb248 0000eea9 000cb070 
7fc0: 0000eea9 000ca1b0 000cb050 00000000 000cb060 be8a0918 000c7f7c 00000002 
7fe0: be89efb8 be89efac 00052354 401b7a00 60000010 00900080 00000000 00000000 
Backtrace: 
[<bf000000>] (a+0x0/0x24 [test]) from [<bf000040>] (first_drv_init+0x1c/0x28 [test])
[<bf000024>] (first_drv_init+0x0/0x28 [test]) from [<c006285c>] (sys_init_module+0x1424/0x1514)
 r4:00000000
[<c0061438>] (sys_init_module+0x0/0x1514) from [<c002aea0>] (ret_fast_syscall+0x0/0x2c)
Code: e24cb004 e59f300c e5932000 e3a03001 (e5823000) 
Segmentation fault

分析:
1、找到pc=bf000018
2、打开System.map文件,发现内核模块函数的地址范围是:
      c0004000~c03be254
      所以出错位置不是内核模块
3、查看开发板上:/proc/kallsyms文件
      cat /proc/kallsyms > /kallsyms.txt
      vi /kallsyms.txt
      找到离bf000018比较近的函数,发现如下信息:
      bf000000 t $a   [test]
      于是知道错误出现在:加载模块test,出错函数是:a
4、我们将模块test反汇编:arm-linux-objdump -D test.ko > test.dis
      在test.dis找到如下信息:
00000000 <a>:
   0:   e1a0c00d        mov     ip, sp
   4:   e92dd800        stmdb   sp!, {fp, ip, lr, pc}
   8:   e24cb004        sub     fp, ip, #4      ; 0x4
   c:   e59f300c        ldr     r3, [pc, #12]   ; 20 <.text+0x20> //到20处取到0给了r3
  10:   e5932000        ldr     r2, [r3]                                       //将0地址的内容放入r2
  14:   e3a03001        mov     r3, #1  ; 0x1                           //将1放入0地址处
   18:   e5823000        str     r3, [r2]                                     //将1放入0地址内容表示的位置,出错了
  1c:   e89da800        ldmia   sp, {fp, sp, pc}
  20:   00000000        andeq   r0, r0, r0

由于之前我们已经知道出错位置相对于函数a的偏移是18,所以这里我们很容易找到了出错位置就是上面蓝色标识处!
通过分析,我们很容易就知道是对指向0地址的i指针赋值的时候出现了错误!

下面我们根据栈内容分析调用关系:

Stack: (0xc3c27eb4 to 0xc3c28000)
7ea0:                                              c3c27ed4 c3c27ec4 bf000040 
7ec0: bf000010 00000000 c3c27fa4 c3c27ed8 c006285c bf000034 00000000 00000398 
7ee0: c02a9f20 c02a9f20 000000fc 0000001c 00000018 c3cce128 c3c27f18 00000000 
7f00: 0000002b 0000002b 0000005c 00000058 00000014 c3c26000 00000000 00000000 
7f20: 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 
7f40: 00000003 00000000 00000005 00000000 00000000 00000000 00000017 00000016 
7f60: c487ce14 c3e16240 c4879550 000cb060 be8a0918 000c7f7c c3c27f9c 0000eea9 
7f80: 000ca1b0 000cb050 00000080 c002b044 c3c26000 000c7f7c 00000000 c3c27fa8 
7fa0: c002aea0 c0061448 000ca1b0 000cb050 00900080 000cb248 0000eea9 000cb070 
7fc0: 0000eea9 000ca1b0 000cb050 00000000 000cb060 be8a0918 000c7f7c 00000002 
7fe0: be89efb8 be89efac 00052354 401b7a00 60000010 00900080 00000000 00000000 

首先我们要知道返回地址在寄存器lr中,而返回指令为调用指令的下一条指令,好的下面开始分析:
上面我们已经得知错误出现在a函数里面,我们贴出a函数的反汇编:
00000000 <a>:
   0: e1a0c00d  mov ip, sp
   4: e92dd800  stmdbsp!, {fp, ip, lr, pc}
   8: e24cb004  sub fp, ip, #4 ; 0x4
   c: e59f300c  ldr r3, [pc, #12] ; 20 <.text+0x20>
  10: e5932000  ldr r2, [r3]
  14: e3a03001  mov r3, #1 ; 0x1
  18: e5823000  str r3, [r2]
  1c: e89da800  ldmia sp, {fp, sp, pc}
  20: 00000000  andeq r0, r0, r0

得出lr=bf000040,回溯一下发现调用语句在init_module函数里面

贴出init_module的反汇编:
00000024 <init_module>:
  24: e1a0c00d mov ip, sp
  28: e92dd810  stmdbsp!, {r4, fp, ip, lr, pc}
  2c: e24cb004 sub fp, ip, #4; 0x4
  30: e59f3010 ldr r3, [pc, #16]; 48 <.text+0x48>
  34: e3a04000 mov r4, #0; 0x0
  38: e5834000 str r4, [r3]
  3c: ebfffffe bl 3c <init_module+0x18>
  40: e1a00004 mov r0, r4
  44: e89da810 ldmia sp, {r4, fp, sp, pc}
  48: 00000000 andeq r0, r0, r0

得出lr=c006285c ,我们看到这是个内核模块的函数地址,我们还需要内核模块的反汇编:
arm-linux-objdump -D vmlinux > vmlinux.dis
 vmlinux.dis文件里面搜索地址:c006285c 
发现地址为c006285c的代码在函数:sys_init_module里面

由此我们得出调用关系为:
sys_init_module----->init_module------>a
由于init_module就相当于执行first_drv_init,所以调用关系是:
sys_init_module----->init_module(first_drv_init)------>a

到此为止,我们通过栈来分析回溯调用成功了!根据真正的回溯函数我们可以验证其正确性!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值