【Linux 驱动】第四章 调试技术

一,内核中的调试支持
在内核配置菜单中有“kernel hacking”菜单选项,这些选项帮助用户检查很多错误,这里我列了一个表方便大家参考:
kernel hacking 在哪里?
~# cd /usr/src/linux-source.3.0.0
~#make menuconfig 则出现
【Linux 驱动】第四章 调试技术_第1张图片
【Linux 驱动】第四章 调试技术_第2张图片
查找USB驱动的方法

1)cd linux-source-3.0.0

2)lsusb /*查看所有连接到系统的USB设备*/

3)拔掉USB设备,然后再运行一遍lsusb命令,这样就可以确定以上哪条信息是针对你的新硬件的了。

Bus 002 Device 004: ID 1e3d:2093 /*我的硬件识别信息*/

其中ID 1e3d:2093这个信息对我们很有用处,我们需要用它来查找内核中与硬件匹配的信息。1e3d代表的是厂商ID,就是哪家厂商。2093是硬件ID。

下面开始用1e3d搜索内核源码树:

4)grep -i -R-l 0403 drivers

drivers/usb/serial/ftdi_sio.mod.o
drivers/usb/serial/ftdi_sio.ko
drivers/usb/serial/ftdi_sio.mod.c
drivers/watchdog/pcwd_pci.ko
drivers/watchdog/pcwd_pci.mod.c
drivers/watchdog/pcwd_pci.mod.o
drivers/bluetooth/btusb.ko
drivers/bluetooth/btusb.mod.c
drivers/bluetooth/btusb.mod.o
drivers/media/rc/keymaps/rc-hauppauge.c
drivers/media/rc/keymaps/rc-dib0700-rc5.c
drivers/media/dvb/dvb-usb/dvb-usb-vp702x.mod.c
drivers/media/dvb/dvb-usb/nova-t-usb2.c
drivers/media/dvb/dvb-usb/dvb-usb-vp702x.ko
drivers/media/dvb/dvb-usb/dvb-usb-vp702x.mod.o

该命令执行完后,会在屏幕上显示若干条以.data .c .h等为结尾的文件,比如drivers/usb/serial/ftdi_sio.ko不用看最后一部分,前三个目录名就可以确定这是个USB串口设备。同样的判断方法,我们就可以确定我们需要的内核文件了。以防万一,我们进入这个文件中,USB驱动告诉内核它们支持哪些谁被,以便内核可以把驱动绑定到设备上。一般在一个结构体变量中列出制造商ID和设备ID。如果我们设备的制造商ID和设备ID在里面的话,说明这个驱动支持我们的硬件设备。

cd linux-3.0.0 /*进入内核文件中*/

find –type f –name Makefile | xargs grep XXXXX/*会显示一个以CONFIG_为前缀的字段*/

找到这个字段后,返回内核Makefile文件中,使用内核配置工具menuconfig,搜索这个字段。最后在该程序菜单中相应位置启动这个驱动。


二,printk打印调试
在应用程序中,我们也经常使用这种经典的打印调试技术。在内核中,printk用来完成相同的工作。
printk与printf的一个不同就是,前者可以对消息进行分类,表示日志级别的宏会自动展成一个字符串,用到的级别有以下几种:
KERN_EMERG:紧急事件,系统崩溃前提示的消息。
KERN_ALERT: 立即采取动作的情况。
KERN_CRIT: 涉及到严重的硬件或者软件操作失败。
KERN_ERR:用于报告错误状态,驱动程序中用于报告来自硬件的问题。
KERN_WARNING: 不会造成系统严重问题的一般警告。
KERN_NOTICE: 进行提示的正常情形。
KERN_INFO: 提示性信息,驱动程序一般提示找到了硬件信息。
KERN_DEBUG: 用于debug。
一般我们需要将这些调试信息打印到控制台上,而控制台本身有一个日志级别(默认为DEFAULT_CONSOLE_LOGLEVEL),printk制定的级别必须数值上小于该默认值且以newline结束才能打印到控制台上,所以我们有时候需要更改控制台的日志级别,比如下面可以将所有的printk消息输出到控制台:
echo 8 > /proc/sys/kernel/printk

