《Linux4.0设备驱动开发详解》笔记--第二十一章:Linux设备驱动的调试

时间:2022-08-29 17:57:38

21.1 GDB调式的方法

  • GDB的四个功能:

    • 启动程序,可以按照工程师自定义的要求运行程序
    • 让被调使得程序可以在指定的地方停住,断点可以是条件表达式
    • 当程序停住时,可以检查此程序发生的事,并追踪上文
    • 动态的改变程序的执行环境
  • 调式内核和应用程序时调试的命令是相同的

  • 基本命令

    • list命令(缩写l):列出代码
      • list ,显示程序第linenum行周围的源代码
      • list ,显示函数名为function的函数源程序
      • list,显示当前行前后的源程序
      • list -,显示当前行前面的源程序
      • run命令:运行程序
      • 程序运行参数
        • set args:指定运行时的参数,如set args 10 20 30
        • show args:查看设置好的运行参数
      • 运行环境
        • path :设定程序的运行路径
        • how paths:查看程序的运行路径
        • set environment varname[=value]:设置环境变量,如set env USER=baohua;
        • show environment[varname]:查看环境变量
      • 工作目录
        • cd :相当于shell的cd命令
        • pwd:显示当前所在的目录
      • 程序的输入输出
        • info terminal:显示程序用到的终端的模式
        • run>outfile:重定向控制程序输出
        • tty:指定输入的终端设备,如tty/dev/ttyS1
    • break命令
      • break:在进入指定函数时停住
      • break:指定行号停住
      • break+offset/break-offset:当前 行号的前面或者后面offset行停住
      • break filename:linenum:在源文件filename的linenum行处停住
      • break filename:function:在源文件filename的function函数处停住
      • break*address:在程序运行的内存地址处停住
      • break:break命令没有参数时,表示在下一条指令处停住
      • beak…if:…可以是上述的break、break+offset/break-offset中的参数,condition表示条件,在条件成立时停住
        • 例如:在循环体中,可以设置break if i=100,表示当i为100时停住程序
      • info:查看断点,如info breakpoints[n]、info break[n](n表示断点号)
    • 单步命令
      • step:单步跟踪,如果有函数调用,则进入该函数(进入该函数的前提是,此函数被编译有debug信息),默认一条条执行,加上count,执行后面count指令然后停止
      • next:单步跟踪,有函数则跳过,不加count,一条条执行,加上count则执行后面count条之后停住
      • set step-mode:set step-mode on用于打开step-mode模式
        • 在进行step时,若跨过某没有调试信息的函数,程序的执行会在该函数的第一条指令处停住,而不会跳过整个函数,这样可以查看该函数的机器指令
      • finish:运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址、返回值及参数值等信息
      • until(缩写为u):一直在循环体内执行单步而退步出来是一件令人烦恼的事情,用until命令可以运行程序直到退出循环体
      • stepi(缩写为si)和nexti(缩写为ni):这两个命里用于单步跟踪一条机器指令,step和next时C语言级别的命令
        • 运行display/i $pc命令之后,单步跟踪会在大厨程序代买的同时打出机器指令,即汇编代码
      • continue命令:当程序被停住后,可以用continue命令(缩写为c,fg命令同continue命令)恢复程序的执行直到程序结束,或到达下一个断点
        • 命令格式为:continue/c/fg [ignore-count],ignore-count表示忽略其后多少次断点
        • 例如:假设设置了函数断点add(),并观察i,则在continue过程中,每次遇到add()函数或者i发生变化,程序就会停住
      • print命令:再掉是程序时,当程序被停住时,可以使用print命令(缩写为p),或是同义命令inspect来查看当前程序的运行数据
        • 命令格式:print print / 其中是表达式,也是被调试的程序总的表达式,时输出的格式,比如,如果表达式按十六进制输出,则时/x
          • 表达式中,有几种GDB所支持的操作符,他们可以用在任何一种语言中
            • @:是一个和数组有关的操作符
            • :: :指定一个在文件或是函数中的变量
            • {}:表示一个指向内存地址的类型为type的对象
            • 例1:演示了查看sum[]数组的值的过程
        • 当需要查看一段连续内存空间的值时,可以使用GDB的@操作符,@的左边是第一个内存地址,@的右边是想查看内存的长度
          • 例2:动态申请内存
        • 输出格式:
          • x:十六进制
          • d:十进制
          • u:按十六进制,显示无符号整型
          • o:八进制
          • t:二进制
          • a:十六进制
          • c:字符格式
          • f:浮点数格式
        • display命令:设置一些自动显示的变量,当程序停住时,或是单步跟踪时,这些变量会自动显示
        • 修改变量:print 变量=值
        • 当GDB的print查看程序运行时的数据时,每个print都会被GDB记录下来。GDB会以 1 2,$3。。。这样的方式为每一个print命令编号,可以用这个编号访问前面的表达式
      • watch命令:观察某个表达式(变量也是一种表达式)的值是否有了变化,有则马上停止运行
        • watch:为表达式expr设置一个观察点,一旦这个表达式发生了变化则停止运行
        • rwatch:当表达式(变量)被读时,停止程序执行
        • awatch:当表达式(变量)的值被读或者被写时,停止运行
        • info watchpoints:列出当前所设置的所有观察点
      • examine命令:查看内存地址中的值
        • 语法:x/
