第18章 调试
准备:
- 一个bug
- 一个藏匿bug的内核版本
- 相关内核代码的知识和运气
想要成功地进行调试,就取决于是否能让这些错误重现
有关printk
p1:printk的健壮性——任何时候任何地方都能调用到它,并且弹性极佳。但是!在终端没有初始化时,有些地方不能使用它。
这里有一个解决手段:
early_think()
这个函数在启动过程的初期就具有在终端上打印的能力。(但是该函数在一些内核支持的硬件结构上无法实现,所有缺乏可移植性)
=>除非在启动过程的初期就要在终端上输出,否则可以认为printk()在什么情况下都能够工作。
printk和printf上使用上最主要的区别就是printk可以指定一个日志级别——内核根据这个级别来判断是否在终端上打印消息。
日志级别的指定方式
printk(KERN WARNING "This is warning!\n");
printk(KERN DEBUG "This is a debug notice!\n");
printk("I did not spectify a loglevel!\n");
18-1
KERN WARNING和KERN DEBUG是<linux/kernel.h>中的简单宏定义。需要加入printk()函数要打印的信息开头。内核通过这个指定的记录等级和当前终端的记录等级console_loglevel来决定是不是向终端上打印。
(如果没有特别的指定一个记录等级,默认使用DEFAULT_MESSAGE_LOGLEVEL,现在的默认等级是KERN_WARNING,但这个默认值具有变化的可能性)
最重要的等级是KERN_EMERG定为<0>,将无关紧要的记录等级“KERN_DEBUG定义为<7>”。
记录缓冲区
内核信息都被保存在一个LOG_BUF_LEN大小环形队列之中。
该缓冲区大小可以在编译时通过CONFIG_BUF_SHIFT进程调整。
在单处理器上其默认值是16kb。如果队列已经达到最大值,那么如果再printk()调用时 ,新消息将覆盖队列中的老消息。
使用环形队列的好处:
1)由于同时读写环形缓冲区时,其同步问题很容易解决,所以即使在中断上下文中也可以方便地使用printk();
2)它使记录维护起来更容易,如果有大量的消息同时产生,新消息只需覆盖掉它本身,但不会因此失控而消耗大量内存。
syslogd和klogd
在标准的Linux系统上,用户空间的守护进程Klogd从记录缓冲区中获取内核信息 ,再通过syslogd守护进程将它们保存在系统日志文件中。
klogd程序既可以从/proc/kmsg文件中,也可以通过syslog()系统调用读取这些信息。
默认情况下,它选择读取/proc方式实现,不管是哪种方式,klod都会阻塞,直到有新的内核消息可供读出。
在被唤醒之后,它会读取新的内核消息并进行处理。默认情况下,它就是把消息传给syslogd守护进程。
syslogd守护进程把它收到的所有消息添加进一个文件中,该文件默认是/var/log/messages,也可以通过/etc/syslog.conf配置文件重新指定。
在启动klod的时候,可以通过指定-c标志来改变终端的记录等级。
oops
1)oops在中断上下文时发生,内核根本无法继续。它会陷入混乱(结局——死机)。
2)oops在idle进程(pid为0)或init进程(pid为1)时也会发生混乱
oops包含的重要信息对于所有体系结构都是相同的:寄存器上下文和回溯线索。
回溯线索显示错误导致错误发生的函数调用链。
回溯线索中的地址需要转化成有意义的符号才能方便使用。这需要调用ksyoops命令,并且还必须提供编译内核时产生的System.map,调用方式:
ksymoops saved_oops.txt
kallsms特性,它可以通过定义CONFIG_KALLSYMS配置选项启用。
该选项放着相应函数地址的符号名称,所以内核可以打印解码好的跟踪线索。
配置选项CONFIG_KALLSYMS_ALL表示不仅存放在函数名称,还存放所有的符号名称。
CONFIG_KALLSYMS_EXTRAPASS选项会引起内核构建过程中再次忽略内核的目标代码。这个选项只有调试kallsyms本身时才会用。
内核调试配置选项
为了方便调试和测试内核代码,内核提供许多配置选项,在内核配置编译器的内核开发菜单选项中,它们都依赖于CONFIG_DEBUG_KERNEL
应该启用的一些选项:
slab layer debuggnig(slab层调试选项)
high-memory debugging(高端内存调试选项)
I/O mapping debuging(I/O映射选项)
spin-lock debuging(自旋锁调试选项)
stack-overflow checking(栈溢出检查选项)
原子操作指那些能够不分隔执行的东西,在执行过程中不能中断否则就是完不成的代码。
正在使用一个自旋锁或禁止抢占的代码就是原子操作。
在执行此类操作时,代码不能沉睡——使用锁时睡眠是引发死锁的元凶。
一个原子计数器:可以被配置成一旦在原子操作过程中进程进入睡眠或者做了一些引起睡眠的操作,就打印警告并提供追踪线索。
下面这些选项可以最大限度地利用特性:
CONFIG_PREEMENT=y
CONFIG_DEBUG_KERNEL=y
CONFIG_KALLSYMS=y
CONFIG_DEBUG_SPINLOCK_SLEEP=y
标记和提供断言输出的内核调用:
BUG()
BUG_ON()
当被调用的时候,它们会引发oops,导致栈的回溯和错误信息的打印。
可以把这些调用当做断言使用,想要断言某种情况不该发生:
if (bad thing)
bug();
或
BUG_ON(bad_thing)
BUG_ON()比BUG()更加清晰,而且BUG_on()会将其声明作为一个语句放入unlikely()中 。BUILD_BUG_ON和BUG()作用相同,仅在编译时使用。如果编译阶段已提供的声明为真,那么编译将会一个错误而终止。
可以用pianc()引发更严重的错误。调用panic()不但会打印出错误信息,而且还会挂起整个系统,显然只在最糟糕的时候使用它。
在终端上打印栈的回溯信息来帮助调,使用dump_stack()。
神奇的系统请求键:
该功能通过CONFIG_MAGIC_SYSRQ配置选项来启用。
Sysqn(系统请求)在大多数键盘上都是标准键。
还要通过一个Sysctl用来标记该特性的开或关。需要启用它时使用如下命令:
echo 1 >/proc/sys/kernel/sysrq
从终端上,可以输入Sysrq-h获取一份可用的选项列表,SysRq-s将“脏”缓冲区和硬盘交换分区同步,SysRq-u卸载所有的文件系统,SysRq-b重启设备。
18-2
gdb
gdb vmlinux /proc/kcore
其中vmlinux文件是未经压缩的内核映像,不是压缩过的zImage或bzImage。它存放在源代码树的根目录上。
/proc/kcore作为一个参数选项,是作为core文件来用,通过它能访问在内核驻留的高端内存。只有超级用户才能 读取此文件的数据。
kgdb
kgdb可以远端主机上通过串口利用gdb的所有功能对内核进行调试。
需要两个计算机:
第一台运行带有kgdb补丁的内核
第二台通过串行线(不通过modem,直接连接机器的电缆)
一般情况下,只要保留原有的算法而把你的新算法加入到其他位置上,基本就能保证安全。
可以利用用户id(UID)作为选择条件来实现这种功能,通过这种选择条件,可以安排到底执行那个算法:
if(current->uid!=7777){
/*老算法*/
else{
/*新算法*/
}
}
除了UID为7777以外,其他所有的用户都用老算法。可以创建一个UID为7777的用户,专门用来测试新算法。
如果代码与进程无关,或者希望有一个针对所有情况都使用的机制来控制某个特性,可以通过某个接口提供对这个变量的操控,也可以直接通过调试器进行操控。
通过创建统计量并提供某种机制访问其统计结果->掌握某个特定事件的发生规律。
防止被检查语句压爆->1)重复频率限制:为了避免调试信息发生井喷,可以每隔几秒打印.
如果只使用printk(),可以用一个特殊的函数去限制printk()的调用频率:
if(error && printk_ratelimit())
{
printk(KERN_DEBUG "error=%d\n",error);
}
如果频率限制生效,那么printk_ratelimit()返回0,否则返回非0.
2)发生次数限制
static unsigned long limit=0;
if(limit <5){
limit++;
printf(KERN_ERR "blah blah blah\n");
}
调试信息输出5次封顶了。
不管是哪个示列,用到的变量都应该是静态(static),并且应该限制在函数的局部范围之内。
用二分查找出引发罪恶的变更
使用Git进行二分搜索
1)告诉git你要进行二分搜索
$git bisect start
2)git提供一个出现问题的最早内核版本
$git biesct bad <version>
2*)本版本就是罪魁祸首的时候:
$git bisect bad
3)为git提供一个最新的可正常运行的内核版本
$git bisect good v2.6.28
4)版本一切正常:
$git bisect good
4*)版本有异常
$git bisect bad
5)指定 git仅仅在与错误相关的目录列表中去二分搜索提交补丁:
$git bisect start - arch/x86