调试系统故障
即使采用了所有的监视和调试技术,有时驱动程序依然会有错误,这样的驱动程序在执行时就会产生故障。
注意,“故障”并不意味着“崩溃”,故障通常会导致当前进程崩溃,而系统仍会继续运行,如果在进程上下文之外发生了故障,或是系统的关键部分被损害时,系统才有可能panic
驱动程序有问题通常会导致正在使用驱动程序的那个进程突然终止。唯一不能恢复的就是在进程上下文分配的一些内存可能会丢失。
oops消息
大部分错误都是因为对NULL指针取值或因为使用了其他不正确的指针值,这些错误通常会产生一个oops消息。
使用如下代码会产生一个oops消息:
void test(void) { *(int *)0 = 0; }
BUG: unable to handle kernel NULL pointer dereference at (null)
IP: [<e0b85041>] hello_init+0x11/0x20 [helloworld]
*pdpt = 000000001fb7e001 *pde = 0000000000000000
Oops: 0002 [#1] SMP
Pid: 2165, comm: insmod Not tainted (2.6.32 #1)
EIP: 0060:[<e0b85041>] EFLAGS: 00010246 CPU: 0
EIP is at hello_init+0x11/0x20 [helloworld]
EAX: 00000000 EBX: 00000000 ECX: c099f40c EDX: 00000000
ESI: e0b850a0 EDI: bfdf70a8 EBP: e0b85030 ESP: df909f64
DS: 007b ES: 007b FS: 00d8 GS: 00e0 SS: 0068
Process insmod (pid: 2165, ti=df908000 task=dcdaf030 task.ti=df908000)
Stack:
e0b8508d c040303f 00000000 e0b850a0 fffffffc e0b850a0 bfdf70a8 fffffffc
<0> e0b850a0 bfdf70a8 df908000 c04867d3 08860018 dcdaf030 dfbba774 bfdf88cd
<0> 08860018 08860018 00000000 c04094bb 08860018 000121ee 08860008 00000000
Call Trace:
[<c040303f>] ? do_one_initcall+0x2f/0x1c0
[<c04867d3>] ? sys_init_module+0xb3/0x220
[<c04094bb>] ? sysenter_do_call+0x12/0x28
Code: 74 50 b8 e0 e8 16 71 c5 df 83 c4 04 c3 8d b6 00 00 00 00 8d bc 27 00 00 00 00 83 ec 04 c7 04 24 8d 50 b8 e0 e8 f6 70 c5 df 31 c0 <c7> 05 00 00 00 00 00 00 00 00 83 c4 04 c3 00 04 00 00 00 14 00
EIP: [<e0b85041>] hello_init+0x11/0x20 [helloworld] SS:ESP 0068:df909f64
CR2: 0000000000000000我们在这引用了一个NULL指针,因为0决不会是一个合法的指针值,所以产生了错误,内核进入上面的oops消息状态。这个调用进程接着就被杀掉了。
ssize_t faulty_read(struct file *filp, char __user *buf, size_t count, loff_t *pos) { int ret; char stack_buf[4]; /*试着产生缓冲区溢出错误*/ memset(stack_buf, 0xff, 20); if(count >4) count = 4; ret = copy_to_user(buf, stack_buf, count); if(!ret) return count; return ret; }
BUG: unable to handle kernel NULL pointer dereference at 0000000b
IP: [<c05089c7>] vfs_read+0xa7/0x190
*pdpt = 000000001fb6e001 *pde = 0000000000000000
Oops: 0000 [#2] SMP
Pid: 2580, comm: cat Tainted: G D (2.6.32 #1)
EIP: 0060:[<c05089c7>] EFLAGS: 00010202 CPU: 0
EIP is at vfs_read+0xa7/0x190
EAX: 00000004 EBX: ffffffff ECX: 00000000 EDX: dcf38000
ESI: 00000004 EDI: ffffffff EBP: ffffffff ESP: dcf39f78
DS: 007b ES: 007b FS: 00d8 GS: 00e0 SS: 0068
Process cat (pid: 2580, ti=dcf38000 task=df9e6030 task.ti=dcf38000)
Stack:
dcf39f9c df2b9800 c049f294 dfae3ac0 fffffff7 00008000 dcf38000 c0508af1
<0> dcf39f9c 00000000 00000000 00000000 00000003 00008000 c04094bb 00000003
<0> 085ce000 00008000 00008000 00008000 bf9986f8 00000003 0000007b 0000007b
Call Trace:
[<c049f294>] ? audit_syscall_entry+0x204/0x230
[<c0508af1>] ? sys_read+0x41/0x70
[<c04094bb>] ? sysenter_do_call+0x12/0x28
Code: 89 c6 78 a2 8b 43 10 8b 68 08 85 ed 0f 84 d5 00 00 00 8b 44 24 20 89 f1 89 fa 89 04 24 89 d8 ff d5 89 c6 85 f6 0f 8e 8e 00 00 00 <8b> 7b 0c bb 01 00 00 40 8b 6f 10 0f b7 45 72 c7 44 24 04 00 00
EIP: [<c05089c7>] vfs_read+0xa7/0x190 SS:ESP 0068:dcf39f78
CR2: 000000000000000b在这种情况下我们只能看到调用栈的部分信息,(无法看到faulty_read),内核栈已经被破坏。
通常,在面对一条oops时,首先要观察的是发生问题所在的位置,这通常可以通过调用栈信息得到。在上面给出的第一个oops中是:
EIP is at hello_init+0x11/0x20 [helloworld]
这里我们可以看到故障所在的函数是hello_init,该函数位于helloworld模块([列在中括号内)。十六进制数表明指令指针在该函数的11字节处,函数本身是20字节,通常这些信息足以让我们看到问题所在。
如果需要更多信息,调用栈可以告诉我们系统是如何到达故障的。栈本身以十六进制形式打印,我们可以通过栈清单确定局部变量和函数参数的值。
Stack:
dcf39f9c df2b9800 c049f294 dfae3ac0 fffffff7 00008000 dcf38000 c0508af1
dcf39f9c 00000000 00000000 00000000 00000003 00008000 c04094bb 00000003
085ce000 00008000 00008000 00008000 bf9986f8 00000003 0000007b 0000007b内核空间地址起始于0xc0000000,故在大于0xc0000000的值几乎可以肯定是内核空间的地址。
在构造内核时只有打开了CONFIG_KALLSYMS选项,才能看到符号化的调用栈,就像上面那样,否则就是十六进制清单。
系统挂起
尽管内核代码大多数错误只会导致一个oops消息,但有时它们会将系统完全挂起。如果系统挂起了,任何消息都无法打印出来。例如系统进入了一个死循环,内核就会停止调度(如果内核是可抢占的也会重新调度),系统不会再响应其它动作。
处理系统挂起有两个选择——要么防患于未然,要么亡羊补牢,在发生挂起后高度代码。
- 通过在一些关键点插入schedule调用可以防止死循环。schedule函数会调用调度器,并因此允许其它进程“偷取”当前进程的CPU时间。如果该进程因驱动程序错误而在内核空间陷入死循环,则可以在跟踪到这种情况之后借助schedule调用杀死这个进程。
- 任何对schedle的调用都可能给驱动程序带来代码重入的问题,因为schedule允许其它进程开始运行,如果程序中使用了锁定,这种重入通常不会带来问题。不过,一定不要在驱动程序持有自旋锁的任何时候调用schedule。
- 如果驱动程序确实会挂起系统,而你又不知道在什么位置插入schedule调用时,最好的方法是加入一些打印信息,并把它们写入控制台。
- 有时系统看起来像挂起了,但其实并没有。如键盘因某种奇怪的原因被锁住了就会发生这种情况,这时可以查看显示器上的时钟或系统负荷,如果这个些程序保持更新,就说明调度器仍在工作。
对于上述情形,可以使用SysRq魔法键(ALT+SysRq+第三个键),内核会执行许多有用的动作,如下所示:
r 关闭键盘的raw模式。当某个崩溃的应用程序(比如X服务器)让键盘处于一种奇怪的状态时,就可以使用这个键关闭raw模式。
k 激活“留意安全键(secure attention key, SAK)时”功能。SAK将杀死当前控制台上运行的所有进程,留下一个干净的终端。
s 对所有磁盘进行紧急同步。
u 尝试以只读模式重新挂装所有磁盘。这个操作通常紧接着s动作之后立即被调用,它可以在系统处于严重故障状态时节省很多检查文件系统的时间。
b 立即重启系统。注意先要执行同步并重新挂装磁盘。
p 打印当前的处理器寄存器信息。
t 打印当前的任务列表
m 打印内存信息。
其它一些SysRq功能的信息可参阅内核源代码Documentation目录下的sysrq.txt文件。
SysRq功能必须显示地在内核配置中启用,出于安全原因,大多数发行版并未启用这一功能。在系统运行时可以使用下面语句来开户该功能:
echo 1 > /proc/sys/kernel/sysrq
如果未授权用户可以使用系统键盘,则应该考虑禁用这个功能,以避免出现意外或蓄意的破坏。
如果遇到“活的挂起”时,即驱动程序进入了某个死循环但系统整体还能工作,针对这种情况可有三种方法:
通常SysRq的p功能可直接指出有问题的例程所在的位置 使用内核剖析功能。构造一个打开剖析功能的内核并引导它,利用readprofile工具重置剖析计数器,然后让驱动程序进入死循环状态。经过一段时间后,再次使用readprofile即可观察到浪费CPU资源的内核位置。 另外一个更高级的方法是使用oprofile。源代码是的Documentation/basic_profiling.txt文件详细描述了剖析器相关的所有东西。