【原创】《Linux设备驱动程序》学习之循序渐进 --- 调试技术


【原创】《Linux设备驱动程序》学习之循序渐进 --- 调试技术


第四章 --- 调试技术

内核编程带有它自己的, 独特的调试挑战性. 内核代码无法轻易地在一个调试器下运行, 也无法轻易的被跟踪, 因为它是一套没有与特定进程相关连的功能的集合. 内核代码错误也特别难以重现, 它们会牵连整个系统与它们一起失效, 从而
破坏了大量的能用来追踪错误的证据。一句话,内核编程的调试相对于应用程序来说比较困难。

我们建议你建立并安装你自己的内核, 而不是运行来自你的发布商的现成的内核. 运行你自己的内核的最充分的理由之一是内核开发者已经在内核自身中构建了多个调试特性. 但是这些特性能产生额外的输出并降低性能, 因此发布商的产品内核中往往不会使能它们.除了另外指出的, 所有的这些选项都在 "kernel hacking" 菜单, 注意有些选项不是所有体系都支持.  

最普通的调试技术是监视, 在应用程序编程当中是通过在合适的地方调用 printf 来实现. 在你调试内核代码时, 你可以通过 printk 来达到这个目的.

printk 

printk 允许你根据消息的严重程度对其分类, 通过附加不同的记录级别或者优先级在消息上. 你常常用一个宏定义来指示记录级别.

有 8 种可能的记录字串, 在头文件 <linux/kernel.h> 里定义; 我们按照严重性递减的顺序列出它们: 
KERN_EMERG 
用于紧急消息, 常常是那些崩溃前的消息. 
KERN_ALERT 
需要立刻动作的情形. 
KERN_CRIT 
严重情况, 常常与严重的硬件或者软件失效有关. 
KERN_ERR 
用来报告错误情况; 设备驱动常常使用 KERN_ERR 来报告硬件故障. 
KERN_WARNING 
有问题的情况的警告, 这些情况自己不会引起系统的严重问题. 
KERN_NOTICE 
正常情况, 但是仍然值得注意. 在这个级别一些安全相关的情况会报告. 
KERN_INFO 
信息型消息. 在这个级别, 很多驱动在启动时打印它们发现的硬件的信息. 
KERN_DEBUG 
用作调试消息. 

每个字串( 在宏定义扩展里 )代表一个在角括号中的整数. 整数的范围从 0 到 7, 越小的数表示越大的优先级. 一条没有指定优先级的 printk 语句缺省是 DEFAULT_MESSAGE_LOGLEVEL, 在kernel/printk.c 里指定作为一个整数. 

printk 函数将消息写入一个 __LOG_BUF_LEN 字节长的环形缓存, 长度值从 4 KB 到 1 MB, 由配置内核时选择. 这个函数接着唤醒任何在等待消息的进程, 就是说, 任何在系统调用中睡眠或者在读取 /proc/kmsg 的进程. 

dmesg 命令可用来查看缓存的内容, 不会冲掉它; 实际上, 这个命令将缓存区的整个内容返回给 stdout, 不管它是否已经被读过.

Linux 对于消息的解决方法的另一个特性是, printk 可以从任何地方调用, 甚至从一个中断处理里面, 没有限制能打印多少数据. 唯一的缺点是可能丢失一些数据.

如果你要避免你的系统被来自你的驱动的监视消息击垮, 你或者给 klogd 指定一个 -f (文件) 选项来指示它保存消息到一个特定的文件, 或者定制 /etc/syslog.conf 来适应你的要求. 但是另外一种可能性是采用粗暴的方式: 杀掉 klogd 和详细地打印消息在一个没有用到的虚拟终端上, 或者从一个没有用到的 xterm 上发出命令 cat /proc/kmsg. 

重定向到控制台

Linux 在控制台记录策略上允许一些灵活性, 它允许你发送消息到一个指定的虚拟控制台(如果你的控制台使用的是文本屏幕). 缺省地, 这个"控制台"是当前虚拟终端. 为了选择一个不同地虚拟终端来接收消息, 你可对任何控制台设备调用 ioctl(TIOCLINUX). 程序setconsole, 可以用来选择哪个控制台接收内核消息; 它必须由超级用户运行, 可以从 misc-progs 目录得到. 