如何关闭打开调试信息?
技巧:通过ifdef定义宏来完成,需要编译debug信息在makefile中打开此宏,否则关闭。
速度限制?
为了防止大量产生log信息,导致在某些慢速设备上出现系统假死的情况,使用内核函数
int printk_ratelimit(void) 返回非0值,可以继续打印,如果输出速度超过一个值,返回0避免发送重复消息。
if(printk_ratelimit())
printk(KERN_NOTICE"The printer is still on fire\n");
打印设备编号
int print_dev_t(char *buffer,dev_t dev); //返回打印字符数
char *format_dev_t(char *buffer, dev_t dev); //返回缓冲区
三,使用proc文件系统进行调试
/proc 文件系统是一种特殊的,由软件创建的文件系统,内核使用它向外界导出信息。/proc 下面的每个文件都绑定于一个内核函数,用户读取其中的文件时,该函数动态完成文件内容。
所有proc模块需要包含#include<linux/fs_proc.h>
内核函数接口:
  1. struct proc_dir_entry*create_proc_read_entry(constchar*name,mode_t mode,
  2. struct proc_dir_entry*base,
  3. read_proc_t*read_proc,void*data);
  4. /*创建proc文件接口
  5. **name:要创建的文件名称
  6. **mode:该文件的保护码,0表示系统默认值
  7. **base:指定文件所在目录,base如果为NULL,表示在proc根目录下创建该文件
  8. **read_proc:该文件的read_proc函数,读取该文件时调用
  9. **data:传递给read_proc的参数
  10. */
    1. int(*read_proc)(char*page,char**start,off_t offset,intcount,int*eof,void*data);
    2. /*
    3. **page:指针指向用来写入数据的缓冲区
    4. **start:返回实际的数据写到内存页得哪个位置
    5. **offset,count:和read方法一样
    6. **eof:指向一个整形数,当没有数据可返回时,驱动程序必须设置这个参数
    7. **data:提供给驱动程序的专用数据指针,可用于内部记录
    8. */
    9. 返回值:必须返回存放到内存页缓冲区的字节数,*eof和*start也属于返回值
    //由于内核信任驱动程序,因此不会检查某个名称是否已经被注册,所以有可能导致注册同名入口项
    1. void remove_proc_entry(constchar*name,struct proc_dir_entry*base)//如果删除已卸载模块,内核会崩溃
    2. /*移除proc文件
    3. **name:文件名
    4. **base:目录,和创建前面一样
    5. */
    针对proc文件的不足而诞生了 Seq_file
    Seq_file 的实现基于proc文件。使用 Seq_file ,用户必须抽象出一个链接对象,然后可以依次遍历这个链接对象。这个链接对象可以是链表,数组,哈希表等等。
    编程接口
    Seq_file 必须实现四个操作函数:start(), next(), show(), stop()。
    struct seq_operations {
    void * ( * start ) ( struct seq_file * m , loff_t * pos ) ;
    void ( * stop ) ( struct seq_file * m , void * v ) ;
    void * ( * next ) ( struct seq_file * m , void * v , loff_t * pos ) ;
    int ( * show ) ( struct seq_file * m , void * v ) ;
    } ;
    start():
    主要实现初始化工作,在遍历一个链接对象开始时,调用。返回一个链接对象的偏移或者SEQ_START_TOKEN(表征这是所有循环的开始)。出错返回ERR_PTR。
    stop():
    当所有链接对象遍历结束时调用。主要完成一些清理工作。
    next():
    用来在遍历中寻找下一个链接对象。返回下一个链接对象或者NULL(遍历结束)。
    show():
    对遍历对象进行操作的函数。主要是调用seq_printf(), seq_puts()之类的函数,打印出这个对象节点的信息。
    下图描述了 seq_file 函数对一个链表的遍历。
    2、重要的数据结构
    除了struct seq_operations以外,另一个最重要的数据结构是struct seq_file
    struct seq_file {
    char *buf;
    size_t size;
    size_t from;
    size_t count;
    loff_t index;
    u64 version;
    struct mutex lock;
    const struct seq_operations *op;
    void *private;
    };
    该结构会在seq_open函数调用中分配,然后作为参数传递给每个 seq_file 的操作函数。Privat变量可以用来在各个操作函数之间传递参数。
    3、 Seq_file 使用示例:
    # include /* for use of init_net*/
    # include /* We're doing kernel work */
    # include /* Specifically, a module */
    # include /* Necessary because we use proc fs */
    # include /* forseq_file*/
    # define PROC_NAME "my_seq_proc"
    MODULE_LICENSE ( "GPL" ) ;
    static void * my_seq_start ( struct seq_file * s , loff_t * pos )
    {
    static unsigned long counter = 0 ;
    printk ( KERN_INFO "Invoke start\n" ) ;
    /* beginning a new sequence ? */
    if ( * pos = = 0 )
    {
    /* yes => return a non null value to begin the sequence */
    printk ( KERN_INFO "pos == 0\n" ) ;
    return & counter ;
    }
    else
    {
    /* no => it's the end of the sequence, return end to stop reading */
    * pos = 0 ;
    printk ( KERN_INFO "pos != 0\n" ) ;
    return NULL ;
    }
    }
    static void * my_seq_next ( struct seq_file * s , void * v , loff_t * pos )
    {
    unsigned long * tmp_v = ( unsigned long * ) v ;
    printk ( KERN_INFO "Invoke next\n" ) ;
    ( * tmp_v ) + + ;
    ( * pos ) + + ;
    return NULL ;
    }
    static void my_seq_stop ( struct seq_file * s , void * v )
    {
    printk ( KERN_INFO "Invoke stop\n" ) ;
    /* nothing to do, we use a static value in start() */
    }
    static int my_seq_show ( struct seq_file * s , void * v )
    {
    printk ( KERN_INFO "Invoke show\n" ) ;
    loff_t * spos = ( loff_t * ) v ;
    seq_printf ( s , "%Ld\n" , * spos ) ;
    return 0 ;
    }
    static struct seq_operations my_seq_ops = {
    . start = my_seq_start ,
    . next = my_seq_next ,
    . stop = my_seq_stop ,
    . show = my_seq_show
    } ;
    static int my_open ( struct inode * inode , struct file * file )
    {
    return seq_open ( file , & my_seq_ops ) ;
    } ;
    static struct file_operations my_file_ops = {
    . owner = THIS_MODULE ,
    . open = my_open ,
    . read = seq_read ,
    . llseek = seq_lseek ,
    . release = seq_release
    } ;
    int init_module ( void )
    {
    struct proc_dir_entry * entry ;
    entry = create_proc_entry ( PROC_NAME , 0 , init_net . proc_net ) ;
    if ( entry ) {
    entry - > proc_fops = & my_file_ops ;
    }
    printk ( KERN_INFO "Initialze my_seq_proc success!\n" ) ;
    return 0 ;
    }
    /**
    * This function is called when the module is unloaded.
    *
    */

    void cleanup_module ( void )
    {
    remove_proc_entry ( PROC_NAME , init_net . proc_net ) ;
    printk ( KERN_INFO "Remove my_seq_proc success!\n" ) ;
    }
    该程序在/proc/net下注册一个my_seq_proc文件。
    四,通过监视调试
    有时候监视用户空间应用程序的运行情况,可以捕捉到一些小问题。
    strace工具,可以显示用户空间程序所发出的所有系统调用,并显示调用参数以及字符串形式的返回值。
    常用参数:
    -t 显示调用发生时间
    -T 显示调用所花费的时间
    -e 限定被跟踪的调用类型
    -o 将输出重定向到一个文件
    五,oops消息
    大部分错误是因为对NULL指针值取值或者因为使用了其他不正确的指针指,这些错误将导致oops消息。
    oops产生原因:

    1. 引用空指针

    Unable to handle kernel NULL pointer dereference at virtual address 00000000
    printing eip:
    d083a064
    Oops: 0002 [#1]
    SMP
    CPU: 0
    EIP: 0060:[<d083a064>] Not tainted
    EFLAGS: 00010246 (2.6.6)
    EIP is at faulty_write+0x4/0x10 [faulty]
    eax: 00000000 ebx: 00000000 ecx: 00000000 edx: 00000000
    esi: cf8b2460 edi: cf8b2480 ebp: 00000005 esp: c31c5f74
    ds: 007b es: 007b ss: 0068
    Process bash (pid: 2086, threadinfo=c31c4000 task=cfa0a6c0)
    Stack: c0150558 cf8b2460 080e9408 00000005 cf8b2480 00000000 cf8b2460 cf8b2460
    fffffff7 080e9408 c31c4000 c0150682 cf8b2460 080e9408 00000005 cf8b2480
    00000000 00000001 00000005 c0103f8f 00000001 080e9408 00000005 00000005
    Call Trace:
    [<c0150558>] vfs_write+0xb8/0x130
    [<c0150682>] sys_write+0x42/0x70
    [<c0103f8f>] syscall_call+0x7/0xb
    Code: 89 15 00 00 00 00 c3 90 8d 74 26 00 83 ec 0c b8 00 a6 83 d0

    这个错误消息比较明显的,指到了空指针,位置在faulty_write 后 四个字节。

    2. 堆栈被破坏

    EIP: 0010:[<00000000>]
    Unable to handle kernel paging request at virtual address ffffffff
    printing eip:
    ffffffff
    Oops: 0000 [#5]
    SMP
    CPU: 0
    EIP: 0060:[<ffffffff>] Not tainted
    EFLAGS: 00010296 (2.6.6)
    EIP is at 0xffffffff
    eax: 0000000c ebx: ffffffff ecx: 00000000 edx: bfffda7c
    esi: cf434f00 edi: ffffffff ebp: 00002000 esp: c27fff78
    ds: 007b es: 007b ss: 0068
    Process head (pid: 2331, threadinfo=c27fe000 task=c3226150)
    Stack: ffffffff bfffda70 00002000 cf434f20 00000001 00000286 cf434f00 fffffff7
    bfffda70 c27fe000 c0150612 cf434f00 bfffda70 00002000 cf434f20 00000000
    00000003 00002000 c0103f8f 00000003 bfffda70 00002000 00002000 bfffda70
    Call Trace:
    [<c0150612>] sys_read+0x42/0x70
    [<c0103f8f>] syscall_call+0x7/0xb
    Code:Bad EIP value.

    这个错误信息比较隐晦的。 说的是,找不到一个虚拟地址。 EIP一看就是个乱七八糟的值。

    call trace不完整,只指示到了 sys_read。

    造成错误的源代码是:

    ssize_t faulty_read(struct file *filp, char __user *buf,size_t count, loff_t *pos)
    {
    int ret;
    char stack_buf[4];
    /* Let's try a buffer overflow */
    memset(stack_buf, 0xff, 20);
    if (count > 4)
    count = 4; /* copy 4 bytes to the user */
    ret = copy_to_user(buf, stack_buf, count);
    if (!ret)
    return count;
    return ret;
    }

    处理器使用的几乎都是虚拟地址,这些地址通过mmu转换成物理地址,当引用一个非法指针时,分页机制无法将该地址映射到物理地址,此时处理器就会向os发出一个页面失效的信号,如果是非法地址,内核就无法换入缺失页面,如果这时处理器处于超级用户模式,系统就会产生一个oops。
    oops显示错误发生时处理器的状态
    EIP 指令指针
    六,调试器gdb
    跟踪代码调试是比较耗时的,所以不到万不得已感觉不要走这一步
    调试内核和应用程序很不一样,用gdb对内核进行调试许多常用功能不能使用,比如设置断点观察点,单步跟踪内核函数。
    一个典型的gdb调试内核的命令如下:
    gdb /usr/src/linux/vmlinux /proc/kcore
    第一个是内核ELF可执行文件,不是经过压缩的zImage
    第二个是core文件的名字
    linux下的可装载模块是ELF格式的可执行映像,对于调试来讲,关心下面三个段:.text .bass .data
    而这三个段的信息可以在/sys/modules/scull/sections中获得,然后gdb需要做的就是:
    add-symbol-file ./scull.ko 0xd0832000 -s .bss 0xd0837100 -s .data 0xd0836e0
    有用的技巧命令
    print *(address)为address传入一个十六进制的地址值,输出是该地址对应的文件以及代码行数。
    这样的话,我们可以找到某个函数指针所指的函数定义在什么地方~~~
    7.kdb补丁
    kdb是内核内置的调试器,要获得对应内核版本的补丁程序,进行patch后重新编译内核,可以在控制台按下pause或者break键进入调试状态,如果当内核发生oops或者到达摸个断点,也会启动kdb,进入下面的状态:
    Entering kdb(0xc0234580)on processor 0 due to keyboard Entry
    [0]kdb>
    当kdb运行时,内核做的每一件事都会停下,注意不要开启网络功能,除非是在调试网络驱动,一般进入单用户模式进行kdb调试内核。
    bp设置断点
    go表示继续执行
    bt查看backtrace
    mds对数据进行处理
    mm addr value 修改addr的数据为value

    你可能感兴趣的:(linux)