Linux kretprobe使用和原理


前言

之前介绍了kprobe的使用:Linux kprobe的使用
现在介绍kretprobe。kprobe提供三种探测手段:kprobe、kretprobe和jprobe,其中kretprobe和jprobe基于kprobe实现,分别应用于不同探测场景中。

kretprobe基于kprobe实现,用于获取被探测函数的返回值。
jprobe基于kprobe实现,它用于获取被探测函数的入参值。

一、demo

仍然拿_do_fork() 举例,_do_fork() 是Linux内核用来创建新任务的函数接口。

1.1 demo演示

/*
 * kretprobe_example.c
 *
 * Here's a sample kernel module showing the use of return probes to
 * report the return value and total time taken for probed function
 * to run.
 *
 * usage: insmod kretprobe_example.ko func=<func_name>
 *
 * If no func_name is specified, _do_fork is instrumented
 *
 * For more information on theory of operation of kretprobes, see
 * Documentation/kprobes.txt
 *
 * Build and insert the kernel module as done in the kprobe example.
 * You will see the trace data in /var/log/messages and on the console
 * whenever the probed function returns. (Some messages may be suppressed
 * if syslogd is configured to eliminate duplicate messages.)
 */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>
#include <linux/ktime.h>
#include <linux/limits.h>
#include <linux/sched.h>

static char func_name[NAME_MAX] = "_do_fork";
module_param_string(func, func_name, NAME_MAX, S_IRUGO);
MODULE_PARM_DESC(func, "Function to kretprobe; this module will report the"
			" function's execution time");

/* per-instance private data */
struct my_data {
    
    
	ktime_t entry_stamp;
};

/* Here we use the entry_hanlder to timestamp function entry */
static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    
    
	struct my_data *data;

	if (!current->mm)
		return 1;	/* Skip kernel threads */

	data = (struct my_data *)ri->data;
	data->entry_stamp = ktime_get();
	return 0;
}

/*
 * Return-probe handler: Log the return value and duration. Duration may turn
 * out to be zero consistently, depending upon the granularity of time
 * accounting on the platform.
 */
static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    
    
	unsigned long retval = regs_return_value(regs);
	struct my_data *data = (struct my_data *)ri->data;
	s64 delta;
	ktime_t now;

	now = ktime_get();
	delta = ktime_to_ns(ktime_sub(now, data->entry_stamp));
	pr_info("%s returned %lu and took %lld ns to execute\n",
			func_name, retval, (long long)delta);
	return 0;
}

static struct kretprobe my_kretprobe = {
    
    
	.handler		= ret_handler,
	.entry_handler		= entry_handler,
	.data_size		= sizeof(struct my_data),
	/* Probe up to 20 instances concurrently. */
	.maxactive		= 20,
};

static int __init kretprobe_init(void)
{
    
    
	int ret;

	my_kretprobe.kp.symbol_name = func_name;
	ret = register_kretprobe(&my_kretprobe);
	if (ret < 0) {
    
    
		pr_err("register_kretprobe failed, returned %d\n", ret);
		return -1;
	}
	pr_info("Planted return probe at %s: %p\n",
			my_kretprobe.kp.symbol_name, my_kretprobe.kp.addr);
	return 0;
}

static void __exit kretprobe_exit(void)
{
    
    
	unregister_kretprobe(&my_kretprobe);
	pr_info("kretprobe at %p unregistered\n", my_kretprobe.kp.addr);

	/* nmissed > 0 suggests that maxactive was set too low. */
	pr_info("Missed probing %d instances of %s\n",
		my_kretprobe.nmissed, my_kretprobe.kp.symbol_name);
}

module_init(kretprobe_init)
module_exit(kretprobe_exit)
MODULE_LICENSE("GPL");

结果,获取 _do_fork 函数的返回值和执行时间:
在这里插入图片描述

1.2 dmeo分析

(1)regs_return_value(regs)
这个函数就是来获取探测点函数返回值。

/*
 * Return-probe handler: Log the return value and duration. Duration may turn
 * out to be zero consistently, depending upon the granularity of time
 * accounting on the platform.
 */
static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    
    
	unsigned long retval = regs_return_value(regs);
	......
}

regs_return_value(regs)宏提供了一个简单的抽象,从体系结构ABI定义的适当寄存器中提取返回值。
这个函数与体系架构有关,从struct pt_regs结构体获取返回值:

