kprobe 原理详细分析

===============================》内核新视界文章汇总《===============================

文章目录

    • kprobe实现原理分析
      • 1 简介
      • 2 一个简单的例子
      • 3 原理分析
        • 3.1 struct kprobe
        • 3.2 register_kprobe
        • 3.3 arm_kprobe
        • 3.4 kprobe 的执行
          • 3.4.1 single_step_handler
      • 4 总结

kprobe实现原理分析

1 简介

linux内核提供了许多调试内核的方式,比如 ftrace,tracepoint,kprobe/uprobe 等可以跟踪内核函数的执行流程,获取内核执行信息,这里主要详细分析 kprobe的实现机制。(还有一个 uprobe,机制和 kprobe 相同,只是探测的是用户态程序,这里不介绍)

kprobe 是内核的动态探测工具,它提供了一种调试机制,能够在不修改现有代码的基础上,灵活的跟踪内核函数的执行,并且它几乎可以探测任何一条内核指令。它的基本工作原理是:用户指定一个探测点(可以是内核函数,也可以是内核函数加偏移,或者一个内核地址),并且把一个用户定义的处理函数关联到该探测点,该探测点指定的地址将被替换为对应架构的 trap 指令(x86 的 int3,arm64 的 brk),当内核执行到该地址时,触发异常,在架构的异常处理中可以检测到探测点,并且探测点关联的函数将被执行,接着继续正常执行代码路径。

2 一个简单的例子

#include 
#include 
#include 

#define MAX_SYMBOL_LEN	64
static char symbol[MAX_SYMBOL_LEN] = "_do_fork";
module_param_string(symbol, symbol, sizeof(symbol), 0644);

/* For each probe you need to allocate a kprobe structure */
static struct kprobe kp = {
	.symbol_name	= symbol,
};

/* kprobe pre_handler: called just before the probed instruction is executed */
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_ARM64
	pr_info("<%s> pre_handler: p->addr = 0x%p, pc = 0x%lx,"
			" pstate = 0x%lx\n",
		p->symbol_name, p->addr, (long)regs->pc, (long)regs->pstate);
#endif
	/* A dump_stack() here will give a stack backtrace */
	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)
{
#ifdef CONFIG_ARM64
	pr_info("<%s> post_handler: p->addr = 0x%p, pstate = 0x%lx\n",
		p->symbol_name, p->addr, (long)regs->pstate);
#endif
}

/*
 * fault_handler: this is called if an exception is generated for any
 * instruction within the pre- or post-handler, or when Kprobes
 * single-steps the probed instruction.
 */
static 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;
}

