Linux内核调试技术之kprobes:基本原理与使用

概述

Linux kprobes技术是一种可以跟踪内核函数执行状态的轻量级内核调试技术,利用kprobes技术可以在运行的内核中动态的插入探测点,当内核运行到该探测点后可以执行用户预定义的回调函数,以收集所需的调试状态信息而基本不影响内核原有的执行流程。

kprobes的工作机制

kprobes技术依赖硬件架构相关的支持,主要包括CPU的异常处理和单步调试机制,前者用于让程序的执行流程陷入到用户注册的回调函数中去,而后者则用于单步执行被探测点指令,目前支持kprobes技术的架构包括i386、x86_64、ppc64、ia64、sparc64、arm、ppc和mips。kprobe的工作原理和流程如图所示:
在这里插入图片描述

  1. 注册kprobe时,kprobe会复制被探测位置的指令,并用断点指令(例如i386和x86_64上的int3,ARM上的brk)替换被探测指令的第一个字节;
  2. 当CPU执行到断点指令时,会触发陷阱,CPU自动保存寄存器上下文,并通过notifier_call_chain机制将控制传递给kprobe;
  3. kprobe将kprobe结构的地址和保存的寄存器传递给注册时指定的pre_handler处理程序,并执行;
  4. 接下来,kprobe单步执行其被探测指令的副本;
  5. 指令单步执行后,kprobe执行注册的post_handler处理程序;
  6. 继续执行探测点后面的指令。

kprobes探测手段

kprobes技术包括3种探测手段分别为kprobe、jprobe和kretprobe,其中:

  • kprobe是最基本的探测方式,是实现后两种的基础,它可以在内核的任何指令位置插入探测点;
  • jprobe基于kprobe实现,只能插入到一个内核函数的入口,它用于获取被探测函数的入参值;
  • kretprobe也是基于kprobe实现,可以在指定的内核函数返回时才被执行。利用该方式可以获取被探测函数的返回值,还可以用于计算函数执行时间等方面。

kprobes接口

kprobes提供了注册与卸载kprobe探测点的API函数接口,其函数原型定义如下:

int register_kprobe(struct kprobe *kp); /* 向内核注册kprobe探测点 */
void unregister_kprobe(struct kprobe *kp);  /* 从内核卸载kprobe探测点 */

内核提供了一个struct kprobe结构体以及一系列的内核API函数接口,用户可以通过这些接口自行实现探测回调函数并实现struct kprobe结构,然后将它注册到内核的kprobes子系统中来达到探测的目的。struct kprobe结构体定义如下:

struct kprobe {
    ...
	kprobe_opcode_t *addr; /* 被探测点的地址 */
	const char *symbol_name;   /* 被探测函数的名称 */
	unsigned int offset;   /* 被探测点在函数内部的偏移,可用于探测函数内部的指令,若为0则表示函数入口 */
 
	kprobe_pre_handler_t pre_handler;      /* 该回调函数用于在执行被探测指令前执行 */
	kprobe_post_handler_t post_handler;    /* 该回调函数用于在执行完被探测指令后执行 */
	kprobe_fault_handler_t fault_handler;  /* 此函数用于在出现内存访问错误时进行处理 */
 
	kprobe_opcode_t opcode;
    
    ...
};

编写kprobe探测模块

通过编写内核模块,向内核注册探测点。探测函数可根据需要自行定制,使用灵活方便。在内核的samples/kprobes目录下有一个实例kprobe_example.c描述了kprobe模块最简单的编写方式,后续分析实际问题时,我们可以以此为模板编写自己的探测模块。

定义回调函数

static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
	pr_info("<%s> pre_handler: p->addr = 0x%p, ip = %lx, flags = 0x%lx\n",
		p->symbol_name, p->addr, regs->ip, regs->flags);

	return 0;
}

/* kprobe post_handler: called after the probed instruction is executed */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
				unsigned long flags)
{
    pr_info("<%s> post_handler: p->addr = 0x%p, flags = 0x%lx\n",
		p->symbol_name, p->addr, regs->flags);
}

int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
	pr_info("fault_handler: p->addr = 0x%p, trap #%dn", p->addr, trapnr);
	/* Return 0 because we don't handle the fault. */
	return 0;
}

定义kprobe结构

static struct kprobe kp = {
    .symbol_name   = "do_fork",      // 要追踪的内核函数为 do_fork
    .pre_handler   = handler_pre    // pre_handler 回调函数
    .post_handler  = handler_post;   // post_handler 回调函数
    .fault_handler = handler_fault;  // fault_handler 回调函数
};

注册探测点

static int __init kprobe_init(void)
{
	int ret;

	ret = register_kprobe(&kp);
	if (ret < 0) {
		pr_err("register_kprobe failed, returned %d\n", ret);
		return ret;
	}
	pr_info("Planted kprobe at %p\n", kp.addr);
	return 0;
}

static void __exit kprobe_exit(void)
{
	unregister_kprobe(&kp);
	pr_info("kprobe at %p unregistered\n", kp.addr);
}

module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");

最后,使用如下Makefile编译kprobe_example模块:

obj-m := kprobe_example.o 
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
        $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
clean:
        rm -f *.mod.c *.ko *.o

安装内核模块,任意执行一条shell命令,都会在后台看到kprobe_example模块的输出。

参考链接

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值