对于x86体系架构:
RAX:Accumulator for operands and results data 。一般用于存放返回值。

// arch/x86/include/asm/ptrace.h

static inline unsigned long regs_return_value(struct pt_regs *regs)
{
    
    
	return regs->ax;
}

对于ARM64架构:
regs[0]:X0,一般存放函数返回值。

// arch/arm64/include/asm/ptrace.h
static inline unsigned long regs_return_value(struct pt_regs *regs)
{
    
    
	return regs->regs[0];
}

static struct kretprobe my_kretprobe = {
    
    
	.handler		= ret_handler,
	......
};

(2)struct kretprobe_instance 成员data
data保存用户使用的kretprobe私有数据,它会在整个kretprobe探测运行期间在entry_handler和handler回调函数之间进行传递(一般用于实现统计被探测函数的执行耗时)。

struct kretprobe_instance {
    
    
	......
	char data[0];
};
/* per-instance private data */
struct my_data {
    
    
	ktime_t entry_stamp;
};

/* Here we use the entry_hanlder to timestamp function entry */
static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    
    
	struct my_data *data;

	if (!current->mm)
		return 1;	/* Skip kernel threads */

	//保存_do_fork 函数执行的开始时间
	data = (struct my_data *)ri->data;
	data->entry_stamp = ktime_get();
	return 0;
}
static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs){
    
    
	......
	//计算出_do_fork 函数的执行时间
	struct my_data *data = (struct my_data *)ri->data;
	......
}

kretprobe_instance中私有数据data,在entry_handler回调函数中保存_do_fork 函数执行的开始时间。函数ret_handler在do_fork函数执行完成返回后被调用,它根据当前的时间减去kretprobe_instance中私有数据保存的起始时间,即可计算出_do_fork 函数执行的耗时。

二、源码分析

2.1 struct kretprobe

/*
 * Function-return probe -
 * Note:
 * User needs to provide a handler function, and initialize maxactive.
 * maxactive - The maximum number of instances of the probed function that
 * can be active concurrently.
 * nmissed - tracks the number of times the probed function's return was
 * ignored, due to maxactive being too low.
 *
 */
struct kretprobe {
    
    
	struct kprobe kp;
	kretprobe_handler_t handler;
	kretprobe_handler_t entry_handler;
	int maxactive;
	int nmissed;
	size_t data_size;
	struct hlist_head free_instances;
	raw_spinlock_t lock;
};

typedef int (*kretprobe_handler_t) (struct kretprobe_instance *,
				    struct pt_regs *);

(1)其中我们可以看到 struct kretprobe 结构体中 有struct kprobe成员(kretprobe时基于 kprobe实现的)。
handler:用户自定义回调函数,被探测函数返回后被调用,一般在这个函数中获取被探测函数的返回值。

(2)entry_handler:用户自定义回调函数,这是Kretprobes 提供了一个可选的用户指定的处理程序,它在函数入口上运行。 每当 kretprobe 放置在函数入口处的 kprobe 被命中时,都会调用用户定义的 entry_handler,如果有的话。 如果 entry_handler 返回 0(成功),则保证在函数返回时调用相应的返回处理程序。 如果 entry_handler 返回非零错误,则 Kprobes 将返回地址保持原样,并且 kretprobe 对该特定函数实例没有进一步的影响。

(3)maxactive:被探测函数可以同时活动的最大实例数。来指定可以同时探测多少个指定函数的实例。
register_kretprobe() 预分配指定数量的 kretprobe_instance 对象。

(4)nmissed:跟踪被探测函数的返回被忽略的次数(maxactive设置的过低)。

(5)data_size:表示kretprobe私有数据的大小,在注册kretprobe时会根据该大小预留空间。
(6)free_instances :表示空闲的kretprobe运行实例链表,它链接了本kretprobe的空闲实例struct kretprobe_instance结构体表示。

2.2 struct kretprobe_instance

struct kretprobe_instance {
    
    
	struct hlist_node hlist;
	struct kretprobe *rp;
	kprobe_opcode_t *ret_addr;
	struct task_struct *task;
	char data[0];
};

