kprobes_使用Kprobes进行内核调试

kprobes

Kprobes是Linux中的一种简单轻量级的机制,它允许您将断点插入正在运行的内核中。 Kprobes提供了一个接口,可闯入任何内核例程并从中断处理程序中无中断地收集信息。 使用Kprobes可以轻松收集调试信息,例如处理器寄存器和全局数据结构。 开发人员甚至可以使用Kprobes修改寄存器值和全局数据结构值。

为此,Kprobes通过在正在运行的内核中的给定地址处动态编写断点指令来插入探针。 执行所探查的指令会导致断点错误。 Kprobes挂接到断点处理程序并收集调试信息。 Kprobes甚至可以单步探查指令。

安装

要安装Kprobes的,请从Kprobes的主页上最新的补丁(请参阅相关信息中的链接)。 tar文件将按照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标志。 构建并引导到新内核。 现在,您可以通过编写简单的Kprobes模块来插入printk并动态且毫不费力地收集调试信息。

编写Kprobes模块

对于每个探针,您将需要分配结构struct kprobe kp; (有关更多信息,请参见include / linux / kprobes.h)。

清单1.定义前置,后置和故障处理程序
/* 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_module中注册您的探针:

清单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.拆卸后的叉子
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的地址中。 (要确定do_fork的地址,请运行$cat /proc/kallsyms | grep do_fork 。)

您还可以将do_fork 0x22c4 - 0x22b0 = 0x14的相对偏移量添加到kallsyms_lookup_name("do_fork");的输出中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的struct 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系统上的竞争条件-从而省去了所有重建和重新启动的麻烦。 您会发现内核调试比以往更快,更轻松。


翻译自: https://www.ibm.com/developerworks/opensource/library/l-kprobes/index.html

kprobes