使用 Kprobes 调试内核(转贴)

2004 年 9 月

使用 printk 收集 Linux™ 内核的调试信息是一个众所周知的方法 —— 而使用了 Kprobes,不需要经常重新 引导和重新编译内核就可以完成这一任务。Kprobes 与 2.6 内核结合起来提供了一个动态插入 printk's 的轻量级、 无干扰而且强大的装置。记录调试信息(比如内核栈追踪、内核数据结构和寄存器)日志从来没有这么简单过!


Kprobes 是 Linux 中的一个简单的轻量级装置,让您可以将断点插入到正在运行的内核之中。 Kprobes 提供了一个强行进入任何内核例程并从中断处理器无干扰地收集信息的接口。使用 Kprobes 可以 轻松地收集处理器寄存器和全局数据结构等调试信息。开发者甚至可以使用 Kprobes 来修改 寄存器值和全局数据结构的值。


为完成这一任务,Kprobes 向运行的内核中给定地址写入断点指令,插入一个探测器。 执行被探测的指令会导致断点错误。Kprobes 钩住(hook in)断点处理器并收集调试信息。Kprobes 甚至可以单步执行被探测的指令。


安装
要安装 Kprobes,需要从 Kprobes 主页下载最新的补丁(参阅 参考资料 中的链接)。 打包的文件名称类似于 kprobes-2.6.8-rc1.tar.gz。解开补丁并将其安装到 Linux 内核:


$tar -xvzf kprobes-2.6.8-rc1.tar.gz
$cd /usr/src/linux-2.6.8-rc1
$patch -p1 < ../kprobes-2.6.8-rc1-base.patch


Kprobes 利用了 SysRq 键,这个 DOS 时代的产物在 Linux 中有了新的用武之地(参阅 参考资料)。您可以在 Scroll Lock 键左边找到 SysRq 键;它通常标识为 Print Screen。要为 Kprobes 启用 SysRq 键,需要安装 kprobes-2.6.8-rc1-sysrq.patch 补丁:


$patch -p1 < ../kprobes-2.6.8-rc1-sysrq.patch


使用 make xconfig/ make menuconfig/ make oldconfig 配置内核,并 启用 CONFIG_KPROBES 和 CONFIG_MAGIC_SYSRQ 标记。 编译并引导到新内核。您现在就已经准备就绪,可以插入 printk 并通过编写简单的 Kprobes 模块来动态而且无干扰地 收集调试信息。


编写 Kprobes 模块
对于每一个探测器,您都要分配一个结构体 struct kprobe kp; (参考 include/linux/kprobes.h 以获得关于此数据结构的详细信息)。

清单 1. 定义 pre、post 和 fault 处理器

/* pre_handler: this is called just before the probed instruction is
  *     executed.
  */

int handler_pre(struct kprobe *p, struct pt_regs *regs) {
        printk("pre_handler: p->addr=0x%p, eflags=0x%lx\n",p->addr,
                regs->eflags);
        return 0;
}

/* post_handler: this is called after the probed instruction is executed
  *     (provided no exception is generated).
  */

void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
        printk("post_handler: p->addr=0x%p, eflags=0x%lx \n", p->addr,
                regs->eflags);
}

/* fault_handler: this is called if an exception is generated for any
  *     instruction within the fault-handler, or when Kprobes
  *     single-steps the probed instruction.
  */

int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr) {
        printk("fault_handler:p->addr=0x%p, eflags=0x%lx\n", p->addr,
                regs->eflags);
        return 0;
}




获得内核例程的地址
在注册过程中,您还需要指定插入探测器的内核例程的地址。使用这些方法中的任意一个来获得内核例程 的地址:

从 System.map 文件直接得到地址。
例如,要得到 do_fork 的地址,可以在命令行执行 $grep do_fork /usr/src/linux/System.map。

使用 nm 命令。
$nm vmlinuz |grep do_fork

从 /proc/kallsyms 文件获得地址。
$cat /proc/kallsyms |grep do_fork

使用 kallsyms_lookup_name() 例程。
这个例程是在 kernel/kallsyms.c 文件中定义的,要使用它,必须启用 CONFIG_KALLSYMS 编译内核。 kallsyms_lookup_name() 接受一个字符串格式内核例程名, 返回那个内核例程的地址。例如:kallsyms_lookup_name("do_fork");


然后在 init_moudle 中注册您的探测器:

清单 2. 注册一个探测器

/* specify pre_handler address
  */
        kp.pre_handler=handler_pre;
/* specify post_handler address
  */
        kp.post_handler=handler_post;
/* specify fault_handler address
  */
        kp.fault_handler=handler_fault;
/* specify the address/offset where you want to insert probe.
  * You can get the address using one of the methods described above.
  */
        kp.addr = (kprobe_opcode_t *) kallsyms_lookup_name("do_fork");