(1)这个结构体表示kretprobe的运行实例,前文说过被探测函数在跟踪期间可能存在并发执行的现象,因此kretprobe使用一个kretprobe_instance来跟踪一个执行流,支持的上限为maxactive。在没有触发探测时,所有的kretprobe_instance实例都保存在free_instances表中,每当有执行流触发一次kretprobe探测,都会从该表中取出一个空闲的kretprobe_instance实例用来跟踪。
(2)kretprobe_instance结构提中的rp指针指向所属的kretprobe;
(3)ret_addr用于保存原始被探测函数的返回地址(后文会看到被探测函数返回地址会被暂时替换);
(4)task用于绑定其跟踪的进程;
(5)data保存用户使用的kretprobe私有数据,它会在整个kretprobe探测运行期间在entry_handler和handler回调函数之间进行传递(一般用于实现统计被探测函数的执行耗时)。

2.3 register_kretprobe

(1)register_kretprobe
kretprobe探测点的blackpoint,用来表示不支持kretprobe探测的函数的信息。name表示该函数名,addr表示该函数的地址。

struct kretprobe_blackpoint {
    
    
	const char *name;
	void *addr;
};

blackpoint与架构相关,x86架构不支持的kretprobe探测点如下:

// arch/x86/kernel/kprobes/core.c
// 不支持kretprobe探测的函数,从blacklist这个名字中我们也知道其含义了。
struct kretprobe_blackpoint kretprobe_blacklist[] = {
    
    
	{
    
    "__switch_to", }, /* This function switches only current task, but
			      doesn't switch kernel stack.*/
	{
    
    NULL, NULL}	/* Terminator */
};

const int kretprobe_blacklist_size = ARRAY_SIZE(kretprobe_blacklist);

函数的开头首先处理 kretprobe_blacklis t,如果指定的被探测函数在这个blacklist中就直接返回EINVAL,表示不支持探测,在x86架构中是"__switch_to" 这个函数,表示这个函数不能被kretprobe。

int register_kretprobe(struct kretprobe *rp)
{
    
    
	int ret = 0;
	struct kretprobe_instance *inst;
	int i;
	void *addr;

	if (kretprobe_blacklist_size) {
    
    
		addr = kprobe_addr(&rp->kp);
		if (IS_ERR(addr))
			return PTR_ERR(addr);
		//如果kretprobe到kretprobe_blacklist中函数,则返回EINVAL
		for (i = 0; kretprobe_blacklist[i].name != NULL; i++) {
    
    
			if (kretprobe_blacklist[i].addr == addr)
				return -EINVAL;
		}
	}

	//内核设置回调函数 pre_handler_kretprobe 。
	//与kprobe不同的是:kretprobe不支持用户定义pre_handler和post_handler等回调函数。
	
	rp->kp.pre_handler = pre_handler_kretprobe;
	rp->kp.post_handler = NULL;
	rp->kp.fault_handler = NULL;
	rp->kp.break_handler = NULL;

	/* 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);
	//根据maxactive值分配 struct kretprobe_instance 内存空间
	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);
	}

	rp->nmissed = 0;
	/* Establish function entry probe point */
	//注册kprobe探测点
	ret = register_kprobe(&rp->kp);
	if (ret != 0)
		free_rp_inst(rp);
	return ret;
}
EXPORT_SYMBOL_GPL(register_kretprobe);

最后调用 register_kprobe(&rp->kp),注册kprobe点,可以看出kretprobe也是基于kprobe机制实现的,kretprobe也是一种特殊形式的kprobe。
kretprobe注册完成后就默认启动探测。

2.4 pre_handler_kretprobe

pre_handler_kretprobe这个函数是内核自己定义的,内核已经指定该回调函数,不支持用户自定义。这个 kprobe pre_handler 在每个 kretprobe 中注册。 当探针命中时,它将设置返回探针。

#ifdef CONFIG_KRETPROBES
/*
 * This kprobe pre_handler is registered with every kretprobe. When probe
 * hits it will set up the return probe.
 */
