20135220谈愈敏Linux Book_18

第18章 调试

调试内核艰难且风险高,关键在于对内核的深刻理解。

18.1 准备开始

需要的是:

  • 一个bug
  • 一个藏匿bug的内核版本
  • 相关内核代码的知识和运气

内核中的bug不是很清晰,调试成功的关键点在于精确的重现一个bug

18.2 内核中的bug

内核bug多种多样,表象也变化多端:

  • 明白无误的错误代码
  • 同步时发生的错误
  • 错误管理硬件
  • 降低程序运行性能
  • 毁坏数据
  • 使系统死锁

bug展现出来往往是经历一系列的连锁反应,内核调试没有特别难,但也有一些独特的问题:如定时限制和竞争条件,这些是允许多个线程在内核中同时运行产生的结果。

18.3 通过打印来调试

内核提供的打印函数printk()和C库提供的printf()功能几乎相同。

printk()特质:健壮性:任何时候任何地方都能调用。

  • 唯一缺陷:启动过程终端还没有初始化之前,某些地方不能使用。
  • 解决:printk()的变体函数:early_printk(),在启动过程的初期就具有在终端上打印的能力,缺少可移植性,但支持大多数硬件体系。

日志等级

printk()指定一个日志级别,内核把级别比某个特定值低的消息显示在终端上。

有7个等级:KERN_EMERG~KERN_DEBUG(宏),对应<0>~<7>
默认是DEFAULT_MESSAGE_LOGLEVEL(现在是KERN_WARNING,可能改)

记录缓冲区

内核消息被保存在一个LOG_BUF_LEN大小的环形队列中,大小可在编译时设置CONFIG_LOG_BUF_SHIFT进行调整。
单处理器系统上默认值是16KB,超过最大值时,新消息将覆盖老消息。

环形队列好处:

  • 同步问题容易解决
  • 记录维护容易

缺点:可能会丢失信息

syslogd和klogd是用户空间守护进程。

klogd从记录缓冲区获取内核信息(会先堵塞,到有新信息可读),传给syslogd,syslogd把它们保存在系统日志文件中(默认是/var/log/messages,可通过/etc/syslog.conf配置文件重新指定)。

读取两种方式:

  • klogd从/proc/kmsg中读取(默认)
  • syslog()系统调用

启动klogd时,可通过-c改变终端的记录等级。

18.4 oops

oops是内核告知用户有不幸发生的最常用的方式,oops包括:

  • 输出错误信息
  • 重点:输出寄存器中保存的信息(寄存器上下文)并输出可供跟踪的回溯线索(显示了导致错误发生的函数调用链)

发送完oops后,内核会处于一种不稳定状态,如果oops在idle进程(pid=0)或init进程(pid=1)时发生,系统将陷入混乱,若是其他进程,内核会杀死该进程并尝试继续执行。

ksymoops命令:将回溯线索中的地址转化为有意义的符号名称,必须提供编译内核时产生的System.map,ksymoops会自行解析,得到解码版:

ksymoops saved_oops.txt

kallsyms特性:内核引入kallsyms特性,通过定义CONFIG_KALLSYMS配置选项启用,存放着内核镜像中相应函数地址的符号名称,内核可以打印解码好的跟踪线索。不再需要System.map或ksymoops了。

18.5 内核调试配置选项

为了方便调试和测试内核代码,内核提供了许多配置选项:

位于内核配置编辑器的内核开发菜单项中,依赖于CONFIG_DEBUG_KERNEL。

如:

  • slab layer debugging slab层调试选项
  • high-memory debugging 高端内存调试选项
  • I/O mapping debugging I/O映射调试选项
  • spin-lock debugging 自旋锁调试选项
  • stack-overflow debugging 栈溢出检查选项
  • sleep-inside-spinlock checking 自旋锁内睡眠选项

原子操作:指那些能够不分隔执行的东西;在执行时不能中断否则就是完不成的代码。这时睡眠就是引发死锁的元凶。

解决:内核提供了一个原子操作计数器,在进程要进入睡眠时打印警告信息并提供追踪线索。

18.6 引发bug并打印信息

BUG()和BUG_ON():用来方便标记bug,提供断言并输出信息,会引发oops,导致栈的回溯和错误信息的打印。可以把这些调用当做断言使用,想要断言某种情况不该发生:

if (bad_thing)
	BUG();
或
BUG_ON(bad_thing);(更清晰更可读,会将其声明作为一个语句放入unlikely()中)

另:BUILD_BUG_ON():与BUG_ON()作用相同,仅在编译时调用。

panic():引发更严重的错误,不但会打印错误信息,还会挂起整个系统。

dump_stack():只在终端上打印寄存器上下文和函数的跟踪线索。

18.7 神奇的系统请求键