static int __init kprobe_init(void)
{
	int ret;
	kp.pre_handler = handler_pre;
	kp.post_handler = handler_post;
	kp.fault_handler = handler_fault;

	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");

上述代码中通过 register_kprobe 注册了一个探测点,该探测点指定了探测的内核函数是 _do_fork,注册成功后,一旦执行到 _do_fork 内核函数,相对应的回调则会被调用。

  • pre_handler
    当执行 _do_fork 之前被调用,当回调返回 0 时,则会单步执行原理指令,并且单步执行后调用 post_handler。
  • post_handler
    当对应指令执行后,会调用注册的 post_handler。
  • fault_handler
    当在 pre_handler 到 post_handler 之间发生 pagefault,则会调用 fault_handler。

通过这个例子可以看到 kprobe 通过修改指令方式几乎是可以探测任意的内核地址(有一些地址范围不允许探测,__kprobe 段,预留段等等。。。),从而获取内核信息。

3 原理分析

首先内核提供了一些 api 来注册移除 hook 点。
register_kprobe,unregister_kprobe,register_kprobes,unregister_kprobes
register_kretprobe,unregister_kretprobe。。。

通过 register_kprobe 可以注册一个 hook点,register_kprobes 可以一次性注册多个 hook 点。

上述可以在对应内核地址注册 hook 点来执行 kprobe,同样的这里基于 kprobe 的能力,还提供了 register_kretprobe。。。在函数返回处注册 hook,意思是对对应注册的 hook 点,检测返回地址,并在函数返回时调用注册的 handler,我们常见到的注册方式即是内核函数返回点。

3.1 struct kprobe

看一下注册的 kprobe 结构体的数据结构:

struct kprobe {
    struct hlist_node hlist; // 注册到系统的全局链表节点。
    
    struct list_head list; // 一个 hook 点可以注册多个 kprobe,这个链表用于将同一个 hook 点串联起来。
    unsigned long nmissed; // 因断点指令不能重入处理, 当多个 kprobe 一起触发时会放弃执行后面的probe, 同时该计数增加。
    
    kprobe_opcode_t *addr; // 探测点对应的地址, 用户在调用注册接口时可以指定地址, 也可以传入函数名让内核自己查找。
    
    const char *symbol_name; // 探测点对应的函数名, 在注册 kprobe 时会将其翻译为十六进制地址并修改addr。
    unsigned int offset; // 相对于入口点地址的偏移, 会在计算 addr 以后再加上 offset 得到最终的 addr。
	kprobe_pre_handler_t pre_handler; // 在执行探测地址之前调用的 handler

	kprobe_post_handler_t post_handler; // 在探测地址执行后调用的 handler

	kprobe_fault_handler_t fault_handler; // 在 kprobe 运行期间触发异常则会调用该 handler

	kprobe_opcode_t opcode; // 探测地址对应的原始指令

	struct arch_specific_insn ainsn; // 架构相关结构,用于保存架构对应使用的数据。

	u32 flags; // 当前 kprobe 的状态记录。
    // 有如下状态
#define KPROBE_FLAG_GONE	1	// kprobe 已经取消,内部特殊状态
#define KPROBE_FLAG_DISABLED	2 // kprobe 暂时性的禁用
#define KPROBE_FLAG_OPTIMIZED	4 // 可以优化 kprobe,架构相关,arm64 没有优化
#define KPROBE_FLAG_FTRACE	8 // kprobe 可以被优化到 ftrace 上处理,目前只有 x86 会使用该状态。
}

// arm64 数据结构
struct arch_specific_insn {
	struct arch_probe_insn api;
};

struct arch_probe_insn {
	probe_opcode_t *insn; // 当探测地址的指令不是需要仿真的指令时,会分配一个指令槽,用于存放原始指令,以便于单步执行。
	pstate_check_t *pstate_cc; // 对应 pstate 状态,会在合适实际修改,恢复。
	probes_handler_t *handler; // 当对应指令是一个模拟指令时,对应的指令类型会有一个 handler 用于执行模拟指令运行。
	/* restore address after step xol */
	unsigned long restore; // 当非模拟指令时,会保存下一个执行的 pc 地址,为模拟指令时该值等于 0。
};

3.2 register_kprobe

注册一个探测点函数的重要部分:

int register_kprobe(struct kprobe *p)
{
...
    /* Adjust probe address from symbol */
	addr = kprobe_addr(p); ----------------------------------1if (IS_ERR(addr))
		return PTR_ERR(addr);
	p->addr = addr;
...
    ret = check_kprobe_rereg(p); ----------------------------2...
	p->flags &= KPROBE_FLAG_DISABLED; -----------------------3)
	p->nmissed = 0; -----------------------------------------3...
    ret = check_kprobe_address_safe(p, &probed_mod); --------4...
    // 从 hashtable 中获取该 hook 点原始的 kprobe,我们会将新的 kprobe 挂入原始 kp 的 list 中,后续有头 kp 调用。
	old_p = get_kprobe(p->addr);
	if (old_p) {
		/* Since this may unoptimize old_p, locking text_mutex. */
		ret = register_aggr_kprobe(old_p, p);
		goto out;
	}
...
	cpus_read_lock();
	/* Prevent text modification */
	mutex_lock(&text_mutex);
	ret = prepare_kprobe(p); --------------------------------5mutex_unlock(&text_mutex);
	cpus_read_unlock();
...
    // 将设置好的 kprobe 加入到全局 hashtable 中
	hlist_add_head_rcu(&p->hlist,
		       &kprobe_table[hash_ptr(p->addr, KPROBE_HASH_BITS)]);
...
	if (!kprobes_all_disarmed && !kprobe_disabled(p)) {
        // 当没有禁用 kprobe 时,调用 arm_kprobe 激活该 kprobe。
		ret = arm_kprobe(p);
...
    // 对于可以对 kprobe 的架构执行优化,arm64 为空
	try_to_optimize_kprobe(p);
...    
}

(1)首先会通过传入的 kprobe 结构获取到对应探测点的内核地址

kprobe_addr
    -> _kprobe_addr(p->addr, p->symbol_name, p->offset);

static kprobe_opcode_t *_kprobe_addr(kprobe_opcode_t *addr,
			const char *symbol_name, unsigned int offset)
{
    // 在注册 kprobe 中不能同时指定 addr 和 symbol_name
	if ((symbol_name && addr) || (!symbol_name && !addr))
		goto invalid;

    // 如果 name 存在,则通过 kall_symbol 模块获取到内核符号对应的内核地址
	if (symbol_name) {
		addr = kprobe_lookup_name(symbol_name, offset);
		if (!addr)
			return ERR_PTR(-ENOENT);
	}

    // 根据偏移返回实际我们要探测的内核地址,可能是内核函数的地址也可能时函数内部某个指令的地址。
	addr = (kprobe_opcode_t *)(((char *)addr) + offset);
	if (addr)
		return addr;
...
...
}

(2)检测该 kprobe 是否是重复注册的,是重复注册会返回错误。

check_kprobe_rereg
    -> __get_valid_kprobe // 如果返回有 kprobe 则当前 kprobe 被注册了,返回错误。

static struct kprobe *__get_valid_kprobe(struct kprobe *p)
{
	struct kprobe *ap, *list_p;

	// 遍历全局 hashtable 查看对应地址是否已经有 kprobe。
	ap = get_kprobe(p->addr);
	if (unlikely(!ap)) // 如果为空则没有注册过。
		return NULL;

    // 如果现在注册的 kprobe 与原有的 kprobe 不相等,那么遍历原有 kprobe list 链表,看看是否是串联链表中的 kprobe。
	if (p != ap) {
		list_for_each_entry_rcu(list_p, &ap->list, list)
			if (list_p == p)
			/* kprobe p is a valid probe */
				goto valid;
		return NULL;
	}
valid:
	return ap;
}

struct kprobe *get_kprobe(void *addr)
{
	struct hlist_head *head;
	struct kprobe *p;

    // 从全局 hashtable 得到对应数组的 hlist_head,再去遍历 hlist 获取 kprobe。
	head = &kprobe_table[hash_ptr(addr, KPROBE_HASH_BITS)];
	hlist_for_each_entry_rcu(p, head, hlist) {
		if (p->addr == addr) // hashtable 中有对应的 kprobe
			return p;
	}

	return NULL;
}

(3)用户指令标记 kprobe 的状态为禁用状态,其他状态不能设置。设置 nmissed 为 0,没有错过处理该 kprobe。
(4)检查探测地址是否是一个合法的地址

check_kprobe_address_safe
    -> arch_check_ftrace_location
    -> !kernel_text_address || within_kprobe_blacklist || jump_label_text_reserved
    -> __module_text_address

arch_check_ftrace_location 检测地址是否也是一个 ftrace 的地址,如果是,并且对应架构支持 CONFIG_KPROBES_ON_FTRACE kprobe 附加在 ftrace 上则会为该 kprobe 添加 KPROBE_FLAG_FTRACE 标记。

目前 arm64 不支持附加在 ftrace 上,需要走原始路径。

接着检测是否是内核的文本段,只有文本段可以设置 kprobe,内核自定义的"黑名单"不能设置kprobe。
如果 arm64 中的 异常处理程序段,idmap段,hyp 文本段,search_execption 段,__kprobe 标记的内核函数,NOKPROBE_SYMBOL 申明的导出函数这些都属于 kprobe 黑名单,不能被设置为探测点。
(5)配置这个 kprobe

prepare_kprobe
	-> arch_prepare_kprobe

int __kprobes arch_prepare_kprobe(struct kprobe *p)
{
    unsigned long probe_addr = (unsigned long)p->addr;
...
    // 检测探测点地址对齐
	if (probe_addr & 0x3)
		return -EINVAL;

	// 获取到探测点对应指令
	p->opcode = le32_to_cpu(*p->addr);

...

	// 解码该指令
    // 返回 INSN_REJECTED 指令不支持 kprobe
    // INSN_GOOD_NO_SLOT 是一条模拟指令
    // INSN_GOOD 是一条正常指令
	switch (arm_kprobe_decode_insn(p->addr, &p->ainsn)) {
	case INSN_REJECTED:	/* insn not supported */
		return -EINVAL;

	case INSN_GOOD_NO_SLOT:	/* insn need simulation */
		p->ainsn.api.insn = NULL;
		break;

	case INSN_GOOD:	/* instruction uses slot */
		p->ainsn.api.insn = get_insn_slot();
		if (!p->ainsn.api.insn)
			return -ENOMEM;
		break;
	}

	/* prepare the instruction */
	if (p->ainsn.api.insn)
		arch_prepare_ss_slot(p); -----------1else
        // 对于模拟指令,直接设置 p->ainsn.api.restore = 0,表示不需要从这里得到恢复地址。
		arch_prepare_simulate(p);

	return 0;
}

// 调用
arm_kprobe_decode_insn
    -> arm_probe_decode_insn // 解码指令
		-> aarch64_insn_is_steppable // 检测指令是否可以单步下,可以单步返回 INSN_GOOD
		// 首先分支指令不能单步,因为他可能修改 pc,该 pc 与 xol 地址相关
    	// 异常指令,smc,hvc,eret 不可以单步。
    	// msr 指令可以使单步下有无法处理的情况出现,修改系统行为寄存器?。
    	// hit 指令相关,wfe,wfi,yield,sev,sevl 等也不能单步。
    	// pc 相对的 ldr/str* 和独占指令也会有问题,也不允许单步。
		-> aarch64_insn_is_bcond
    	-> aarch64_insn_is_cbz
    	...
    	...
    	-> aarch64_insn_is_ldr_lit
    	...
    	这些属于模拟指令,会绑定各自对应的 api->handler,用于模拟执行指令,那么这些指令就是不需要分配一个 slot 来 cpu 执行的。标记为 INSN_GOOD_NO_SLOT。
    	// 剩下的情况则是不支持的指令,返回 INSN_REJECTED

对应 INSN_GOOD 指令,我们需要调用 get_insn_slot 来为该指令分配一个 slot 槽,后续单步执行会跳到该 slot 执行指令。而 INSN_GOOD_NO_SLOT 指令,在单步使将会调用上述绑定的 api->handler 来模拟执行该指令,所以不需要系统分配 slot。get_insn_slot 会预分配一些可执行页面,从页面中分配一个 slot 并返回地址给调用。该 slot 是可执行的,用于执行替换下来的指令。

1)当分配 slot 时,使用 arch_prepare_ss_slot 向 slot 槽中写入需要指令的指令。