static int pre_handler_kretprobe(struct kprobe *p, struct pt_regs *regs)
{
    
    
	struct kretprobe *rp = container_of(p, struct kretprobe, kp);
	unsigned long hash, flags = 0;
	struct kretprobe_instance *ri;

	/*
	 * To avoid deadlocks, prohibit return probing in NMI contexts,
	 * just skip the probe and increase the (inexact) 'nmissed'
	 * statistical counter, so that the user is informed that
	 * something happened:
	 */
	if (unlikely(in_nmi())) {
    
    
		rp->nmissed++;
		return 0;
	}

	/* TODO: consider to only swap the RA after the last pre_handler fired */
	hash = hash_ptr(current, KPROBE_HASH_BITS);
	raw_spin_lock_irqsave(&rp->lock, flags);
	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;1if (rp->entry_handler && rp->entry_handler(ri, regs)) {
    
    
			raw_spin_lock_irqsave(&rp->lock, flags);
			hlist_add_head(&ri->hlist, &rp->free_instances);
			raw_spin_unlock_irqrestore(&rp->lock, flags);
			return 0;
		}2arch_prepare_kretprobe(ri, regs);

		/* XXX(hch): why is there no hlist_move_head? */
		INIT_HLIST_NODE(&ri->hlist);
		kretprobe_table_lock(hash, &flags);
		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;
}
NOKPROBE_SYMBOL(pre_handler_kretprobe);

2.4.1 entry_handler

struct kretprobe *rp
rp->entry_handler && rp->entry_handler(ri, regs)

entry_handler这个回调函数就是用户自己定义的回调函数(可选的用户指定的处理程序),前面我们已经介绍过了,在这里不在介绍。

/* Here we use the entry_hanlder to timestamp function entry */
static int entry_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    
    
	struct my_data *data;

	//内核线程 task->mm == NULL
	if (!current->mm)
		return 1;	/* Skip kernel threads */

	data = (struct my_data *)ri->data;
	data->entry_stamp = ktime_get();
	return 0;
}

2.4.1 arch_prepare_kretprobe

arch_prepare_kretprobe(ri, regs)
该函数架构相关,struct kretprobe_instance结构体 的 ret_addr 成员用于保存并替换regs中的返回地址。返回地址被替换为kretprobe_trampoline。

2.4.1.1 x86架构

// arch/x86/kernel/kprobes/core.c

#define stack_addr(regs) ((unsigned long *)kernel_stack_pointer(regs))

//	x86_64
//	arch/x86/include/asm/ptrace.h
static inline unsigned long kernel_stack_pointer(struct pt_regs *regs)
{
    
    
	return regs->sp;
}
// arch/x86/kernel/kprobes/core.c

void arch_prepare_kretprobe(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    
    
	unsigned long *sara = stack_addr(regs);

	ri->ret_addr = (kprobe_opcode_t *) *sara;

	/* Replace the return addr with trampoline addr */
	*sara = (unsigned long) &kretprobe_trampoline;
}
NOKPROBE_SYMBOL(arch_prepare_kretprobe);
//struct kretprobe_instance *ri;
//ri->ret_addr;

struct kretprobe_instance {
    
    
	kprobe_opcode_t *ret_addr;  //用于保存原始被探测函数的返回地址
};

2.4.1.2 ARM64架构

// arch/arm64/kernel/probes/kprobes.c

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

ARM64架构中regs->regs[30]是LR(procedure link register)寄存器(X30 :LR)。

2.4.1.3 小结

kretprobe是基于kprobe实现的,有一个固定的pre_handler回调函数,在内核中实现,无需用户编写。而在kprobe中pre_handler函数是提供给用户的回调函数。

rp->kp.pre_handler = pre_handler_kretprobe;  //内核中已经实现
rp->kp.post_handler = NULL;
rp->kp.fault_handler = NULL;
rp->kp.break_handler = NULL;

kretprobe提供给用户的两个回调函数:

kretprobe_handler_t handler;
kretprobe_handler_t entry_handler; // (可选)

pre_handler回调函数会为kretprobe探测函数执行的返回值做准备工作,其中最主要的就是替换掉正常流程的返回地址,让被探测函数在执行之后能够跳转到kretprobe设计的函数 kretprobe_trampoline中去。

2.5 kretprobe_trampoline

pre_handler_kretprobe函数返回后,kprobe流程接着执行singlestep流程并返回到正常的执行流程,被探测函数(do_fork)继续执行,直到它执行完毕并返回。由于返回地址被替换为kretprobe_trampoline,所以跳转到kretprobe_trampoline执行,该函数架构相关且有嵌入汇编实现。

