`

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

 
阅读更多
一,内核中的调试支持
在内核配置菜单中有“kernel hacking”菜单选项,这些选项帮助用户检查很多错误,这里我列了一个表方便大家参考:
kernel hacking 在哪里?
~# cd /usr/src/linux-source.3.0.0
~#make menuconfig 则出现
查找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()。
    structseq_operations{
    void*(*start)(structseq_file*m,loff_t*pos);
    void(*stop)(structseq_file*m,void*v);
    void*(*next)(structseq_file*m,void*v,loff_t*pos);
    int (*show)(structseq_file*m,void*v);
    };
    start():
    主要实现初始化工作,在遍历一个链接对象开始时,调用。返回一个链接对象的偏移或者SEQ_START_TOKEN(表征这是所有循环的开始)。出错返回ERR_PTR。
    stop():
    当所有链接对象遍历结束时调用。主要完成一些清理工作。
    next():
    用来在遍历中寻找下一个链接对象。返回下一个链接对象或者NULL(遍历结束)。
    show():
    对遍历对象进行操作的函数。主要是调用seq_printf(), seq_puts()之类的函数,打印出这个对象节点的信息。
    下图描述了seq_file函数对一个链表的遍历。
    2、重要的数据结构
    除了struct seq_operations以外,另一个最重要的数据结构是structseq_file
    structseq_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*/
    #definePROC_NAME"my_seq_proc"
    MODULE_LICENSE("GPL");
    staticvoid*my_seq_start(structseq_file*s,loff_t*pos)
    {
    staticunsignedlongcounter=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");
    returnNULL;
    }
    }
    staticvoid*my_seq_next(structseq_file*s,void*v,loff_t*pos)
    {
    unsignedlong*tmp_v=(unsignedlong*)v;
    printk(KERN_INFO"Invoke next\n");
    (*tmp_v)++;
    (*pos)++;
    returnNULL;
    }
    staticvoidmy_seq_stop(structseq_file*s,void*v)
    {
    printk(KERN_INFO"Invoke stop\n");
    /* nothing to do, we use a static value in start() */
    }
    staticintmy_seq_show(structseq_file*s,void*v)
    {
    printk(KERN_INFO"Invoke show\n");
    loff_t*spos=(loff_t*)v;
    seq_printf(s,"%Ld\n",*spos);
    return0;
    }
    staticstructseq_operations my_seq_ops={
    .start=my_seq_start,
    .next=my_seq_next,
    .stop=my_seq_stop,
    .show=my_seq_show
    };
    staticintmy_open(structinode*inode,structfile*file)
    {
    returnseq_open(file,&my_seq_ops);
    };
    staticstructfile_operations my_file_ops={
    .owner=THIS_MODULE,
    .open=my_open,
    .read=seq_read,
    .llseek=seq_lseek,
    .release=seq_release
    };
    intinit_module(void)
    {
    structproc_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");
    return0;
    }
    /**
    * This function is called when the module is unloaded.
    *
    */

    voidcleanup_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设备驱动详解第二版

      Linux设备驱动详解【第二版】,作者宋宝华,此版PDF是经过本人整理的文字版PDF,带目录、高清无水印版。...第4篇 Linux设备驱动调试、移植 第22章 Linux设备驱动的调试 564 第23章 Linux设备驱动的移植 602

      LINUX 设备驱动程序(第二版)

      第4章 调试技术 第5章 字符设备驱动程序的扩展操作 第6章 时间流 第7章 获取内存 第8章 硬件管理 第9章 中断处理 第10章 合理使用数据类型 第11章 kerneld和高级模块化 第12章 加载快设备驱动程序 第13章 MMAP和DMA ...

      精通LINUX设备驱动程序开发

       第4章 基本概念   第5章 字符设备驱动程序   第6章 串行设备驱动程序   第7章 输入设备驱动程序   第8章 I2C协议  第9章 PCMCIA和CF   第10章 PCI   第11章 USB  第12章 视频驱动程序   ...

      精通Linux设备驱动程序开发

      第4章 基本概念 第5章 字符设备驱动程序 第6章 串行设备驱动程序 第7章 输入设备驱动程序 第8章 I2C协议 第9章 PCMCIA和CF 第10章 PCI 第11章 USB 第12章 视频驱动程序 第13章 音频驱动程序 第14章 块...

      嵌入式linux设备驱动(共17章)

      第4章 调试技术 第5章 字符设备驱动程序的扩展操作 第6章 时间流 第7章 获取内存 第8章 硬件管理 第9章 中断处理 第10章 合理使用数据类型 第11章 kerneld和高级模块化 第十二章 加载快设备驱动程序 第十三章 MMAP...

      Linux设备驱动程序.pdf

      第4章介绍了调试技术。内核开发中难免会遇到各种问题,因此掌握有效的调试技术至关重要。本章提供了一些内核调试支持的细节,包括打印调试、查询调试、使用观察(watchpoint)来调试,以及系统故障的调试方法。调试...

      Linux驱动程序开发第三版

      《Linux驱动程序开发第三版》是一本专注于Linux操作系统下驱动程序开发的专业书籍,适用于那些希望深入理解Linux内核机制和想要提升驱动程序编写能力的开发者。本书详细阐述了如何在Linux环境中设计、实现和调试设备...

      国嵌培训课件Linux驱动程序设计

      第一天 1.Linux驱动简介 2.字符设备驱动程序设计 3.驱动调试技术 4. 并发与竞态 第二天 1.Ioctl型驱动 2.内核等待队列 3. 阻塞型驱动程序设计 4.Poll设备操作 第三天 1.Mmap设备操作 2. 硬件访问 3. 混杂...

      Linux驱动开发第三版(英文)

      通过阅读《Linux驱动开发第三版》,读者不仅可以掌握Linux驱动程序开发的基本技术,还能了解到最新的Linux内核特性。书中丰富的实例和详尽的解释,对于提升读者的实践能力非常有帮助,无论你是初学者还是经验丰富的...

      Linux设备驱动开发 IDE驱动

      学习和参考华清远见的Linux设备驱动开发IDE驱动代码,可以帮助你深入理解这些概念和技术,从而能够独立编写和调试IDE驱动。这不仅有助于你提升Linux内核编程能力,也有助于解决实际项目中遇到的硬件兼容性和性能问题...

      《LINUX驱动程序开发实例第2版》_冯国进编著_2017.zip

      《LINUX驱动程序开发实例第2版》是冯国进先生编著的一本关于Linux内核驱动程序开发的专业书籍,该书深入浅出地讲解了Linux系统下的驱动编写技术,旨在帮助读者掌握如何为Linux系统编写高效、稳定的硬件驱动程序。...

      LINUX设备驱动程序(Linux.Device.Driver)

      《LINUX设备驱动程序》(Linux.Device.Driver) 第三版是深入探讨Linux内核设备驱动程序开发的重要参考资料,尤其对于那些希望理解Linux系统如何与硬件交互的开发者来说,这本书是不可或缺的。书中详细阐述了如何编写...

      嵌入式Linux设备驱动程序开发.pdf

      4. 设备驱动程序的调试:在调试设备驱动程序时,需要使用各种调试工具和方法,例如使用gdb调试器。 四、嵌入式Linux设备驱动程序的关键代码 嵌入式Linux设备驱动程序的关键代码主要包括设备驱动接口的实现、文件...

      Linux驱动书籍汇总

      这本书是Linux驱动程序开发的经典之作,全面覆盖了从基础到高级的Linux驱动编程技术。书中主要讲解了以下内容: - Linux内核架构:介绍Linux内核的基本组织结构和工作原理。 - 驱动程序基础:讲解设备驱动的分类...

      linux设备驱动程序 (中文第二版)

      通过阅读《Linux设备驱动程序》(中文第二版),读者不仅可以理解Linux设备驱动的基本原理,还能掌握编写驱动程序的实践技能,为开发和调试Linux系统的硬件设备打下坚实基础。无论是初学者还是有经验的开发者,都能...

      marvell_linux驱动

      - 访问官方网站或第三方网站获取 Marvell Yukon 88E8056 网卡驱动,文件名为 `Marvell_LAN_CAP_Linux_105013.zip`。 - 注意:官方网站可能不直接提供适用于 RHEL5 的驱动,但通常情况下旧版驱动能够兼容新版系统。...

      linux驱动开发,linux驱动开发

      通过学习这四章内容,你将能够掌握Linux驱动开发的基本概念和技术,了解如何编写和调试驱动程序,从而能够有效地控制和优化硬件设备。在实际项目中,你可能需要结合具体的硬件手册和Linux内核源码,进行更深入的学习...

      嵌入式Linux应用开发完全手册有目录2

       第18章 Linux内核调试技术 第4篇 嵌入式Linux设备驱动开发篇  第19章 字符设备驱动程序  第20章 Linux异常处理体系结构  第21章 扩展串口驱动程序移植  第22章 网卡驱动程序移植  第23章 IDE...

    Global site tag (gtag.js) - Google Analytics