第十八章 调试
18.1 准备开始
开始学习内核调试之前需要准备的东西有:
一个bug
一个藏匿bug的内核版本
相关内核代码的知识和运气
想要成功的进行调试,取决于是否能让这些错误重现
18.2 内核中的bug
隐藏在源代码中的错误到展现到目击者面前的bug,往往是经历一系列连锁反应的事件才可能触发的
一个被共享的结构体,如果没有引用计数那么他就有可能引发竞争条件
18.3 通过打印来调试
内核提供的打印函数printk()(与C库提供的printf()函数功能基本相同)就是内核的格式化打印函数。此外他还有一些自身的特殊功能
18.3.1 健壮性
健壮性是该函数最容易让人接受的一个特质,可以再任何地方任何时候调用
中断上下文和进程上下文被调用
在任何持有锁时被调用
在多处理器上同时被调用
漏洞:在系统启动过程中,终端还没有初始化之前,有些地方不能使用(然而这并没有什么卵用)。结局办法是提供一个变体函数——early_printk()
18.3.2 日志等级
printf()和printk()最主要的区别就是前者可以制定一个日志级别。内核根据这个级别来判断是否在终端上打印消息。内核把级别比某个特定值低的消息显示在终端上
18.3.3 记录缓冲区
内核消息都被保存在一个LOG_BUT_LEN大小的环形队列中单处理器的系统上默认值是16kb。换句话说就是内核在同一时间只能保存16kb的内核消息。
如果消息队列已经达到最大值那么如果再有printk调用,新消息将覆盖队列中的老消息
使用环形队列的好处
由于同时读写环形缓冲区时其同步问题分容易解决,所以即使中断上下文也可以方便的使用
使记录维护起来更容易
18.3.4 syslogd和klogd
在标准的Linux系统上,用户空间的守护进程klogd从记录缓冲区中获取内核消息,在通过syslogd守护进程将他们保存在系统日志文件中。
klogd会阻塞直到有新的内核消息可供读出。被环形之后,会读出新的内核消息并进行处理。(默认情况下把信息传给syslogd守护进程)
syslogd守护进程把收到的所有消息添加在一个文件中var/log/messages
18.4 OOPS
oops是内核告诉用户有不幸发生的最常用的方法。内核是整个系统的管理者,那一自行修复也不能把自己杀死。只能发布oops。过程包括:
向终端上输出错误消息
输出寄存器中保存的信息并输出可供追踪的线索
通常内核在发布完oops后会处于种不稳定的状态
18.4.1 ksymoops
回溯线索中的地址需要转化成有意义的符号名称才能方便使用。。这时需要调用ksymoops命令,并且还必须提供内核编译时产生的ksymoops.map
ksymoops saved_oops.txt
18.4.2 kallsyms
开发版的2.5内核引入了kallsyma特性,可以通过定义GONFIG_KALLSYMS配置选项启用
从函数的地址到符号名称的映射都必须永久的驻留在内核所映射的地址上
CONFIG_KALLSYMS_ALL:不仅存放函数名称,还存放所有的符号名称
CONFIG_KALLSYMS_EXTRA_PASS:会引起内核构建过程中再次忽略内核的目标代码
18.5 内核调试配置选项
为了方便调试和测试内核代码,内核提供了许多配置选项:
- slab层调试选项
- 高端内存调试选项
- I/O映射调试选项
- 自旋锁调试选项
- 站溢出检查选项
- 自旋锁内睡眠选项
原子操作就是指那些不能分割执行的东西,在执行时不能中断否则就是完不成的代码
原子操作计数器:可以被配置成一旦在原子操作过程中进程进入睡眠或做了一些可能引起睡眠的操作,就打印警告信息并提供追踪线索
18.6 引发bug并打印信息
BUG()和BUG_ON():方便标记bug。提供断言并输出信息的内核调用
大部分体系把他们定义成某种非法操作,这样自然会产生需要的oops
可以用panic()引发更严重的错误:不但谁打印错误信息,还会降整个系统挂起。显然只应该在最糟糕的情况下使用
if (terrible_thing)
panic("terrible thing is %ld
",terrible_thing);
有些时候,值需要在终端上打印一下栈的回溯信息来帮助调试
if (!debug_check)
printk(KERN_DEBUG "provide some information...
");
dump_stack();
18.7 神奇的系统请求键(Magic SysRq key)
这个功能可以通过定义CONFIG_MAGIC_SYSRQ配置选项来启动
无论内核处于什么状态都可以通过特殊的组合键跟内核进行通信。
启动命令:
echo 1 > /proc/sys/kernel/sysrq
输入Sysrq-h获取一份可用的选项列表:
SysRq-s:将“脏”缓冲区跟硬盘交换分区同步
SysRq-u:卸载所有的文件系统
SysRq-b:重启设备
在一行内发送三个键的组合可以重新启动濒死的系统,比reset键安全
18.8 内核调试器的传奇
18.8.1 gdb
可以使用标准的GNU调试器对正在运行的内核进程查看
gdb vmlinux /proc/kcore
vmlinux文件是未经压缩的内核映象
/proc/kcore作为一个参数选项,是作为core文件来用的
如果编译内核的时候使用了-g参数,gdb还可以提供更多的信息。
局限性:
没有任何办法修改内核数据
不能单步执行内核代码,不能加断点
18.8.2 kgdb(补丁)
作用:在远端主机上通过串口利用gdb的所有功能对内核进行调试
方法:第一台带有kgdb补丁,第二台通过串行线使用gdb对第一台计算机进行调试
可用的功能:读取或修改变量值,设置断点,设置关注变量,单步执行
18.9 探测系统
18.9.1 用UID作为选择条件
提供代替物同时不打破原有代码的可执行性
做法:保留原有算法,而把新的算法添加到其他位置,可以利用用户id作为选择条件来实现这种功能,通过选择条件,可安排到底执行那种算法
18.9.2 使用条件变量
18.9.3 使用统计量
需要掌握某个特定事件的发生规律:通过创建统计量并提供某种机制访问其统计结果。事件发生时想对应的量+1
19.9.4 重复频率限制
防止函数多次调用的方法:
- 重复使用频率限制
- 发生次数的限制
18.10 用二分法找出引发罪恶的变更
一开始,需要一个可靠的可复制的错误
接下来,需要一个能确保没问题的内核
接下来,需要一个肯定有问题的内核
最后,使用二分法检查测试
18.11 使用Git进行二分搜索
Git源码管理工具提供了一个有用的二分搜索机制,如果使用Git来控制Linux源码树的副本,那么Git将自动运行二分搜索进程
$ git bisect start //告诉Git要进行二分搜索
$ git bisect bad <version> //提供一个出现问题的最早内核版本
$ git bisect good //提供一个最新的可正常运行的内核版本
总结
调试:其实就是一种寻求一种现实与目标偏差的行为
技术
从内核内置的调试架构到调试程序
从记录日志到用git二分法查找