该函数会获取被探测函数的寄存器信息并调用用户定义的回调函数输出其中的返回值,最后函数返回正常的执行流程。

static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs)
{
    
    
	unsigned long retval = regs_return_value(regs);
	......
}


static struct kretprobe my_kretprobe = {
    
    
	.handler		= ret_handler,
};

2.5.1 x86架构

(1)

kretprobe_trampoline
	-->trampoline_handler
kretprobe_trampoline

(2)
kretprobe_trampoline

// arch/x86/kernel/kprobes/core.c

/*
 * When a retprobed function returns, this code saves registers and
 * calls trampoline_handler() runs, which calls the kretprobe's handler.
 */
asm(
	".global kretprobe_trampoline\n"
	".type kretprobe_trampoline, @function\n"
	"kretprobe_trampoline:\n"
#ifdef CONFIG_X86_64
	/* We don't bother saving the ss register */
	"	pushq %rsp\n"
	"	pushfq\n"
	SAVE_REGS_STRING
	"	movq %rsp, %rdi\n"
	"	call trampoline_handler\n"
	/* Replace saved sp with true return address. */
	"	movq %rax, 152(%rsp)\n"
	RESTORE_REGS_STRING
	"	popfq\n"
#else
	"	pushf\n"
	SAVE_REGS_STRING
	"	movl %esp, %eax\n"
	"	call trampoline_handler\n"
	/* Move flags to cs */
	"	movl 56(%esp), %edx\n"
	"	movl %edx, 52(%esp)\n"
	/* Replace saved flags with true return address. */
	"	movl %eax, 56(%esp)\n"
	RESTORE_REGS_STRING
	"	popf\n"
#endif
	"	ret\n"
	".size kretprobe_trampoline, .-kretprobe_trampoline\n"
);
NOKPROBE_SYMBOL(kretprobe_trampoline);
STACK_FRAME_NON_STANDARD(kretprobe_trampoline);

(3)
trampoline_handler

// arch/x86/kernel/kprobes/core.c

/*
 * Called from kretprobe_trampoline
 */
__visible __used void *trampoline_handler(struct pt_regs *regs)
{
    
    
	struct kretprobe_instance *ri = NULL;
	struct hlist_head *head, empty_rp;
	struct hlist_node *tmp;
	unsigned long flags, orig_ret_address = 0;
	unsigned long trampoline_address = (unsigned long)&kretprobe_trampoline;
	kprobe_opcode_t *correct_ret_addr = NULL;

	INIT_HLIST_HEAD(&empty_rp);
	kretprobe_hash_lock(current, &head, &flags);
	/* fixup registers */
#ifdef CONFIG_X86_64
	regs->cs = __KERNEL_CS;
#else
	regs->cs = __KERNEL_CS | get_kernel_rpl();
	regs->gs = 0;
#endif
	regs->ip = trampoline_address;
	regs->orig_ax = ~0UL;

	/*
	 * It is possible to have multiple instances associated with a given
	 * task either because multiple functions in the call path have
	 * return probes installed on them, and/or more than one
	 * return probe was registered for a target function.
	 *
	 * We can handle this because:
	 *     - instances are always pushed into the head of the list
	 *     - when multiple return probes are registered for the same
	 *	 function, the (chronologically) first instance's ret_addr
	 *	 will be the real return address, and all the rest will
	 *	 point to kretprobe_trampoline.
	 */
	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;
	}

	kretprobe_assert(ri, orig_ret_address, trampoline_address);

	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;
			ri->rp->handler(ri, regs);
			__this_cpu_write(current_kprobe, NULL);
		}

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

	kretprobe_hash_unlock(current, &flags);

	hlist_for_each_entry_safe(ri, tmp, &empty_rp, hlist) {
    
    
		hlist_del(&ri->hlist);
		kfree(ri);
	}
	return (void *)orig_ret_address;
}
NOKPROBE_SYMBOL(trampoline_handler);

(4)
ri->rp->handler(ri, regs)表示执行用户态自定义的回调函数handler(用来获取_do_fork函数的返回值),handler回调函数执行完毕以后,调用recycle_rp_inst函数将当前的kretprobe_instance实例从kretprobe_inst_table哈希表释放,重新链入free_instances中,以备后面kretprobe触发时使用,另外如果kretprobe已经被注销则将它添加到销毁表中待销毁。