static void __kprobes arch_prepare_ss_slot(struct kprobe *p)
{
	/* prepare insn slot */
    // 向 slot 槽写入原始指令
	patch_text(p->ainsn.api.insn, p->opcode);

    // 刷新对应地址的 icache。
	flush_icache_range((uintptr_t) (p->ainsn.api.insn),
			   (uintptr_t) (p->ainsn.api.insn) +
			   MAX_INSN_SIZE * sizeof(kprobe_opcode_t));

    // 设置吓一跳执行的指令,单步后使用。
	/*
	 * Needs restoring of return address after stepping xol.
	 */
	p->ainsn.api.restore = (unsigned long) p->addr +
	  sizeof(kprobe_opcode_t);
}

3.3 arm_kprobe

kprobe 实现机制的重要函数时 arm_kprobe ,它会调用对应架构的 __arm_kprobe,来实际替换原始。

arm_kprobe 
	-> __arm_kprobe
		-> arch_arm_kprobe
			// 在 arm64 中,调用 patch_text 将 addr 对应的指令替换为了 BRK64_OPCODE_KPROBES 指令,该指令是一个 brk debug 指令,当触发时,系统会进入 debug 异常处理流程,从而调用 kprobe 相关处理函数。
			-> patch_text(p->addr, BRK64_OPCODE_KPROBES);
		-> optimize_kprobe

patch_text
	-> aarch64_insn_patch_text
    	-> stop_machine_cpuslocked
            -> aarch64_insn_patch_text_cb