/* check if the kallsyms_lookup_name() returned the correct value.
  */
        if (kp.add == NULL) {
                printk("kallsyms_lookup_name could not find address
                                        for the specified symbol name\n");
                return 1;
        }

/*     or specify address directly.
  * $grep "do_fork" /usr/src/linux/System.map
  * or
  * $cat /proc/kallsyms |grep do_fork
  * or
  * $nm vmlinuz |grep do_fork
  */
        kp.addr = (kprobe_opcode_t *) 0xc01441d0;

/* All set to register with Kprobes
  */
        register_kprobe(&kp);




一旦注册了探测器,运行任何 shell 命令都会导致一个对 do_fork 的调用,您将可以在控制台上或者运行 dmesg 命令来查看您的 printk。做完后要记得注销探测器:


unregister_kprobe(&kp);


下面的输出显示了 kprobe 的地址以及 eflags 寄存器的内容:


$tail -5 /var/log/messages

Jun 14 18:21:18 llm05 kernel: pre_handler: p->addr=0xc01441d0, eflags=0x202
Jun 14 18:21:18 llm05 kernel: post_handler: p->addr=0xc01441d0, eflags=0x196


获得偏移量
您可以在例程的开头或者函数中的任意偏移位置插入 printk(偏移量必须在指令范围之内)。 下面的代码示例展示了如何来计算偏移量。首先,从对象文件中反汇编机器指令,并将它们 保存为一个文件:


$objdump -D /usr/src/linux/kernel/fork.o > fork.dis


其结果是:

清单 3. 反汇编的 fork

000022b0 <do_fork>:
    22b0:       55                      push   %ebp
    22b1:       89 e5                   mov    %esp,%ebp
    22b3:       57                      push   %edi
    22b4:       89 c7                   mov    %eax,%edi
    22b6:       56                      push   %esi
    22b7:       89 d6                   mov    %edx,%esi
    22b9:       53                      push   %ebx
    22ba:       83 ec 38                sub    $0x38,%esp
    22bd:       c7 45 d0 00 00 00 00    movl   $0x0,0xffffffd0(%ebp)
    22c4:       89 cb                   mov    %ecx,%ebx
    22c6:       89 44 24 04             mov    %eax,0x4(%esp)
    22ca:       c7 04 24 0a 00 00 00    movl   $0xa,(%esp)
    22d1:       e8 fc ff ff ff          call   22d2 <do_fork+0x22>
    22d6:       b8 00 e0 ff ff          mov    $0xffffe000,%eax
    22db:       21 e0                   and    %esp,%eax
    22dd:       8b 00                   mov    (%eax),%eax




要在偏移位置 0x22c4 插入探测器,先要得到与例程的开始处相对的偏移量 0x22c4 - 0x22b0 = 0x14,然后将这个偏移量添加到 do_fork 的地址 0xc01441d0 + 0x14。(运行 $cat /proc/kallsyms | grep do_fork 命令以获得 do_fork 的地址。)


您还可以将 do_fork 的相对偏移量 0x22c4 - 0x22b0 = 0x14 添加到 kallsyms_lookup_name("do_fork"); 的输入,即: 0x14 + kallsyms_lookup_name("do_fork");



转储内核数据结构
现在,让我们使用修改过的用来转储数据结构的 Kprobe post_handler 来转储运行在系统上的所有作业的一些组成部分:

清单 4. 用来转储数据结构的修改过的 Kprope post_handler

void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags) {
        struct task_struct *task;
        read_lock(&tasklist_lock);
        for_each_process(task) {
                printk("pid =%x task-info_ptr=%lx\n", task->pid,
                        task->thread_info);
                printk("thread-info element status=%lx,flags=%lx, cpu=%lx\n",
                        task->thread_info->status, task->thread_info->flags,
                        task->thread_info->cpu);
        }
        read_unlock(&tasklist_lock);
}




这个模块应该插入到 do_fork 的偏移位置。

清单 5. pid 1508 和 1509 的结构体 thread_info 的输出

$tail -10 /var/log/messages

Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=0, cpu=1
Jun 22 18:14:25 llm05 kernel: pid =5e4 task-info_ptr=f5948000
Jun 22 18:14:25 llm05 kernel: thread-info element status=0,flags=8, cpu=0
Jun 22 18:14:25 llm05 kernel: pid =5e5 task-info_ptr=f5eca000




启用奇妙的 SysRq 键
为了支持 SysRq 键,我们已经进行了编译。这样来启用它:


$echo 1 > /proc/sys/kernel/sysrq


现在,您可以使用 Alt+SysRq+W 在控制台上或者到 /var/log/messages 中去查看所有插入的内核探测器。

清单 6. /var/log/messages 显示出在 do_fork 插入了一个 Kprobe

Jun 23 10:24:48 linux-udp4749545uds kernel: SysRq : Show kprobes
Jun 23 10:24:48 linux-udp4749545uds kernel:
Jun 23 10:24:48 linux-udp4749545uds kernel: [<c011ea60>] do_fork+0x0/0x1de




使用 Kprobes 更好地进行调试
由于探测器事件处理器是作为系统断点中断处理器的扩展来运行,所以它们很少或者根本不依赖于系统 工具 —— 这样可以被植入到大部分不友好的环境中(从中断时间和任务时间到禁用的上下文间切换和支持 SMP 的代码路径)—— 都不会对系统性能带来负面影响。


使用 Kprobes 的好处有很多。不需要重新编译和重新引导内核就可以插入 printk。为了进行调试可以记录 处理器寄存器的日志,甚至进行修改 —— 不会干扰系统。类似地,同样可以无干扰地记录 Linux 内核数据结构的日志,甚至 进行修改。您甚至可以使用 Kprobes 调试 SMP 系统上的竞态条件 —— 避免了您自己重新编译和重新引导的所有 麻烦。您将发现内核调试比以往更为快速和简单。