ri->rp->handler(ri, regs);
	->recycle_rp_inst(ri, &empty_rp);
void recycle_rp_inst(struct kretprobe_instance *ri,
		     struct hlist_head *head)
{
    
    
	struct kretprobe *rp = ri->rp;

	/* remove rp inst off the rprobe_inst_table */
	hlist_del(&ri->hlist);
	INIT_HLIST_NODE(&ri->hlist);
	if (likely(rp)) {
    
    
		raw_spin_lock(&rp->lock);
		hlist_add_head(&ri->hlist, &rp->free_instances);
		raw_spin_unlock(&rp->lock);
	} else
		/* Unregistering */
		hlist_add_head(&ri->hlist, head);
}
NOKPROBE_SYMBOL(recycle_rp_inst);

(5)
trampoline_handler函数执行完后,返回被探测函数的原始返回地址,执行流程再次回到kretprobe_trampoline函数中,将保存的 sp 替换为真实的返回地址。
从rax寄存器中取出原始的返回地址,然后恢复原始函数调用栈空间,最后跳转到原始返回地址执行,至此函数调用的流程就回归正常流程了,整个kretprobe探测结束。

/* Replace saved sp with true return address. */
	"	movq %rax, 152(%rsp)\n"
	RESTORE_REGS_STRING
	"	popfq\n"

2.5.2 ARM64架构

(1)

kretprobe_trampoline 
	-->trampoline_probe_handler
kretprobe_trampoline 

(2)
kretprobe_trampoline

// arch/arm64/kernel/probes/kprobes_trampoline.S

ENTRY(kretprobe_trampoline)
	sub sp, sp, #S_FRAME_SIZE

	save_all_base_regs

	mov x0, sp
	bl trampoline_probe_handler
	/*
	 * Replace trampoline address in lr with actual orig_ret_addr return
	 * address.
	 */
	mov lr, x0

	restore_all_base_regs

	add sp, sp, #S_FRAME_SIZE
	ret

ENDPROC(kretprobe_trampoline)

(3)
trampoline_probe_handler

// arch/arm64/kernel/probes/kprobes.c

void __kprobes __used *trampoline_probe_handler(struct pt_regs *regs)
{
    
    
	struct kretprobe_instance *ri = NULL;
	struct hlist_head *head, empty_rp;
	struct hlist_node *tmp;
	unsigned long flags, orig_ret_address = 0;
	unsigned long trampoline_address =
		(unsigned long)&kretprobe_trampoline;
	kprobe_opcode_t *correct_ret_addr = NULL;

	INIT_HLIST_HEAD(&empty_rp);
	kretprobe_hash_lock(current, &head, &flags);

	/*
	 * It is possible to have multiple instances associated with a given
	 * task either because multiple functions in the call path have
	 * return probes installed on them, and/or more than one
	 * return probe was registered for a target function.
	 *
	 * We can handle this because:
	 *     - instances are always pushed into the head of the list
	 *     - when multiple return probes are registered for the same
	 *	 function, the (chronologically) first instance's ret_addr
	 *	 will be the real return address, and all the rest will
	 *	 point to kretprobe_trampoline.
	 */
	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;
	}

	kretprobe_assert(ri, orig_ret_address, trampoline_address);

	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;
			ri->rp->handler(ri, regs);
			__this_cpu_write(current_kprobe, NULL);
		}

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

	kretprobe_hash_unlock(current, &flags);

	hlist_for_each_entry_safe(ri, tmp, &empty_rp, hlist) {
    
    
		hlist_del(&ri->hlist);
		kfree(ri);
	}
	return (void *)orig_ret_address;
}

(4)
将 lr寄存器中的trampoline地址替换为实际的 orig_ret_addr 返回地址。
从x0寄存器中取出原始的返回地址,然后恢复原始函数调用栈空间,最后跳转到原始返回地址执行,至此函数调用的流程就回归正常流程了,整个kretprobe探测结束。

/*
	 * Replace trampoline address in lr with actual orig_ret_addr return
	 * address.
	 */
	mov lr, x0

	restore_all_base_regs

	add sp, sp, #S_FRAME_SIZE
	ret

2.6 unregister_kretprobe

