Linux內核調試技術——kretprobe使用與實現

前兩篇博文介紹了kprobes探測技術中kprobe和jprobe的使用與實現。本文介紹kprobes中的最後一種探測技術kretprobe,它同樣基於kprobe實現,可用於探測函數的返回值以及計算函數執行的耗時。本文首先通過一個簡單的示例程序介紹kretprobe的使用方式,然後通過源碼分析它是如何實現的。

內核源碼:Linux-4.1.x

實驗環境:Fedora25(x86_64)、樹莓派1b


1、kretprobe使用示例

使用kretprobe探測函數的返回值同jprobe一樣需要編寫內核模塊,當然內核也提供了一個簡單的示例程序kretprobe_example.c(位於sample/kprobes目錄),該程序的實現更爲通用,用戶可以在使用時直接通過模塊參數指定需要探測的函數,它在探測函數返回值的同時還統計了被探測函數的執行用時。在分析kretprobe_example.c之前先熟悉一下kretprobe的結構體定義和API接口。

1.1、kretprobe結構體與API介紹

/*
 * 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;
};
struct kretprobe結構體用於定義一個kretprobe。由於它的實現基於kprobe,結構體中自然也少不了kprobe字段;然後handler和entry_handler分別表示兩個回調函數,用戶自行定義,entry_handler會在被探測函數執行之前被調用,handler在被探測函數返回後被調用(一般在這個函數中打印被探測函數的返回值);maxactive表示同時支持並行探測的上限,因爲kretprobe會跟蹤一個函數從開始到結束,因此對於一些調用比較頻繁的被探測函數,在探測的時間段內重入的概率比較高,這個maxactive字段值表示在重入情況發生時,支持同時檢測的進程數(執行流數)的上限,若並行觸發的數量超過了這個上限,則kretprobe不會進行跟蹤探測,僅僅增加nmissed字段的值以作提示;data_size字段表示kretprobe私有數據的大小,在註冊kretprobe時會根據該大小預留空間;最後free_instances表示空閒的kretprobe運行實例鏈表,它鏈接了本kretprobe的空閒實例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];
};
這個結構體表示kretprobe的運行實例,前文說過被探測函數在跟蹤期間可能存在併發執行的現象,因此kretprobe使用一個kretprobe_instance來跟蹤一個執行流,支持的上限爲maxactive。在沒有觸發探測時,所有的kretprobe_instance實例都保存在free_instances表中,每當有執行流觸發一次kretprobe探測,都會從該表中取出一個空閒的kretprobe_instance實例用來跟蹤。

kretprobe_instance結構提中的rp指針指向所屬的kretprobe;ret_addr用於保存原始被探測函數的返回地址(後文會看到被探測函數返回地址會被暫時替換);task用於綁定其跟蹤的進程;最後data保存用戶使用的kretprobe私有數據,它會在整個kretprobe探測運行期間在entry_handler和handler回調函數之間進行傳遞(一般用於實現統計被探測函數的執行耗時)。

1.2、示例kretprobe_example分析與演示

內核提供的kretprobe_example.c示例程序默認探測do_fork函數的執行耗時和返回值,支持通過模塊參數指定被探測函數,用戶若需要探測其他函數,只需要在加載內核模塊時傳入自己需要探測的函數名即可,無需修改模塊代碼。

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

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) {
		printk(KERN_INFO "register_kretprobe failed, returned %d\n",
				ret);
		return -1;
	}
	printk(KERN_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);
	printk(KERN_INFO "kretprobe at %p unregistered\n",
			my_kretprobe.kp.addr);

	/* nmissed > 0 suggests that maxactive was set too low. */
	printk(KERN_INFO "Missed probing %d instances of %s\n",
		my_kretprobe.nmissed, my_kretprobe.kp.symbol_name);
}
程序定義了一個結構體struct my_data,其中唯一的參數entry_stamp用於計算函數執行的時間;同時程序定義了一個struct kretprobe實例,注意其中的私有數據長度爲my_data的長度,最大支持的並行探測數爲20(即若在某一時刻,do_fork函數同時調用的執行流數量超過20那將不會再進行探測跟蹤)。最後在模塊的init和exit函數中僅僅調用register_kretprobe和unregister_kretprobe函數對my_kretprobe進行註冊和註銷,在kretprobe註冊完成後就默認啓動探測了。