神奇的系统请求键是调试和挽救垂危系统所必需的一种工具:该功能被启用时,无论内核出于什么状态,都可以通过特殊的组合键和内核进行通信。

通过定义CONFIG_MAGIC_SYSRQ配置选项来启用。
除了配置选项以外,还要通过一个sysctl用来标记该特性的开或关,启动命令如下:
echo 1 > /proc/sys/kernel/sysrq

输入Sysrq-h可获取可用的选项列表,三键组合可重新启动濒临死亡的系统:

SysRq-s:将“脏”缓冲区跟硬盘交换分区同步
SysRq-u:卸载所有的文件系统
SysRq-b:重启设备

18.8 内核调试器的传奇

由于没有用于内核的调试器,许多补丁应运而生,为标准内核附加上了内核调试的支持,这些补丁功能完善,十分强大。

gdb

可以使用标准的GNU调试器对正在运行的内核进行查看。针对内核启动调试器的方法与针对进程的方法大致相同:

gdb vmlinux /proc/kcore

vmlinx:未经压缩的内核映像,不是压缩过的zImage或bImage,它存放于源代码树的根目录上。
/proc/kcore:作为一个参数选项,是作为core文件来用的,通过它能够访问到内核驻留的高端内存。只有超级用户才能读取此文件的数据。

可以使用gdb的命令来获取信息。如:

打印一个变量的值:p global_variable
反汇编一个函数:disassemble function
编译内核时使用了-g参数gdb还可以提供更多的信息。(不能当习惯,这样编译的内核会很大)

局限性:没有办法修改内核数据,不能单步执行内核代码,不能加断点

kgdb

是一个补丁 ,可以让我们在远程主机上通过串口利用gdb的所有功能对内核进行调试。这需要两台计算机:第一台运行带有kgdb补丁的内核,第二台通过串行线使用gdb对第一台进行调试。
这样通过kgdb,gdb的所有功能都能使用:

  • 读取和修改变量值
  • 设置断点
  • 设置关注变量
  • 单步执行

18.9 探测系统

使用uid作为选择条件

提供替代物的同时不打破原有代码的可执行性:把用户id(UID)作为选择条件来实现:

if (current-> uid !=7777) {    //新创建的uid=7777的用户,专门用来测试新算法。
	/* 老算法…… */
} else {
	/* 新算法…… */
}

使用条件变量

如果代码与进程无关,或者希望有一个针对所有情况都能使用的机制来控制某个特性,可以使用条件变量。

只需要创建一个全局变量作为一个条件选择开关:如果该变量为0,就使用某一个分支上的代码;否则,选择另外一个分支。通过某种接口或者调试器直接操控。

使用统计量

需要掌握某个特定事件的发生规律时使用:创建统计量,并提供某种机制访问其统计结果。

重复频率限制

当系统要显示的调试信息过多的时候,有两种技巧可以防止这类问题发生:

  • 重复频率限制:最多几秒执行一次打印。printk_ratelimit()函数可限制printk()的调节频率。
  • 发生次数限制:调试信息至多输出几次,超过次数限制后就不能打印。(用来确认在特定情况下某段代码的确被执行了。)

另:用到的变量都应该是静态的,并限制在函数局部范围以内。

注意:以上都不是SMP(对称多处理结构)安全的,理想的方式是用原子操作。

18.10 用二分查找法找出引发罪恶的变更

找到bug是什么时候引入内核源代码的,就是从哪一个版本开始有bug的。

在一个确保没有问题的内核和一个肯定有问题的内核之间使用二分法,重复筛选,将问题局限在两个相继发行的版本之间,对引发bug的代码变更进行定位。

18.11 使用Git进行二分搜索

Git可自动进行二分搜索进程找到具体哪次提交的代码引发了bug:

git bisect start  //告诉git要进行二分搜索

git bisect bad <revision>  //提供出现问题的最早内核版本,如果就是当前版本:git bisect bad

git bisect good <revision>  //提供最新的可正常运行的内核版本

git利用二分搜索法在Linux源码树中,自动检测正常的版本内核和有bug的内核版本之间那个版本有隐患,再编译、运行以及测试正被检测的版本。

如果这个版本正常:git bisect good
如果这个版本运行有异常:git bisect bad

注意:对于每一个命令,git将在一个版本的基础上反复二分搜索源码树,并且返回所查的下一个内核版本。反复执行直到不能再进行二分搜索为止,最终git会打印出有问题的版本号。    

可指定git仅在与错误相关的目录列表中(这里是arch/x86)去二分搜索提交的补丁:git bisect start - arch/x86           

18.12 当所有努力都失败时:社区

在内核开发社区中寻找其他开发者的帮助。

原文地址:https://www.cnblogs.com/tymjava/p/5333888.html