void unregister_kretprobe(struct kretprobe *rp)
{
    
    
	unregister_kretprobes(&rp, 1);
}
EXPORT_SYMBOL_GPL(unregister_kretprobe);

void unregister_kretprobes(struct kretprobe **rps, int num)
{
    
    
	int i;

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

	synchronize_sched();
	for (i = 0; i < num; i++) {
    
    
		if (rps[i]->kp.addr) {
    
    
			__unregister_kprobe_bottom(&rps[i]->kp);
			cleanup_rp_inst(rps[i]);
		}
	}
}
EXPORT_SYMBOL_GPL(unregister_kretprobes);

三、总结

3.1 kretprobe工作原理

当您调用 register_kretprobe() 时,Kprobes 在函数的入口处建立一个 kprobe。 当被探测的函数被调用并且这个探测被命中时,Kprobes 会保存一份返回地址的副本,并将返回地址替换为“trampoline”的地址。trampoline是一段任意代码——通常只是一条 nop 指令。 在启动时,Kprobes 在 trampoline 上注册一个 kprobe。

/* Establish function entry probe point */
	ret = register_kprobe(&rp->kp);

当被探测的函数执行它的 return instruction时,控制权传递给trampoline并且该探测被命中。 Kprobes 的 trampoline 处理程序调用与 kretprobe 关联的用户指定的返回处理程序,然后将保存的指令指针设置为保存的返回地址,这就是从陷阱返回后恢复执行的地方。

由于返回探测是通过将返回地址替换为 trampoline 的地址来实现的,因此堆栈回溯和对 __builtin_return_address() 的调用通常会产生 trampoline 的地址,而不是 kretprobed 函数的实际返回地址。 ( __builtin_return_address() 仅用于检测和错误报告。)

当被探测函数正在执行时,它的返回地址存储在一个 kretprobe_instance 类型的 ret_add r对象中。 在调用 register_kretprobe() 之前,用户设置 kretprobe 结构的 maxactive 字段来指定可以同时探测多少个指定函数的实例。 register_kretprobe() 预分配指定数量的 kretprobe_instance 对象。

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

例如,如果函数是非递归的并且在调用时持有自旋锁,那么 maxactive = 1 就足够了。 如果函数是非递归的并且永远不会放弃 CPU(例如,通过信号量或抢占),则 NR_CPUS 应该足够了。 如果 maxactive <= 0,则设置为默认值。 如果启用了 CONFIG_PREEMPT,则默认值为 max(10, 2*NR_CPUS)。 否则,默认值为 NR_CPUS。

	/* 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
	}

如果你将 maxactive 设置得太低,不会有多大问题,只会错过一些探测。 在 kretprobe 结构中,nmissed 字段在注册返回探针时设置为零,并且每次进入被探测函数但没有可用于建立返回探针的 kretprobe_instance 对象时递增。

3.2 Kretprobe entry-handler

Kretprobes 还提供了一个可选的用户指定的处理程序,它在函数入口上运行。 该处理程序是通过设置 kretprobe 结构的 entry_handler 字段来指定的。 每当 kretprobe 放置在函数入口处的 kprobe 被命中时,都会调用用户定义的 entry_handler,如果有的话。 如果 entry_handler 返回 0(成功),则保证在函数返回时调用相应的返回处理程序。 如果 entry_handler 返回非零错误,则 Kprobes 将返回地址保持原样,并且 kretprobe 对该特定函数实例没有进一步的影响。

使用与它们关联的唯一 kretprobe_instance 对象来匹配多个入口和返回处理程序调用。 此外,用户还可以将每个返回实例的私有数据指定为每个 kretprobe_instance 对象的一部分。 这在相应的用户条目和返回处理程序之间共享私有数据时特别有用。 每个私有数据对象的大小可以在 kretprobe 注册时通过设置 kretprobe 结构的 data_size 字段来指定。 可以通过每个 kretprobe_instance 对象的数据字段访问此数据。

如果输入了探测函数但没有可用的 kretprobe_instance 对象,则除了增加 nmissed 计数外,还会跳过用户 entry_handler 调用。

参考资料

Linux 内核源码 4.10.0

https://blog.csdn.net/luckyapple1028/article/details/54782659
https://www.kernel.org/doc/html/latest/trace/kprobes.html

猜你喜欢

转载自blog.csdn.net/weixin_45030965/article/details/125922528