内核编程调试技术(2)

调试系统故障

即使采用了所有的监视和调试技术,有时驱动程序依然会有错误,这样的驱动程序在执行时就会产生故障。

注意,“故障”并不意味着“崩溃”,故障通常会导致当前进程崩溃,而系统仍会继续运行,如果在进程上下文之外发生了故障,或是系统的关键部分被损害时,系统才有可能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

如果未授权用户可以使用系统键盘,则应该考虑禁用这个功能,以避免出现意外或蓄意的破坏。

 

如果遇到“活的挂起”时,即驱动程序进入了某个死循环但系统整体还能工作,针对这种情况可有三种方法:

  1. 通常SysRq的p功能可直接指出有问题的例程所在的位置
  2. 使用内核剖析功能。构造一个打开剖析功能的内核并引导它,利用readprofile工具重置剖析计数器,然后让驱动程序进入死循环状态。经过一段时间后,再次使用readprofile即可观察到浪费CPU资源的内核位置。
  3. 另外一个更高级的方法是使用oprofile。源代码是的Documentation/basic_profiling.txt文件详细描述了剖析器相关的所有东西。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值