第十八章 调试
一、调试开始前的准备
1.准备开始
- bug
- 藏匿bug的版本
- 相关内核代码的知识
成功调试的关键在于能否将错误重现
2.内核中的bug
其产生原因无数,表象变化也多种多样。从隐藏在源代码中的错误到出现的bug,可能是由一系列连锁反应触发的。
二、通过打印调试
1.健壮性
printk()函数的健壮性特质使得任何时候、任何地方都能调用它。
- 在中断上下文和进程上下中使用
- 在任何持有锁被调用时
- 在多处理器上同时被调用
2.日志等级
printk()和printf()在使用上的最主要区别是前者可以指定一个日志级别,内核根据这个级别判断终端是否打印消息;
KERN_WARNING和KERN_DEBUG都是<linux/kernel.h>中的简单宏定义,扩展开是<4>和<7>这样的字符串,加进printk()函数要打印的开头。
内核将最重要的记录等级定为<0>,最无关紧要的记录等级定为<7>。
3.记录缓冲区
- 记录消息被保存在一个环形队列中,读写按照环形队列方式进行操作
- 内核在同一时间只能保存16KB的消息,当消息队列达到最大值,再有调用时,新消息将覆盖队列中的老消息。
- 优点:同步问题很容易解决;记录维护也变得容易。
- 缺点:可能会丢失消息
4.syslogd和klogd
用户空间的守护进程klogd从记录缓存区中获取内核消息,再通过守护进程syslogd将它们保存在系统日志文件中。
三、oops
1.发布oops的过程
包括向终端上输出错误消息,输出寄存器中保存的信息并输出可供跟踪的回溯线索。通常,发送完内核会处于不稳定状态。
2.导致结果
如果发生在中断上下文时,内核会陷入混乱,系统死机;
如果在idle或init进程时发生,系统同样会陷入混乱,缺失这两个进程,内核无法工作。
如果在其他进程运行时发生,内核会杀死该进程并尝试继续执行。
3.发生原因
内存访问越界或非法指令
4.包含的信息结构
寄存器上下文和回溯线索
- 回溯线索显示导致错误发生的函数调用链
- 寄存器数据可以帮助重建引发问题现场
四、引发bug并打印信息
BUG()和BUG_ON()是常用的标记bug的内核调用,被调用时,会引发oops,导致栈的回溯和错误信息的打印。
用panic()引发更严重的错误,不但打印错误信息,还会挂起整个系统。
五、Magic SysRq Key
当该功能被启用时,无论内核处于什么状态,都可通过特殊的组合键跟内核进行通信。
- 使用定义CONFIG_MAGIC_SYSRQ配置选项和命令echo 1 > /proc/sys/kernel/sysyrq启用
六、内核调试器和系统探测
1.内核调试器
- 可以使用标准的GNU内核调试器对正在运行的内核进行查看
- 可以使用gdb的所有命令来获取信息
2.探测系统
- 用UID作为选择条件,可以安排到底执行哪种算法
- 使用条件变量,创建一个全局变量作为一个条件选择开关
- 使用统计量,通过创建统计量提供某种机制访问其统计结果
- 重复频率限制,用于某种事件发生频繁,又要观察它的整体情况。为避免信息发生井喷,可以每隔几秒执行一次打印。
七、二分查找法与GIT工具
1.当不知道哪个内核版本引入bug,但又确实存在使用二分法进行查找
假定一个版本有问题,而另一个没有,从二者正中选择版本检查,从而缩小范围,以此类推。
2.使用GIT进行二分搜索
如果使用GIT来控制Linux源码树的副本,那么它将自动进行二分搜索进程
- 告诉GIT进行二分搜索
$ git bisect start
- 为GIT提供一个出现问题的最早内核版本
$ git bisect bad <revision>
- 提供一个最新的可正常运行的内核版本
$ git bisect good 版本号
这样,GIT将自动检测正常版本和有bug的版本哪个之间有隐患,接着再编译运行以及测试正被检测版本。