/* 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)
{
	int 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));
	printk(KERN_INFO "%s returned %d and took %lld ns to execute\n",
			func_name, retval, (long long)delta);
	return 0;
}
函數entry_handler在do_fork函數被調用時觸發調用,注意第一個入參不是struct kretprobe結構,而是代表一個探測實例的struct kretprobe_instance結構,它從kretprobe的free_instances鏈表中分配,在跟蹤完本次觸發流程後回收。entry_handler函數利用了kretprobe_instance中私有數據,保存do_fork函數執行的開始時間。

函數ret_handler在do_fork函數執行完成返回後被調用,它根據當前的時間減去kretprobe_instance中私有數據保存的起始時間,即可計算出do_fork函數執行的耗時。同時它調用regs_return_value函數獲取do_fork函數的返回值,該函數是架構相關的:

static inline long regs_return_value(struct pt_regs *regs)
{
	return regs->ARM_r0;
}
例如arm環境是通過r0寄存器傳遞返回值的,因此該函數的實現僅僅是返回r0寄存器的值(需要注意的是regs指針傳遞的是do_fork函數返回時所保存的寄存器信息,這一點後面會分析)。ret_handler函數最後打印出do_fork函數的返回值和執行耗時(單位ns)。

下面在x86_64環境下演示該示例程序的實際效果(環境配置請參考《Linux內核調試技術——kprobe使用與實現》):

<6>[ 1217.859349] _do_fork returned 1838 and took 518081 ns to execute
<6>[ 1217.863880] _do_fork returned 1839 and took 223701 ns to execute
<6>[ 1217.865731] _do_fork returned 1840 and took 221746 ns to execute
<6>[ 1220.077508] _do_fork returned 1841 and took 433573 ns to execute
<6>[ 1220.081512] _do_fork returned 1842 and took 362684 ns to execute
<6>[ 1220.083767] _do_fork returned 1843 and took 284184 ns to execute
<6>[ 1220.995537] _do_fork returned 1844 and took 503414 ns to execute
<6>[ 1221.000363] _do_fork returned 1845 and took 427427 ns to execute

加載kretprobe_example.ko,不指定探測函數,默認探測do_fork函數,內核輸出以上messages。可見do_fork函數的返回值(新創建進程的pid)是依次遞增的,同時函數執行用時也呈現的非常直觀。因此,使用kretprobe可以簡單的獲取一個函數在執行時的返回值,在內核調試時非常有用。探測其他函數方法類似,不再贅述。


2、kretprobe實現分析

kretprobe的實現基於kprobe,因此這裏將在前一篇博文《Linux內核調試技術——kprobe使用與實現》的基礎之上分析它的實現,主要包括kretprobe註冊流程和觸發探測流程,涉及kprobe的部分不再詳細描。

2.1、kretprobe實現原理

同jprobe類似,kretprobe也是一種特殊形式的kprobe,它有自己私有的pre_handler,並不支持用戶定義pre_handler和post_handler等回調函數。其中它的pre_handler回調函數會爲kretprobe探測函數執行的返回值做準備工作,其中最主要的就是替換掉正常流程的返回地址,讓被探測函數在執行之後能夠跳轉到kretprobe所精心設計的函數中去,它會獲取函數返回值,然後調用kretprobe->handler回調函數(被探測函數的返回地址此刻得到輸出),最後恢復正常執行流程。

2.2、註冊一個kretprobe

kretprobe探測模塊調用register_kretprobe函數向內核註冊一個kretprobe實例,代碼路徑爲kernel/kprobes.c,其主要流程如下圖:


圖1 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);

		for (i = 0; kretprobe_blacklist[i].name != NULL; i++) {
			if (kretprobe_blacklist[i].addr == addr)
				return -EINVAL;
		}
	}
函數的開頭首先處理kretprobe所特有的blacklist,如果指定的被探測函數在這個blacklist中就直接返回EINVAL,表示不支持探測。其中kretprobe_blacklist_size表示隊列的長度,kretprobe_blacklist是一個全局結構體數組,每一項都是一個struct kretprobe_blackpoint結構體:

struct kretprobe_blackpoint {
	const char *name;
	void *addr;
};
其中name字段表示函數名,addr表示函數的運行地址。該kretprobe_blacklist是架構相關的,用於申明該架構哪些函數是不支持使用kretprobe探測的,其中arm架構並沒有被定義,而x86_64架構的定義如下:

struct kretprobe_blackpoint kretprobe_blacklist[] = {
	{"__switch_to", }, /* This function switches only current task, but
			      doesn't switch kernel stack.*/
	{NULL, NULL}	/* Terminator */
};
這表明在x86_64架構下的__switch_to函數不可以被kretprobe所探測(這一點在內核的kprobes.txt中已經有說明)。回到register_kretprobe函數中,在blacklist檢測時比較的是函數運行地址addr字段,該字段在kprobes子系統初始化函數init_kprobes中初始化:

	if (kretprobe_blacklist_size) {
		/* lookup the function address from its name */
		for (i = 0; kretprobe_blacklist[i].name != NULL; i++) {
			kprobe_lookup_name(kretprobe_blacklist[i].name,
					   kretprobe_blacklist[i].addr);
			if (!kretprobe_blacklist[i].addr)
				printk("kretprobe: lookup failed: %s\n",
				       kretprobe_blacklist[i].name);
		}
	}