开启及关闭消息

这里我们展示一种编码 printk 调用的方法, 你可以单独或全局地打开或关闭它们; 这个技术依靠定义一个宏, 在你想使用它时就转变成一个 printk (或者 printf)调用. 
  每个 printk 语句可以打开或关闭, 通过去除或添加单个字符到宏定义的名子. 
  所有消息可以马上关闭, 通过在编译前改变 CFLAGS 变量的值. 
  同一个 print 语句可以在内核代码和用户级代码中使用, 因此对于格外的消息, 驱动和测试程序能以同样的方式被管理. 
下面的代码片断实现了这些特性, 直接来自头文件 scull.h: 

#undef PDEBUG /* undef it, just in case */ 
#ifdef SCULL_DEBUG 
# ifdef __KERNEL__ 
 
/* This one if debugging is on, and kernel space */ 
# define PDEBUG(fmt, args...) printk( KERN_DEBUG "scull: " fmt, ## args) 
# else 
 
/* This one for user space */ 
# define PDEBUG(fmt, args...) fprintf(stderr, fmt, ## args) 
# endif 
#else 
# define PDEBUG(fmt, args...) /* not debugging: nothing */ 
#endif 
 
#undef PDEBUGG #define PDEBUGG(fmt, args...) /* nothing: it's a placeholder */ 
为进一步简化过程, 添加下面的行到你的 makfile 里:

# Comment/uncomment the following line to disable/enable debugging 
DEBUG = y 
 
# Add your debugging flag (or not) to CFLAGS 
ifeq ($(DEBUG),y) 
 DEBFLAGS = -O -g -DSCULL_DEBUG # "-O" is needed to expand inlines 
else 
 DEBFLAGS = -O2 
endif 
 
CFLAGS += $(DEBFLAGS) 

速率限制

在很多情况下, 最好的做法是设置一个标志说, "我已经抱怨过这个了", 并不打印任何后来的消息只要这个标志设置着. 然而, 有几个理由偶尔发出一个"设备还是坏的"的提示. 内核已经提供了一个函数帮助这个情况:  
int printk_ratelimit(void);  
这个函数应当在你认为打印一个可能会常常重复的消息之前调用. 如果这个函数返回非零值, 继续打印你的消息, 否则跳过它. 这样, 典型的调用如这样:  
if (printk_ratelimit()) 
    printk(KERN_NOTICE "The printer is still on fire\n"); 
printk_ratelimit 通过跟踪多少消息发向控制台而工作. 当输出级别超过一个限度, printk_ratelimit 开始返回 0 并使消息被扔掉. 
printk_ratelimit 的行为可以通过修改 /proc/sys/kern/printk_ratelimit( 在重新使能消息前等待的秒数 ) 和 /proc/sys/kernel/printk_ratelimit_burst(限速前可接收的消息数)来定制. 

打印设备号

偶尔地, 当从一个驱动打印消息, 你会想打印与感兴趣的硬件相关联的设备号. 打印主次编号不是特别难, 但是, 为一致性考虑, 内核提供了一些实用的宏定义( 在 <linux/kdev_t.h> 中定义)用于这个目的:  
int print_dev_t(char *buffer, dev_t dev);  
char *format_dev_t(char *buffer, dev_t dev); 
两个宏定义都将设备号编码进给定的缓冲区; 唯一的区别是 print_dev_t 返回打印的字符数, 而 format_dev_t 返回缓存区; 因此, 它可以直接用作 printk 调用的参数, 但是必须记住 printk 只有提供一个结尾的新行才会刷行. 缓冲区应当足够大以存放一个设备号; 如果 64 位编号在以后的内核发行中明显可能, 这个缓冲区应当可能至少是 20 字节长. 

通过查询调试