static int __kprobes aarch64_insn_patch_text_cb(void *arg)
{
	int i, ret = 0;
	struct aarch64_insn_patch *pp = arg;

    // 通过 stop_machine 机制使所有 cpu 进入停机状态,接着只有第一进入的 cpu 去调用 aarch64_insn_patch_text_nosync 来修改 insn,其他 cpu 在 else 中等待完成修改。
	/* The first CPU becomes master */
	if (atomic_inc_return(&pp->cpu_count) == 1) {
		for (i = 0; ret == 0 && i < pp->insn_cnt; i++)
			ret = aarch64_insn_patch_text_nosync(pp->text_addrs[i],
							     pp->new_insns[i]);
		/* Notify other processors with an additional increment. */
		atomic_inc(&pp->cpu_count);
	} else {
		while (atomic_read(&pp->cpu_count) <= num_online_cpus())
			cpu_relax();
		isb();
	}

	return ret;
}

aarch64_insn_patch_text_nosync
    -> aarch64_insn_write
    	-> __aarch64_insn_write
    		// 通过 fixmap 机制,把地址重新映射出来,接着通过 probe_kernel_write 机制把新指令写入探测点。
    		-> waddr = patch_map(addr, FIX_TEXT_POKE0);
			-> ret = probe_kernel_write(waddr, &insn, AARCH64_INSN_SIZE);
    -> __flush_icache_range

至此,一个 kprobe 的注册就完成了,后续只要执行到探测点的 brk,则会进入 debug 模式来调用我们的回调函数。

3.4 kprobe 的执行

首先看一下 arm64 的注册函数。

static struct fault_info __refdata debug_fault_info[] = {
	{ do_bad,	SIGTRAP,	TRAP_HWBKPT,	"hardware breakpoint"	},
	{ do_bad,	SIGTRAP,	TRAP_HWBKPT,	"hardware single-step"	},
	{ do_bad,	SIGTRAP,	TRAP_HWBKPT,	"hardware watchpoint"	},
	{ do_bad,	SIGKILL,	SI_KERNEL,	"unknown 3"		},
	{ do_bad,	SIGTRAP,	TRAP_BRKPT,	"aarch32 BKPT"		},
	{ do_bad,	SIGKILL,	SI_KERNEL,	"aarch32 vector catch"	},
	{ early_brk64,	SIGTRAP,	TRAP_BRKPT,	"aarch64 BRK"		},
	{ do_bad,	SIGKILL,	SI_KERNEL,	"unknown 7"		},
};

void __init hook_debug_fault_code(int nr,
				  int (*fn)(unsigned long, unsigned int, struct pt_regs *),
				  int sig, int code, const char *name)
{
	BUG_ON(nr < 0 || nr >= ARRAY_SIZE(debug_fault_info));

	debug_fault_info[nr].fn		= fn;
	debug_fault_info[nr].sig	= sig;
	debug_fault_info[nr].code	= code;
	debug_fault_info[nr].name	= name;
}

在对应的 debug 相关异常中定义了一个数组,指示对应 debug 异常的类型,其中我们可以处理的是:

hardware breakpoint -> DBG_ESR_EVT_HWBP
hardware single-step -> DBG_ESR_EVT_HWSS
hardware watchpoint -> DBG_ESR_EVT_HWWP
aarch64 BRK -> DBG_ESR_EVT_BRK

通过 hook_debug_fault_code 即可像对应 debug 注册处理函数。对于 kprobe 主要涉及两个 debug 异常,第一个是 aarch64 BRK 异常,当系统第一册触发探测点的 brk 时,进入该异常,并在一系列验证后调用我们注册 pre_handler 表示在执行指令之前调用,第二个 hardware single-step 异常,当从 brk 返回后系统处于单步模式,执行一条指令后会进入该 debug 模式调用注册的 post_handler 回调,表示在指令执行后调用。注册代码如下:

static int __init debug_traps_init(void)
{
	hook_debug_fault_code(DBG_ESR_EVT_HWSS, single_step_handler, SIGTRAP,
			      TRAP_TRACE, "single-step handler");
	hook_debug_fault_code(DBG_ESR_EVT_BRK, brk_handler, SIGTRAP,
			      TRAP_BRKPT, "ptrace BRK handler");
	return 0;
}
arch_initcall(debug_traps_init);

第一次 通过 brk 指令进入 pre_handler 阶段:

entry
    -> do_debug_exception
		-> brk_handler
    		// 匹配到是我们手动修改替换的 BRK64_ESR_KPROBES(0x0004)指令,则调用 kprobe 处理。
    		-> if ((esr & BRK64_ESR_MASK) == BRK64_ESR_KPROBES)
                -> kprobe_breakpoint_handler
                	-> kprobe_handler

static void __kprobes kprobe_handler(struct pt_regs *regs)
{
	struct kprobe *p, *cur_kprobe;
	struct kprobe_ctlblk *kcb;
	unsigned long addr = instruction_pointer(regs);

    // kcb 是一个 per_cpu 变量,用于记录执行 kprobe 期间的一些信息,比如执行状态,对应的 irqflags,单步的上下文信息等。
	kcb = get_kprobe_ctlblk();
    // kprobe 的执行可能是会重入的,所以,同上该变量也是一个 per_cpu 变量,记录当前正在运行 kprobe。 
	cur_kprobe = kprobe_running();

	p = get_kprobe((kprobe_opcode_t *) addr);

	if (p) {
        // cur_kprobe 存在,说明 kprobe 正在运行,我们重入了,进入重入处理。
		if (cur_kprobe) {
			if (reenter_kprobe(p, regs, kcb))-----------------------------3return;
		} else {
            // 首次命中 kprobe,设置 cur_kprobe 为当前 kprobe,修改 kcb 中状态为 KPROBE_HIT_ACTIVE。
			/* Probe hit */
			set_current_kprobe(p);
			kcb->kprobe_status = KPROBE_HIT_ACTIVE;

            // 判断当前 kprobe 是否有 pre_handler,如果有则执行 pre_handler 如果返回不为 0 则调用 reset_current_kprobe 清除 cur_kprobe,该 kprobe 执行完毕。
            // 如果 pre_handler 不存在,或者 pre_handler 执行返回为 0,说明 post_handler 需要被执行,我们设置单步执行。
			if (!p->pre_handler || !p->pre_handler(p, regs)) { ------------1setup_singlestep(p, regs, kcb, 0);-------------------------2} else
				reset_current_kprobe();
		}
	}
}

