第十八章 调试
调试工作艰难是内核级开发区别于用户级开发的一个显著特点。
18.1 准备开始
我们需要什么?
一个bug
一个藏匿bug的内核版本
思路:假定能够让bug重现
在用户级程序中,bug直截了当。在内核中,bug不是那么清晰。
18.2 内核中的bug
往往要经历一系列的连锁反应才可能触发
需要考虑的问题:定时限制、竞争条件,它们都是运行多个线程在内核中同时运行产生的结果
18.3 通过打印来调试
Printk():内核的格式化打印函数
18.3.1 健壮性
任何时候,任何地方都可以调用printk(),它是一个弹性极佳的函数
漏洞:在系统启动过程中,终端还没有初始化之前,在某些地方不能使用它
Printf()的变体函数:early_printk(),在启动过程的初期就具有在终端上打印的能力,缺点是缺少可移植性。
18.3.2 日志等级
Printk()与printf()不同的地方:可以指定一个日志级别,内核根据这个级别来判断是否在中断上打印消息,内核把级别比某个特定值低的所有消息显示在终端上。
可以使用的记录等级在<linux/kernel.h>定义,扩展开是形如<n>的字符串,加进要打开信息的开头。内核用这种方式指定记录等级和当前终端的记录等级console_loglevel来决定是不是向终端上打印
如果没有指定记录等级,函数会选用默认的DEFAULT_MESSAGE_LOGLEVEL
内核将最重要的记录等级KERN_EMERG记为<0>,将无关紧要的记录等级KERN_DEBUG记为<7>。
18.3.3 记录缓冲区
内核消息保存在一个LOG_BUF_SHIFT大小的环形队列中,缓冲区大小可以通过CONFIG_LOG_BUF_SHIFT进行调整。在单处理器的系统上其默认值为16KB
如果消息队列已经达到最大值,再用printk()调用,新消息将覆盖队列中的老消息
优点:在中断上下文中也可以方便地使用printk(),记录维护起来也更容易
缺点:可能会丢失消息
18.3.4 syslogd和klogd
用户空间的守护进程klogd从记录空间中获取内核消息,再通过syslogd守护进程将它们保存在系统日志文件中,klogd程序既可以从/proc/kmsg文件中,也可以通过syslog()系统调用读取这些消息。
不管用什么方法,klogd都会阻塞,直到有新的内核消息可供读出。被唤醒后,默认情况下,它把消息传给syslogd守护进程。
Syslogd把它接收到的所有信息添加进一个文件中,该文件默认/var/log/messages,也可以通过/etc/syslog.conf配置文件进行重新指定。
在启动klogd的时候,可以通过指定-c标志来改变终端的记录等级
18.3.5 从printf()到printk()的转换
18.4 oops
Oops是内核告知用户有不幸发生的最常用的方式,这个过程包括向终端上输出错误消息,输出寄存器中保存的信息并输出可供跟踪的回溯线索。
发送完oops后,内核会处于一种不稳定状态,内核必须适当地从当前的上下文环境中退出并尝试恢复对系统的控制(通常结果是失败的),混乱的结果是系统死机
Oops中包含的重要信息对于所有体系结构都是完全相同的:寄存器上下文和回溯线索
回溯线索显示了导致错误发生的函数调用链,可以通过偏移量找到导致错误的语句
寄存器上下文可以重建引发问题的现场
18.4.1 ksymoops
回溯线索中的地址需要转化成有意义的符号名称才方便使用,这需要调用ksymoops命令,而且还必须提供编译内核时产生的System.map
调用方式:
Ksymoops saved_oops.txt
吐出解码版的oops
18.4.2 kallsyms
可以通过定义CONFIG_KALLSYMS配置选项启动,该选项存放着内核镜像中相应函数地址的符号名称,内核可以打印解码好的跟踪线索,但是这样会使内核变大一些,因为从函数的地址到符号名称的映射必须永久驻留在内核所映射的内存地址上。
配置选项CONFIG_KALLSYMS_ALL表示不仅存放函数名称,还存放所有符号的名称
CONFIG_KALLSYMS_EXTRA_PASS选项会引起内核构建过程中再次忽略内核的目标代码
18.5 内核调试配置选项
配置选项在内核配置编辑器的内核开发菜单项里,依赖于CONFIG_DEBUG_KERNEL
为了检查各类由原子操作引发的问题,内核提供了原子操作计数器
原子操作:能够不分割执行的东西;在执行时不能中断否则就是完不成的代码,在执行这类操作时,代码不能睡眠——使用锁时睡眠是引发死锁的元凶。正在使用一个自旋锁或禁止抢占的代码进行的就是原子操作
原子操作计数器:一旦在原子操作过程中进程进入睡眠,就打印报告信息并提供追踪线索
18.6 引发bug并打印信息
一些系统调用可以用来方便标记bug,提供断言并输出信息。
最常用的两个是BUG()和BUG_ON()。
当被调用时,它们会引发oops,导致栈的回溯和错误信息的打印。
可以把这些调用当做断言使用
If (bad_thing)
BUG();
BUG_ON(bad_thing);
Panic():调用panic()不但会打印错误消息,还会挂起整个系统
If(terrible_thing)
Panic(“terrible thing is %ld\n,terrible_thing);
Dump_stack():在终端上打印栈的回溯信息来帮助调试,只在终端上打印寄存器和函数的跟踪线索
If(!debug_check){
Printk(KERN_DEBUG “provide some information...\n”);
Dump_stack();
}
18.7 神奇的系统求救键
通过定义CONFIG_MAGIC_SYSRQ配置选项来启动
通过一个sysctl来标记该特性的开或关
Echo 1 > /proc/sys/kernel/sysrq
Sysrq-h:获取一份可用的选项列表
Sysrq-s:将”脏“缓冲区跟硬盘交换分区同步
Sysrq-u:卸载所有的文件系统
Sysrq-b:重启设备
在一行内发送这三个键的组合可以重新启动濒临死亡的系统
实际的实现在drivers/char/sysrq.c中
18.8 内核调试器的传奇
18.8.1 gdb
gdb vmlinux /proc/kcore
Vmlinux文件:未经压缩的内核映像,存放在源代码树的根目录上
/proc/kcore:作为core文件来用的,通过它能访问到内核驻留的高端内存
局限性:没有办法修改内核数据,也不能单步执行内核代码,不能加断点
18.8.2 kgdb
Kgdb是一个补丁,可以在远端主机上通过串口利用gdb的所有功能对内核进行调试
通过kgdb,gdb的所有功能都能使用
18.9 探测系统
18.9.1 用UID作为选择条件
只要保留原有的算法而把新算法加入到其他位置上,基本都能保证安全
可以利用把用户id(UID)作为选择条件来实现这种功能,通过这种选择条件,可以安排到底执行那种算法
18.9.2 使用条件变量
创建一个全局变量作为一个条件选择开关。如果该变量为零,就使用一个分支上的代码。如果不为零,就选择另外一个分支
18.9.3 使用统计量
定义两个全局变量,每当事件发生的时候,就让相应的变量加1
18.9.4 重复频率限制
如果只使用printk(),可以用一个特殊的函数去限制printk()的调用频率
If(error && printk_ratelimit())
Printk(KERN_DEBUG “error=%d\n”,error);
如果频率限制生效,那么printk_ratelimit()返回0,否则返回非0.
18.10 用二分查找法找到引发罪恶的变更
1.需要一个可靠的可复制的错误
2.需要一个能确保被问题的内核
3.需要一个肯定有问题的内核,从已知最早出现该问题的内核开始
4.在问题内核和良好内核之间使用二分法
18.11 使用Git进行二分搜索
Git源码管理中心提供了一个有用的二分搜索机制,使用Git来控制Linux源码树的副本,那么Git将自动运行二分搜索进程,并在修订版本中进行二分搜索
告诉git要进行二分搜索
$ git bisect start
为Git提供一个出现问题的最早内核版本
$ git bisect bad <revision>
如果抓到罪魁祸首
$ git bisect bad
为Git提供一个最新的可正常运行的内核版本
$ git bisect good v2.6.28
如果当前版本一切正常
$ git bisect good
如果当前版本有异常
$ git bisect bad
反复执行直到不能进行二分搜索,Git将最终打印出有问题的版本号
如果已经知道了引发bug的源,可以指定git仅仅在与错误相关的目录列表中去二分搜索提交的补丁
$ git bisect start - arch/x86
18.12 当所有的努力都失败时:社区
如果bug是在Linux内核的主流部分中,可以在内核开发社区中寻求其他开发者的帮助,向内核邮件列表发送一份电子邮件