**//例1:**
(gdb) print sum
$2 = {133, 155, 0, 0, 0 ,0 ,0 ,0 ,0 ,0}
(gdb) next

Breakpoint 1, main () at gdb-example.c:25
25 sum[i] = add(array1[i], array2[i]):
(gdb) next
23 for(i = 0; i< 10; i++)
(gdb) print sum
$3 = ({133, 155, 143, 0, 0 ,0 ,0 ,0 ,0 ,0}

//例2:
int *array = (int *) malloc (len * sizeof(int));
在GDB调试过程中个,这样实现这个动态数组的值:
p *array@len

//例3:
main()
{
void *p = malloc(16);
while(1);
}
//用如下命令来修改p指向的内存
(gdb) set *(unsigned char *)p='h'
(gdb) set *(unsigned char *)p='e'
(gdb) set *(unsigned char *)p='l'
(gdb) set *(unsigned char *)p='l'
(gdb) set *(unsigned char *)p='e'
//查看结果
(gdb) x/s p
0x804b008 "hello"
//查看函数func反汇编代码
(gdb) disassemble func
Dump of assembler code for function func:
0x8048450 <func>: push %ebp
0x8048451 <func+1> mov %esp,%ebp
...

21.2 内核调试

21.2.1 内核打印信息—-printk()

  • 内核打印语句printk()会将内核信息输出到内核信息缓冲区中,内核缓冲区时候在kernel/printk.c中通过以下语句静态定义:
    • 内核缓冲区是一个环形缓冲区(Ring Buffer),如果消息过多,则会将之前的消息冲刷掉
static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);
  • printk()的8个消息级别,分别是0~7,级别越低(数值越大),消息越不重要,0级时紧急事件级,7级时调试级

    • #define KERN_EMERG “<0>”:紧急事件,一般是系统崩溃前的提示信息
    • #define KERN_ALERT “<1>”:必须立即采取行动
    • #define KERN_CRIT “<2>”:临界状态,通常涉及严重的硬件或软件操作失败
    • #define KERN_ERR “<3>”:用于报告错误状态,设备驱动程序会经常调用KERN_ERR来报告来自硬件的问题
    • #define KERN_WARNING “<4>”:对可能出现问题的情况进行警告,这类情况通常不会对系统造成严重的问题
    • #define KERN_NOTICE “<5>”:有必要进行提示的正常情形,许多与安全相关的状况用这个级别进行汇报
    • #define KERN_INFO “<6>”:内核提示信息,很多驱动程序在启动的时候,用这个级别打印他们找到的硬件信息
    • #define KERN_DEBUG “<7>”:用于调试
  • 通过/proc/sys/kernel/printk文件可以调节printk()的输出等级,该文件有4个等级

    • 控制台日志级别:当前的打印级别,优先级高于该值的信息将被打印纸控制台
    • 默认的信息日志级别:将用该优先级来打印没有优先级前缀的消息,也就是直接写printk(“xxx”)而不带打印级别的情况下,会用该级别打印
    • 最低的控制台日志级别:控制台日志级别可被设置的最小值(一般都是1)
    • 默认的控制台日志级别:控制台日志级别的默认值
      • 例5:Ubuntu上的输出级别