(1)对于该探测点如果只有一个kprobe 那么 pre_handler 对应的就是kprobe的pre_handler,如果该探测点有多个 kprobe,那么对应的 pre_handler 为 aggr_pre_handler,该 pre_handler 在注册kprobe中设置。

static int aggr_pre_handler(struct kprobe *p, struct pt_regs *regs)
{
	struct kprobe *kp;

    // 遍历首个 kprobe list 链表,一次调用链表中 kprobe 的 pre_handler。
	list_for_each_entry_rcu(kp, &p->list, list) {
		if (kp->pre_handler && likely(!kprobe_disabled(kp))) {
			set_kprobe_instance(kp);
			if (kp->pre_handler(kp, regs))
				return 1;
		}
		reset_kprobe_instance();
	}
	return 0;
}

(2)当 pre_handler 不存在或者 pre_handler 返回 0时,说明我们需要调用 post_handler,所以需要配置单步执行。

为什么需要单步执行?

首先我们把原来指令替换为了 brk,此时调用该地址时实则调用了 pre_handler,下一步我们需要调用原始指令,并且需要原始指令执行完成后接着调用 post_handler,为了实现这个机制,只有单步这个模式才能做到执行一条指令后进入 debug 模式,也是最合适的实现机制。

setup_singlestep 代码如下:

static void __kprobes setup_singlestep(struct kprobe *p,
				       struct pt_regs *regs,
				       struct kprobe_ctlblk *kcb, int reenter)
{
	unsigned long slot;

	if (reenter) { ------------------------------------------5save_previous_kprobe(kcb);
		set_current_kprobe(p);
		kcb->kprobe_status = KPROBE_REENTER;
	} else {
		kcb->kprobe_status = KPROBE_HIT_SS; ------------------1}


	if (p->ainsn.api.insn) { ---------------------------------2/* prepare for single stepping */
		slot = (unsigned long)p->ainsn.api.insn;

		set_ss_context(kcb, slot);	/* mark pending ss */

		spsr_set_debug_flag(regs, 0);

		/* IRQs and single stepping do not mix well. */
		kprobes_save_local_irqflag(kcb, regs);
		kernel_enable_single_step(regs);
		instruction_pointer_set(regs, slot); -----------------3} else {
		/* insn simulation */
		arch_simulate_insn(p, regs); -------------------------4}
}

1)当首次进入 kprobe时,设置kprobe状态为 KPROBE_HIT_SS,标记kprobe开始单步执行

2)当api->insn 存在时,说明我们分配了 slot,我们需要在 slot 中执行原始指令。

set_ss_context 标记 ss_pending 为 true,单步挂起执行。match_addr 为操作地址 + opcode,用于单步中判断地址是否匹配。

spsr_set_debug_flag(regs,0) 清除 pstate 中 D debug 位。

kprobes_save_local_irqflag 保存原来的 pstate 信息,并且在新 pstate 中mask irq。

kernel_enable_single_step 激活单步模式。

3)instruction_pointer_set(regs, slot); 设置下一条执行的指令,这里将 slot 槽的地址设置为了 pc,那么当 debug 返回时将会跳向 slot 地址所在位置执行,其中 slot 槽中存储的则是原始指令,并且由于激活了单步,一旦我们执行了 slot 槽中的指令我们又会进入单步的debug异常。

4)当没有 insn 时,说明我们执行的是一个模拟指令,这里直接调用 arch_simulate_insn 来完成模拟执行。

static void __kprobes arch_simulate_insn(struct kprobe *p, struct pt_regs *regs)
{
	struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();

   	// api.handler 则会在解码 insn 阶段设置的 handler,在handler 中会模拟执行该指令
	if (p->ainsn.api.handler)
		p->ainsn.api.handler((u32)p->opcode, (long)p->addr, regs);

    // 由于是模拟执行的,我们不需要通过单步执行来进入 post 处理阶段,这里直接进行 post_handler 调用处理。
	/* single step simulated, now go for post processing */
	post_kprobe_handler(kcb, regs);
}

上述(3)和 5)均是 kprobe 重入的处理, 首先看(3):

static int __kprobes reenter_kprobe(struct kprobe *p,
				    struct pt_regs *regs,
				    struct kprobe_ctlblk *kcb)
{
    // 检查 kcb 状态,KPROBE_HIT_SSDONE(单步完成)和 KPROBE_HIT_ACTIVE(kprobe激活)
    // 在重入阶段,这两个状态表明重入的kprobe是不会执行的,增加 nmissed 计数,以便告知系统发生了什么
    // 在这种状态下调用setup_singlestep(p, regs, kcb, 1);设置重入状态的单步行为。
	switch (kcb->kprobe_status) {
	case KPROBE_HIT_SSDONE:
	case KPROBE_HIT_ACTIVE:
		kprobes_inc_nmissed_count(p);
		setup_singlestep(p, regs, kcb, 1);
		break;
    // 如果程序正在单步执行,或者 kcb 已经被标记为了,那么执行抛出错误
	case KPROBE_HIT_SS:
	case KPROBE_REENTER:
		pr_warn("Unrecoverable kprobe detected.\n");
		dump_kprobe(p);
		BUG();
		break;
    // 其他情况未知,报一个警告。
	default:
		WARN_ON(1);
		return 0;
	}

	return 1;
}

再来看 5),也就是重入中调用的 setup_singlestep(p, regs, kcb, 1) 处理。

首先调用 save_previous_kprobe