大量使用 printk 能够显著地拖慢系统, 即便你降低 cosole_loglevel 来避免加载控制台设备, 因为 syslogd 会不停地同步它的输出文件; 因此, 要打印的每一行都引起一次磁盘操作.然而, 你不想只是为了调试信息的原因而拖慢你的系统. 可以在出现于 /etc/syslogd.conf 中的你的日志文件名前加一个连字号来解决这个问题。

使用 /proc 文件系统

/proc文件系统是一个特殊的软件创建的文件系统, 内核用来输出消息到外界. /proc 下的每个文件都绑到一个内核函数上, 当文件被读的时候即时产生文件内容. 我们已经见到一些这样的文件起作用; 例如, /proc/modules, 常常返回当前已加载的模块列表. 

seq_file 接口 

如我们上面提到的, 在 /proc 下的大文件的实现有点麻烦. 一直以来, /proc 方法因为当输出数量变大时的错误实现变得声名狼藉. 为使内核开发者活得轻松些, 添加了 seq_file 接口. 这个接口提供了简单的一套函数来实现大内核虚拟文件. 我们建议使用 seq_file , 来实现包含多个非常小数目的输出行数的文件.

ioctl 方法 

我们在第 6 章展示给你如何使用, 是一个系统调用, 作用于一个文件描述符; 它接收一个确定要进行的命令的数字和(可选地)另一个参数, 常常是一个指针. 

通过监视调试

有几个方法来监视用户空间程序运行. 你可以运行一个调试器来单步过它的函数, 增加打印语句, 或者在 strace 下运行程序. 这里, 我们将讨论最后一个技术。strace 命令是一个有力工具, 显示所有的用户空间程序发出的系统调用. 它不仅显示调用, 还以符号形式显示调用的参数和返回值. 当一个系统调用失败, 错误的符号值(例如, ENOMEM)和对应的字串(Out of memory) 都显示. strace 有很多命令行选项; 其中最有用的是 -t 来显示每个调用执行的时间, -T 来显示调用中花费的时间, -e 来限制被跟踪调用的类型, 以及-o 来重定向输出到一个文件. 缺省地, strace 打印调用信息到 stderr.

调试系统故障 

即便你已使用了所有的监视和调试技术, 有时故障还留在驱动里, 当驱动执行时系统出错. 当发生这个时, 能够收集尽可能多的信息来解决问题是重要的. 注意"故障"不意味着"崩溃". Linux 代码是足够健壮地响应大部分错误:一个故障常常导致当前进程崩溃, 系统仍会继续运行。

大部分 bug 是对 NULL 指针取值或者使用其他不正确指针值. 此类 bug 通常的输出是一个 oops 消息.

使用 gdb 

gdb 对于看系统内部是非常有用. 在这个级别精通调试器的使用要求对 gdb 命令有信心, 需要理解目标平台的汇编代码, 以及对应源码和优化的汇编码的能力. 
调试器必须把内核作为一个应用程序来调用. 除了指定内核映象的文件名之外, 你需要在命令行提供一个核心文件的名子. 对于一个运行的内核, 核心文件是内核核心映象, /proc/kcore. 一个典型的 gdb 调用看来如下:  
gdb /usr/src/linux/vmlinux /proc/kcore  
第一个参数是非压缩的 ELF 内核可执行文件的名子, 不是 zImage 或者 bzImage 或者给启动环境特别编译的任何东东. gdb 命令行的第二个参数是核心文件的名子. 

kdb 内核调试器 

许多读者可能奇怪为什么内核没有建立更多高级的调试特性在里面.答案, 非常简单, 是 Linus 不相信交互式的调试器. 他担心它们会导致不好的修改, 这些修改给问题打了补丁而不是找到问题的真正原因. 因此, 没有内嵌的调试器. 其他内核开发者, 但是, 见到了交互式调试工具的一个临时使用. 一个这样的工具是 kdb 内嵌式内核调试器, 作为来自 oss.sgi.com 的一个非官方补丁. 要使用 kdb, 你必须获得这个补丁(确认获得一个匹配你的内核版本的版本), 应用它, 重建并重安装内核.

原文链接:


posted @ 2014-07-10 15:35  GengLUT  阅读(261)  评论(0编辑  收藏  举报