使用 Kprobes 调试内核

将 printk 插入到运行中的 Linux 内核

Comments

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_KPROBESCONFIG_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;
}

获得内核例程的地址

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

  1. 从 System.map 文件直接得到地址。
    例如,要得到 do_fork 的地址,可以在命令行执行 $grep do_fork /usr/src/linux/System.map
  2. 使用 nm 命令。
    $nm vmlinuz |grep do_fork
  3. 从 /proc/kallsyms 文件获得地址。
    $cat /proc/kallsyms |grep do_fork
  4. 使用 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 系统上的竞态条件 —— 避免了您自己重新编译和重新引导的所有 麻烦。您将发现内核调试比以往更为快速和简单。


相关主题

  • 您可以参阅本文在 developerWorks 全球站点上的 英文原文.
  • Kprobes 主页可以找到更多信息、最新消息、README 和下载。 README 详尽地描述 了 kprobes 接口。
  • Kprobes 开发自完整的 Dynamic Probes补丁。Dynamic Probes 使用 Kernel Hooks 来收集难以获得的诊断信息。
  • v.2.5.26 版本的内核已经融入了对 Kprobes 的支持;参阅 Support for kernel probesKernel Traffic,2002 年 7 月 25 日)的声明以及简短评论。 KProbes 在 2.5.73 版本中增加了 out-of-line single-stepping
  • Kprobes 利用了 BIOS 中断 SysRq 键。 它可以成为一个 Magic SysRq 键, 用来实现防御间谍软件、不完全的重新引导、显示内存信息、杀死进程等等(多得多的)功能。 它适合用于调试; 不太适合用于生产环境中的机器,它会引发安全威胁。
  • objdump 显示关于一个或多个对象文件的信息。要获得更多资料,请参阅 objdump 手册页 Linux 2.6 内核模块。
  • Captain's Universe 曾经公布过一个关于 如何为内核 2.6 编译内核模块 的文档。
  • 下载内核 Module Utilities for 2.6 的源文件;它可以取代当前新内核的 modutils。它是 Rusty Russell 的众多实用补丁 之一。
  • developerWorks Linux 专区 可以找到 更多为 Linux 开发者准备的参考资料。
  • 在 Developer Bookstore Linux 区中订购 打折出售的 Linux 书籍
  • 从 developerWorks 的 Speed-start your Linux app 专区下载可以运行于 Linux 之上的经过挑选的 developerWorks Subscription 产品免费测试版本,包括 WebSphere Studio Site Developer、WebSphere SDK for Web services、WebSphere Application Server、DB2 Universal Database Personal Developers Edition、Tivoli Access Manager 和 Lotus Domino Server。要更快速地开始上手,请参阅针对各个产品的 how-to 文章和技术支持。

评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux, Open source
ArticleID=23118
ArticleTitle=使用 Kprobes 调试内核
publish-date=09192004