//例5:Ubuntu上的输出级别
$ cat /proc/sys/kernel/printk
4 4 1 7
  • 显示内核打印信息方法

    • 通过dmesg命令,如果使用dmesg -c命令,则不仅会显示__log_buf,还会清除该缓冲区的内容
    • 使用cat /proc/kmsg 命令,/proc/kmsg是一个“永无休止的文件”,因此,cat /porc/kmsg的进程只能通过“Ctrl+C”或kill终止
  • 设备驱动中的调试函数

    • pr_debug(),pr_info()
      • 使用pr_xxx()族API的好处是,可以在文件开头通过pr_fmt()定义一个打印格式
      • 例6:在kernel/watchdog.c的最开头通过如下定义可以保证之后watchdog.c调用的所有pr_xxx()打印的消息都自动带有“NMI watchdog: ”的前缀
    • dev_debug():如dev_dbg()、dev_err()、dev_info()等
      • 使用dev_xxx()族API打印的时候,设备名称会被自动加到打印消息的前头
      • 打印的附加信息,例7
        • func:输出printk()调用所在的函数名
        • LINE:输出其所在的代码行
        • FILE:输出源代码命令名
//pr_debug()与pr_info()定义
#ifdef DEBUG
#define pr_debug(fmt,arg...) \
printk(KERN_DEBUG fmt,##arg)
#else
static inline int __attribute__ ((format (printf,1,2))) pr_debug(cost char * fmt, ...)
{
return 0;
}
#endif

#define pr_infor(fmt,arg ...) \
printk(KERN_INFO fmt, ##arg)
//例6:
#define pr_fmt(fmt) "NMI watchdog: " fmt

#include <linux/mm.h>
#include <linux/cpu.h>
#include <linux/nmi.h>
...
//例7
printk(KERN_ERR "Assertion failed! %s,%s,%s,line=%d", #expr, __FILE__, __func__, LINE);

21.2.2 DEBUG_LL和EARLY_PRINTK

  • DEBUG_LL对应内核的Kernel low-level debugging功能,EARLY_PRINTK对应内核中一个早起的控制台
    • 为了在内核的drivers/serial下的控制台驱动初始化之前支持打印,可以选择上述两个配置项,另外需要在bootargs中设置earlyprintk的选项

21.2.3 使用“/proc”

  • “/proc”是一个虚拟文件系统,通过它可以在Linux内核空间和用户控件之间进行通信
    • 在“/proc”文件系统中,可以将对虚拟文件的读写作为与内核中实体进行通信的一种手段,这些虚拟文件的内容都是动态的
    • “/proc”下的绝大多数文件是只读的,以显示内核信息为主,也不都是只读,如修改/proc/sys/kernel/printk以改变printk()的打印级别
    • Linux系统的许多命令本身都是通过分析”/proc”下的文件来完成的,如ps、top等,例如,free命令通过分析/proc/meminfo文件的到可用内存信息

21.2.4 Oops

  • 内核出现类似用户空间的Segmentation Fault时,Oops会被打印到控制台和写入内核log缓冲区
    • 反汇编一个目标文件:arm-linux-gnueabihf-objdump -d -s xxx.o

21.2.5 BUG_ON()和WARN_ON()

  • 内核中许多地方调用类似BUG()的语句,它非常像一个内核运行时的断言,以为这本来不该执行到BUG()这条语句,一旦执行到即跑出Oops
    • BUG()语句定义如下:
      • panic()定义在kernel/panic.c中,会导致内核崩溃,并打开Oops
#define BUG() do { \
printk("BUG: failure at %s:%d/%s()!\n", __FILE__, __LINE__, __func__"); \
panic("
BUG!");
}while(0)
  • BUG_ON():时BUG()的变体,只有当括号内的条件成立的时候,才抛出Oops
  • WARN_ON():在括号里的条件成立的时候,内核会抛出栈回溯,但是不会panic(),这通常用于内核抛出一个警告,暗示某种不太合理的事情发生了

21.2.8 strace

  • strace是个有效的跟踪工具,它的主要特点是可以被用来监视系统调用
    • 既可以调试一个新开始的程序,也可以调试一个已经在运行的程序(这意味着把strace绑定到一个已有的PID上)