繼續往下分析register_kretprobe註冊函數:

	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
	}
此處只指定了kprobe的pre_handler回調函數爲pre_handler_kretprobe;然後若用戶沒有指定最大並行探測數maxactive,這裏會計算並設置一個默認的值。

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

	rp->nmissed = 0;
	/* Establish function entry probe point */
	ret = register_kprobe(&rp->kp);
	if (ret != 0)
		free_rp_inst(rp);
	return ret;
}
接下來根據maxactive的值,爲各個kretprobe_instance實例分配內存並將它們鏈接到kretprobe的free_instances鏈表中,最後調用register_kprobe函數註冊內嵌的kprobe。

以上就是kretprobe的註冊流程,可見它同jprobe一樣也非常的簡單,最終依賴的依然是kprobe機制。


2.3、觸發kretprobe探測

基於kprobe機制,在執行到指定的被探測函數後,會觸發CPU異常,進入kprobe探測流程。首先由kprobe_handler函數調用pre_handler回調函數,此處爲pre_handler_kretprobe函數,該函數首先找到一個空閒的kretprobe_instance探測實例並將它和當前進程綁定,然後調用entry_handler回調函數,接着保存並替換被探測函數的返回地址,最後kprobe探測流程結束並回到正常的執行流程執行被探測函數,在函數返回後將跳轉到被替換的kretprobe_trampoline,該函數會獲取被探測函數的寄存器信息並調用用戶定義的回調函數輸出其中的返回值,最後函數返回正常的執行流程。


圖2 kretprobe觸發流程

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

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

		arch_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;
}
首先根據當前的進程描述符地址以及KPROBE_HASH_BITS值計算出hash索引值,如果kretprobe的free_instances鏈表不爲空,則從中找到一個空閒的kretprobe_instance實例,然後對其中的rp和task字段賦值,表示將該探測實例和當前進程綁定;然後調用entry_handler回調函數(前文kretprobe_example示例程序中的entry_handler函數在此被調用);接下來調用arch_prepare_kretprobe函數,該函數架構相關,用於保存並替換regs中的返回地址,其中arm架構的實現如下:

void __kprobes arch_prepare_kretprobe(struct kretprobe_instance *ri,
				      struct pt_regs *regs)
{
	ri->ret_addr = (kprobe_opcode_t *)regs->ARM_lr;

	/* Replace the return addr with trampoline addr. */
	regs->ARM_lr = (unsigned long)&kretprobe_trampoline;
}
這裏將regs->ARM_lr保存到了ri->ret_addr中,然後原始值被替換成了kretprobe_trampoline函數的地址。注意regs->ARM_lr值的含義是原始代碼流程調用被探測函數後的下一條指令的地址(由於regs中指向的是執行被探測函數入口指令時所保存的寄存器值,因此lr寄存器中的內容爲執行被探測函數的返回地址),經過這一替換,原始執行流程在執行完整個被探測函數後將跳轉到kretprobe_trampoline函數執行,整個函數稍後分析。在來看x86_64架構的函數實現:

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;
}
整體大同小異,x86_64架構的函數調用棧同arm的不同,它將返回地址保存在棧頂空間中(即sp指向的位置),因此保存和替換的方式同arm架構略有不同。

繼續回到pre_handler_kretprobe函數中,接下來將本此使用的kretprobe_instance鏈接到全局kretprobe_inst_table哈希表中,該哈希表在init_kprobes中初始化。最後如果kretprobe的free_instances鏈表爲空,則說明被探測函數的並行觸發流程超過了指定的maxactive上限,僅增加nmissed值不進行探測跟蹤。

pre_handler_kretprobe函數返回後,kprobe流程接着執行singlestep流程並返回到正常的執行流程,被探測函數(do_fork)繼續執行,直到它執行完畢並返回。由於返回地址被替換爲kretprobe_trampoline,所以跳轉到kretprobe_trampoline執行,該函數架構相關且有嵌入彙編實現,具體分析一下。

1)arm架構實現:

/*
 * When a retprobed function returns, trampoline_handler() is called,
 * calling the kretprobe's handler. We construct a struct pt_regs to
 * give a view of registers r0-r11 to the user return-handler.  This is
 * not a complete pt_regs structure, but that should be plenty sufficient
 * for kretprobe handlers which should normally be interested in r0 only
 * anyway.
 */
void __naked __kprobes kretprobe_trampoline(void)
{
	__asm__ __volatile__ (
		"stmdb	sp!, {r0 - r11}		\n\t"
		"mov	r0, sp			\n\t"
		"bl	trampoline_handler	\n\t"
		"mov	lr, r0			\n\t"
		"ldmia	sp!, {r0 - r11}		\n\t"
#ifdef CONFIG_THUMB2_KERNEL
		"bx	lr			\n\t"
#else
		"mov	pc, lr			\n\t"
#endif
		: : : "memory");
}
該函數在棧空間構造出一個不完整的pt_regs結構體變量,僅僅填充了r0~r11寄存器(由於kretprobe所關注的僅是函數返回值r0,這已經足夠了),然後跳轉到trampoline_handler函數執行:

/* Called from kretprobe_trampoline */
static __used __kprobes 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;

	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
	 * a return probe installed on them, and/or more than one return
	 * probe was registered for a target function.
	 *
	 * We can handle this because:
	 *     - instances are always inserted at the head of the list
	 *     - when multiple return probes are registered for the same
	 *       function, the first instance's ret_addr will point to 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;

		if (ri->rp && ri->rp->handler) {
			__this_cpu_write(current_kprobe, &ri->rp->kp);
			get_kprobe_ctlblk()->kprobe_status = KPROBE_HIT_ACTIVE;
			ri->rp->handler(ri, regs);
			__this_cpu_write(current_kprobe, NULL);
		}

		orig_ret_address = (unsigned long)ri->ret_addr;
		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_assert(ri, orig_ret_address, trampoline_address);
	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;
}
由於前面的kprobe執行流程已經完全退出了,因此這裏無法通過傳參的手段來獲取所觸發的到底是哪一個kretprobe_instance,所以只能通過前面的全局kretprobe_inst_table哈希表和current進程描述符指針來確定kretprobe_instance實例。所以函數首先遍歷kretprobe_inst_table哈希表,找到和當前進程綁定的kretprobe_instance。找到了以後會臨時修改current_kprobe的值和kprobe的狀態值,表明又進入了kprobe的處理流程,防止衝突。接着調用handler回調函數,傳入的第二個入參就是前面kretprobe_trampoline函數構造出來的pt_regs,注意其中的r0寄存器保存的是函數的返回值。