static void __kprobes save_previous_kprobe(struct kprobe_ctlblk *kcb)
{
    // 保存之前那个 kprobe 的状态。
	kcb->prev_kprobe.kp = kprobe_running();
	kcb->prev_kprobe.status = kcb->kprobe_status;
}

接着调用 set_current_kprobe 更新当前 kprobe 为重入的 kprobe,并且标记自己状态为 KPROBE_REENTER。

至此,pre_handler 的处理就完成了,并且当该 debug 返回后,首先执行的是 slot 槽中原始指令,并且由于处于单步模式,当原始指令执行完成后,进入单步异常,单步异常入口如上所述是 single_step_handler。

3.4.1 single_step_handler
single_step_handler
    -> kprobe_single_step_handler

int __kprobes
kprobe_single_step_handler(struct pt_regs *regs, unsigned int esr)
{
	struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();
	int retval;

	// 检查当前 cpu 的 kcb 的状态
	/* return error if this is not our step */
	retval = kprobe_ss_hit(kcb, instruction_pointer(regs));

   	// ok,kprobe 单步命中,进行 post handler 处理。
	if (retval == DBG_HOOK_HANDLED) {
        // 恢复原来 pstate 的值。
		kprobes_restore_local_irqflag(kcb, regs);
        // 清除单步状态,禁用单步
		kernel_disable_single_step();

        // 开始 post_handler 的处理。
		post_kprobe_handler(kcb, regs);
	}

	return retval;
}

static int __kprobes
kprobe_ss_hit(struct kprobe_ctlblk *kcb, unsigned long addr)
{
    // 如果 ss_pending 为 true 则是我们在 pre_handler 中设置的,检查 match_addr。
    // 当都满足时,说明kprobe的单步执行命令清理 kcb并返回 DBG_HOOK_HANDLED
	if ((kcb->ss_ctx.ss_pending)
	    && (kcb->ss_ctx.match_addr == addr)) {
		clear_ss_context(kcb);	/* clear pending ss */
		return DBG_HOOK_HANDLED;
	}
	/* not ours, kprobes should ignore it */
	return DBG_HOOK_ERROR;
}

post_kprobe_handler 在 kprobe 重入中会跳过 pre_handler 执行而直接调用 post_kprobe_handler,以及正常流程的下调用 post_kprobe_handler。

static void __kprobes
post_kprobe_handler(struct kprobe_ctlblk *kcb, struct pt_regs *regs)
{
	struct kprobe *cur = kprobe_running();
	// 首次检查当前是否是 kprobe 运行,不是则直接返回。
	if (!cur)
		return;

    // 当 restore 不为 0,我们使用了 slot,并在 slot 中执行了指令,现在从 restore 中恢复原来流程的 pc 指针。
	/* return addr restore if non-branching insn */
	if (cur->ainsn.api.restore != 0)
		instruction_pointer_set(regs, cur->ainsn.api.restore);

    // 如果状态是 KPROBE_REENTER,说明是kprobe 重入调用的 post_kprobe_handler,我们从 kcb 的prev 中恢复原来的 kprobe,以便于再次执行原来的 kprobe,并且返回,这次重入即可被处理掉。
	/* restore back original saved kprobe variables and continue */
	if (kcb->kprobe_status == KPROBE_REENTER) {
		restore_previous_kprobe(kcb);
		return;
	}
    // 正常流程,标记状态为单步完成,调用 post_handler。
	/* call post handler */
	kcb->kprobe_status = KPROBE_HIT_SSDONE;
	if (cur->post_handler)	{
		/* post_handler can hit breakpoint and single step
		 * again, so we enable D-flag for recursive exception.
		 */
		cur->post_handler(cur, regs, 0);
	}
	// kprobe 执行完成,清除 cur_kprobe, 表示 kprobe 没有运行了
	reset_current_kprobe();
}

至此 pre_handler 和 post _handler 的处理就完成了。

最后还剩 fault_handler。

该逻辑比较简单代码如下:

do_page_fault
    -> notify_page_fault
    	-> if (kprobe_running() && kprobe_fault_handler(regs, esr))

int __kprobes kprobe_fault_handler(struct pt_regs *regs, unsigned int fsr)
{
	struct kprobe *cur = kprobe_running();
	struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();

	switch (kcb->kprobe_status) {
	case KPROBE_HIT_SS:
	case KPROBE_REENTER:
		// 如果是单步执行触发的 page_fault,这是正常情况,重新指向该地址,由 do_page_fault 流程处理也错误。
		instruction_pointer_set(regs, (unsigned long) cur->addr);
		if (!instruction_pointer(regs))
			BUG();
...
	case KPROBE_HIT_ACTIVE:
	case KPROBE_HIT_SSDONE:

		// 如果是 pre_handler 和 post_handler 触发的 page_fault,则调用 fault_handler
		if (cur->fault_handler && cur->fault_handler(cur, regs, fsr))
			return 1;

		// 如果没有 fault_handler,那么检查错误是否是在 __asm_extable 段中,进行最后可能的处理。
		if (fixup_exception(regs))
			return 1;
	}
	return 0;
}

// 返回 1 说明由 fault_handler 或者 fixup_exception 处理了该错误,do_page_fault 不需要做进一步处理,直接返回。
// 返回 0 错误没有得到处理,交由 do_page_fault 进行处理。

至此 kprobe 的实现机制分析完毕。

接着看看如何移除一个 kprobe,移除kprobe 调用 unregister_kprobe。

unregister_kprobe
    -> unregister_kprobes

void unregister_kprobes(struct kprobe **kps, int num)
{
	int i;

	if (num <= 0)
		return;
	mutex_lock(&kprobe_mutex);
	for (i = 0; i < num; i++)
		if (__unregister_kprobe_top(kps[i]) < 0)
			kps[i]->addr = NULL;
	mutex_unlock(&kprobe_mutex);

	synchronize_rcu();
	for (i = 0; i < num; i++)
		if (kps[i]->addr)
			__unregister_kprobe_bottom(kps[i]);
}
 

卸载分为两个步骤,分别是 __unregister_kprobe_top 和 __unregister_kprobe_bottom,

// 负责从链表中卸载该 kprobe
__unregister_kprobe_top
    -> __disable_kprobe // 标记该 kprobe 为禁用状态,后续该 kprobe 不会被执行,方便卸载
    	-> arch_disarm_kprobe
    		// 恢复原始地址的 insn
    		-> patch_text(p->addr, p->opcode);
    -> list_del_rcu
    -> hlist_del_rcu

// 调用 synchronize_rcu 确保所有对当前 kprobe的访问结束,之后可以安全的移除释放该 kprobe
synchronize_rcu
    
// 释放该 kprobe 的数据,恢复原始指令。
__unregister_kprobe_bottom
    -> free_aggr_kprobe
    	-> arch_remove_kprobe
    		-> free_insn_slot
    	-> kfree(kprobe)

最后看一下 register_kretprobe 的实现原理:

kprobe 可以在内核的地址附加探测点来触发 pre_handler等回调的执行。kretprobe 则是在附加地址对应函数的返回处附加hook,可以在函数返回时被调用。它的实现机制是基于 kprobe的,原理是在要探测点注册一个 kprobe,该kprobe是内核定义的,内核会挂接自己定义的 pre_handler,一旦探测点执行,则是执行自定义的kprobe 的 pre_handler,在 pre_handler 中手动捕获并保存当前位置的返回地址,并修改返回地址为一个跳板函数,跳板函数执行我们kretprobe中注册的handler,并在完成调用后恢复原始的返回地址,完成一个 kretprobe 的触发。

kretprobe 注册的数据结构:

struct kretprobe {
    // kretporbe 注册内部构造的 kprobe,使用它来在 pre_handler 阶段修改函数返回地址为kretprobe的跳板地址
	struct kprobe kp;
	kretprobe_handler_t handler; // 函数返回时调用的 handler
	kretprobe_handler_t entry_handler; // 当 kprobe 中的 pre_handler 触发时在 pre_handler 中调用 entry_handler
	int maxactive; // kretprobe 可能多个 cpu 都会进入该流程,maxactive 限制最大同时能有多少个回调。
	int nmissed; // 一些情况不会调用回调,记录触发后未调用的次数
	size_t data_size; // 未使用?
	struct hlist_head free_instances; // 上述根据 maxactive 大小,会实例化 struct kretprobe_instance 结构体,每一个 kretprobe_instance 是一个 kretprobe 的实例。
	raw_spinlock_t lock;
};

// 记录一个 kretprobe 触发实例
struct kretprobe_instance {
	struct hlist_node hlist; // 每一个正在使用的实例会存放到全局 hashtable 中
	struct kretprobe *rp; // 实列对应自己所属的 kprobe
	kprobe_opcode_t *ret_addr; // 实例对应的真实返回地址
	struct task_struct *task; // 实例对应当前触发的 task,通过 task 计算 hash,在跳板函数中可以拿到自己对应实例
	char data[0]; // ?
};
int register_kretprobe(struct kretprobe *rp)
{
...
    // 检查探测点是不是一个合理的函数入口
    if (!kprobe_on_func_entry(rp->kp.addr, rp->kp.symbol_name, rp->kp.offset))
        return -EINVAL;

    // 检查地址是否在黑名单中, 在则不能 hook
    if (kretprobe_blacklist_size) {
		addr = kprobe_addr(&rp->kp);
		if (IS_ERR(addr))
			return PTR_ERR(addr);

		for (i = 0; kretprobe_blacklist[i].name != NULL; i++) {
			if (kretprobe_blacklist[i].addr == addr)
				return -EINVAL;
		}
	}
    
    // 为内部构造的 kprobe 注册一个 pre_handler,该 pre_handler 将会去修改返回地址以此构造一个返回回调。
    rp->kp.pre_handler = pre_handler_kretprobe;
	rp->kp.post_handler = NULL;
	rp->kp.fault_handler = NULL;
...
    // 根据 maxactive 实例化 kretprobe_instance,每触发一次 kretprobe 使用一个 kretprobe_instance,最大同时可以有 maxactive 个进行处理。
	/* Pre-allocate memory for max kretprobe instances */
	if (rp->maxactive <= 0) {
#ifdef CONFIG_PREEMPT
		rp->maxactive = max_t(unsigned int, 10, 2*num_possible_cpus());
#else
		rp->maxactive = num_possible_cpus();
#endif
	}
	raw_spin_lock_init(&rp->lock);
	INIT_HLIST_HEAD(&rp->free_instances);
	for (i = 0; i < rp->maxactive; i++) {
		inst = kmalloc(sizeof(struct kretprobe_instance) +
			       rp->data_size, GFP_KERNEL);
		if (inst == NULL) {
			free_rp_inst(rp);
			return -ENOMEM;
		}
		INIT_HLIST_NODE(&inst->hlist);
		hlist_add_head(&inst->hlist, &rp->free_instances);
	}
...
    // 将 kretprobe 内部构造的 kprobe 注册到系统中。
	/* Establish function entry probe point */
	ret = register_kprobe(&rp->kp);
}

从上可知,一切的逻辑在 kretprobe 中注册的 pre_handler。

当 kretprobe 中定义的探测点触发时调用 pre_handler:

static int pre_handler_kretprobe(struct kprobe *p, struct pt_regs *regs)
{
    struct kretprobe *rp = container_of(p, struct kretprobe, kp);
 ...
    // 如果当前上下文在 nmi 中,为了防止死锁,是不会进行 hook 的,只增加nmissed计数,让用户知道发生了什么。
    if (unlikely(in_nmi())) {
		rp->nmissed++;
		return 0;
	}

	/* TODO: consider to only swap the RA after the last pre_handler fired */
 	// 将 current 做 hash,然后从 rp 对应的 instance 中取出一个实例使用,该实例以这个 hash 为 key 值存放到全局 hash 中,后续跳板函数,根据 task hash 得到该实例,做进一步处理。
	hash = hash_ptr(current, KPROBE_HASH_BITS);
	raw_spin_lock_irqsave(&rp->lock, flags);
    // 如果 rp->free_instances 为空,则没有可用实例了,增加 nmissed ,并直接返回。
	if (!hlist_empty(&rp->free_instances)) {
        // 从链表中取出一个实例使用。
		ri = hlist_entry(rp->free_instances.first,
				struct kretprobe_instance, hlist);
		hlist_del(&ri->hlist);
		raw_spin_unlock_irqrestore(&rp->lock, flags);

		ri->rp = rp;
		ri->task = current;

        // 在这里调用我们自己定义的 entry_handler,在进入函数之前调用。
		if (rp->entry_handler && rp->entry_handler(ri, regs)) {
            // 返回不为 0,说明有错,直接归还实例,并返回。
			raw_spin_lock_irqsave(&rp->lock, flags);
			hlist_add_head(&ri->hlist, &rp->free_instances);
			raw_spin_unlock_irqrestore(&rp->lock, flags);
			return 0;
		}

        // 开始预备返回 hook。
		arch_prepare_kretprobe(ri, regs);

		/* XXX(hch): why is there no hlist_move_head? */
		INIT_HLIST_NODE(&ri->hlist);
		kretprobe_table_lock(hash, &flags);
        // 将实例根据 task hash 加入到全局 kretprobe_inst_table 中,后续返回处使用。
		hlist_add_head(&ri->hlist, &kretprobe_inst_table[hash]);
		kretprobe_table_unlock(hash, &flags);
	} else {
		rp->nmissed++;
		raw_spin_unlock_irqrestore(&rp->lock, flags);
	}
	return 0;
}

// 对于 arm64 预备一个返回 hook,将实际的返回地址保存到 ret_addr 中,修改现有返回地址为自定义的跳板函数地址 kretprobe_trampoline。
void __kprobes arch_prepare_kretprobe(struct kretprobe_instance *ri,
				      struct pt_regs *regs)
{
	ri->ret_addr = (kprobe_opcode_t *)regs->regs[30];

	/* replace return addr (x30) with trampoline */
	regs->regs[30] = (long)&kretprobe_trampoline;
}


ENTRY(kretprobe_trampoline)
	sub sp, sp, #S_FRAME_SIZE

    // 保存返回之前的寄存器信息,用于后续调用 trampoline_probe_handler 之后恢复原始的 pstate,栈信息。
	save_all_base_regs

	mov x0, sp
    // 执行自定义的kretprobe 处理函数
	bl trampoline_probe_handler
    // 将 trampoline_probe_handler 的返回作为 lr 的新值,恢复原本的返回路径。
	/*
	 * Replace trampoline address in lr with actual orig_ret_addr return
	 * address.
	 */
	mov lr, x0

    // 恢复原始的 pstate,栈信息。
	restore_all_base_regs

	add sp, sp, #S_FRAME_SIZE
	ret

ENDPROC(kretprobe_trampoline)
    
void __kprobes __used *trampoline_probe_handler(struct pt_regs *regs)
{
    // 可能有多个 cpu 进入该处,通过 current 计算 hash 得到实际任务对应的 kretprobe 实例 rp。
	hlist_for_each_entry_safe(ri, tmp, head, hlist) {
		if (ri->task != current)
			/* another task is sharing our hash bucket */
			continue;

		orig_ret_address = (unsigned long)ri->ret_addr;

		if (orig_ret_address != trampoline_address)
			/*
			 * This is the real return address. Any other
			 * instances associated with this task are for
			 * other calls deeper on the call stack
			 */
			break;
	}
...
	correct_ret_addr = ri->ret_addr;
	hlist_for_each_entry_safe(ri, tmp, head, hlist) {
		if (ri->task != current)
			/* another task is sharing our hash bucket */
			continue;

		orig_ret_address = (unsigned long)ri->ret_addr;
		if (ri->rp && ri->rp->handler) {
			__this_cpu_write(current_kprobe, &ri->rp->kp);
			get_kprobe_ctlblk()->kprobe_status = KPROBE_HIT_ACTIVE;
			ri->ret_addr = correct_ret_addr;
            // 调用kreprobe中注册的 handler,在函数返回前夕调用。
			ri->rp->handler(ri, regs);
			__this_cpu_write(current_kprobe, NULL);
		}

        // 将该实例返回 rp->free_instance,本次已处理完毕。
		recycle_rp_inst(ri, &empty_rp);

		if (orig_ret_address != trampoline_address)
			/*
			 * This is the real return address. Any other
			 * instances associated with this task are for
			 * other calls deeper on the call stack
			 */
			break;
	}
    // 返回真实的返回地址。
	return (void *)orig_ret_address;
}

至此,一个 kretprobe 的注册到整个调用逻辑分析完毕。

4 总结

kprobe 机制可以动态在内核任意合规地址注册探测点,使其实现灵活的内核调试机制,它也是后续各种调试基础以及拓展接口,其中 kretprobe 就是在 kprobe 基础上实现的一个捕获函数入口及返回 hook 的机制实现。

你可能感兴趣的:(linux,linux)