handler回調函數執行完畢以後,調用recycle_rp_inst函數將當前的kretprobe_instance實例從kretprobe_inst_table哈希表釋放,重新鏈入free_instances中,以備後面kretprobe觸發時使用,另外如果kretprobe已經被註銷則將它添加到銷燬表中待銷燬:

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);
}
回到trampoline_handler函數中,接下來有一種情況需要注意,由於此處在查找kretprobe_instance時採用的時遍歷全局哈希表的方法,同時可能會存在多個kretprobe實例同當前進程綁定的情況,因爲在一個被探測函數的調用流程中是可能會調用到其他的被探測函數的,例如下面這種情況:

int b(void)
{
	int ret;
	
	...
	
	return ret;
}

int a(void)
{
	int ret;
	
	ret = b();
	...
	
	return ret;
}
如果對a函數和b函數同時註冊了kretprobe,就會出現多kretprobe_instance綁定同一進程的情況。對於這種多綁定的情況,在處理b函數返回值時可能會錯誤的找到綁定到a函數的kretprobe_instance實例,導致探測出現錯誤。那如何避免這種錯誤?其實在註釋中已經給出說明。這裏採用了一種非常巧妙的方法,首先每次插入kretprobe_inst_table表時都是從頭插入的,在取出的時候也是從頭獲取,類似一個堆棧,其次在循環的最後給出了一個break條件,那就是如果函數的原始返回地址不等於kretprobe_trampoline函數的地址,那就break,不再循環查找下一個kretprobe_instance實例。我們知道在一般的情況下這break條件必然滿足,所以這裏找到的必然是流程上最後一次觸發kretprobe探測的實例。

回到trampoline_handler函數最後遍歷empty_rp銷燬需要釋放的kretprobe_instance實例。最後返回被探測函數的原始返回地址,執行流程再次回到kretprobe_trampoline函數中:

		"mov	lr, r0			\n\t"
		"ldmia	sp!, {r0 - r11}		\n\t"
#ifdef CONFIG_THUMB2_KERNEL
		"bx	lr			\n\t"
#else
		"mov	pc, lr			
接下來從r0寄存器中取出原始的返回地址,然後恢復原始函數調用棧空間,最後跳轉到原始返回地址執行,至此函數調用的流程就回歸正常流程了,整個kretprobe探測結束。

2)x86_64架構實現

/*
 * When a retprobed function returns, this code saves registers and
 * calls trampoline_handler() runs, which calls the kretprobe's handler.
 */
static void __used kretprobe_trampoline_holder(void)
{
	asm volatile (
			".global kretprobe_trampoline\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");
}
實現的原理同arm是一致的,這裏會調用SAVE_REGS_STRING把寄存器壓棧,構造出pt_regs變量,然後調用trampoline_handler函數,這個函數基本同arm的一模一樣,就不貼了,最後kretprobe_trampoline_holder恢復棧空間和原始返回地址,跳轉到正常的執行流程中繼續執行。

3、總結

kretprobe探測技術基於kprobe實現,是kprobes探測技術中的最後一種,內核開發人員可以通過它來動態的探測函數執行的返回值,並且也可以做一些定製話的動作,例如檢測函數的執行時間等,使用非常方便。本文介紹了kretprobe探測工具的使用方式及其原理,並通過源碼分析了arm架構和x86_64架構下它的實現方式。

最後,本文連同前兩篇博文較爲詳細的分析了kprobes的三種函數探測技術,靈活的使用這三種調試技術能夠大大的提高內核開發與問題定位的效率。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章