博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Linux内核调试技术——jprobe使用与实现
阅读量:5037 次
发布时间:2019-06-12

本文共 15001 字,大约阅读时间需要 50 分钟。

前一篇博文介绍了kprobes的原理与kprobe的使用与实现方式,本文介绍kprobes中的另外一种探測技术jprobe。它基于kprobe实现,不能在函数的任何位置插入探測点,仅仅能在函数的入口处探測。一般用于监測函数的入參值。本文首先通过一个简单的演示样例介绍jprobe的使用方式。然后通过源代码具体分析jprobe的实现流程。

内核源代码:Linux-4.1.x

实验环境:Fedora25(x86_64)、树莓派1b

1、jprobe使用实例

使用jprobe探測函数的入參值,须要编写内核模块。同kprobe一样,内核相同提供了jprobe的实例程序jprobe_example.c(位于sample/kprobes文件夹),该程序实现了探測do_fork函数入參的功能,用户能够以它为模板来探測其它函数(当然不是说什么函数都能探測的。限制同kprobe一样,另外须要注意的是一个被探測函数仅仅能注冊一个jprobe)。

在分析jprobe_example.c之前先熟悉一下jprobe的基本结构与API接口。

1.1、jprobe结构体与API介绍

struct jprobe结构体定义例如以下:

/* * Special probe type that uses setjmp-longjmp type tricks to resume * execution at a specified entry with a matching prototype corresponding * to the probed function - a trick to enable arguments to become * accessible seamlessly by probe handling logic. * Note: * Because of the way compilers allocate stack space for local variables * etc upfront, regardless of sub-scopes within a function, this mirroring * principle currently works only for probes placed on function entry points. */struct jprobe {	struct kprobe kp;	void *entry;	/* probe handling code to jump to */};
该结构很的简单,仅包括了一个kprobe结构(由于它是基于kprobe实现的)和一个entry指针。它保存的是探測点运行回调函数的地址。当触发调用被探測函数时。保存到该指针的地址会作为目标地址跳转运行(probe handling code to jump to),因此用户指定的探測函数得以运行。

相关的API例如以下:

int register_jprobe(struct jprobe *jp)      //向内核注冊jprobe探測点  void unregister_jprobe(struct jprobe *jp)   //卸载jprobe探測点  int register_jprobes(struct jprobe **jps, int num)     //注冊探測函数向量,包括多个不同探測点  void unregister_jprobes(struct jprobe **jps, int num)  //卸载探測函数向量,包括多个不同探測点  int disable_jprobe(struct jprobe *jp)       //暂时暂停指定探測点的探測  int enable_jprobe(struct jprobe *jp)        //恢复指定探測点的探測

1.2、演示样例jprobe_example分析与演示

同kprobe_example.c一样,该演示样例程序仍以do_fork作为被探測函数进行探測。

当创建进程时,探測函数会调用它打印出do_fork函数的入參值。以下具体分析:

static struct jprobe my_jprobe = {	.entry			= jdo_fork,	.kp = {		.symbol_name	= "do_fork",	},};static int __init jprobe_init(void){	int ret;	ret = register_jprobe(&my_jprobe);	if (ret < 0) {		printk(KERN_INFO "register_jprobe failed, returned %d\n", ret);		return -1;	}	printk(KERN_INFO "Planted jprobe at %p, handler addr %p\n",	       my_jprobe.kp.addr, my_jprobe.entry);	return 0;}static void __exit jprobe_exit(void){	unregister_jprobe(&my_jprobe);	printk(KERN_INFO "jprobe at %p unregistered\n", my_jprobe.kp.addr);}
程序定义了一个struct jprobe实例my_jprobe,指定被探測函数的名字是do_fork(能够改动它以达到探測其它函数的目的),然后探測回调函数为jdo_fork。在模块的初始化函数中,调用register_jprobe函数向kprobe子系统注冊my_jprobe,这样jprobe探測默认就启用了,最后在exit函数中调用unregister_jprobe函数卸载。

/* Proxy routine having the same arguments as actual do_fork() routine */static long jdo_fork(unsigned long clone_flags, unsigned long stack_start,	      unsigned long stack_size, int __user *parent_tidptr,	      int __user *child_tidptr){	pr_info("jprobe: clone_flags = 0x%lx, stack_start = 0x%lx "		"stack_size = 0x%lx\n", clone_flags, stack_start, stack_size);	/* Always end with a call to jprobe_return(). */	jprobe_return();	return 0;}
jdo_fork函数也只打印出了在调用do_fork函数时传入的clone_flags、stack_start和stack_size这三个入參值,整个实现很easy直观。可是有两点须要注意:

1)探測回调函数的入參必须同被探測函数的一致。否则无法达到探測函数入參的目的,比如此处的jdo_fork函数入參unsigned long clone_flags、unsigned long stack_start、unsigned long stack_size、int __user *parent_tidptr和int __user *child_tidptr同do_fork函数是全然一致的(注意返回值固定为long类型)。

2)在回调函数运行完成以后,必须调用jprobe_return函数(凝视中也有强调)。否则运行流程就回不到正常的运行流程中了。这一点后文会具体分析。

以下在x86_64环境下演示该程序的实际效果(环境配置请參考前一篇博文):

<6>[15817.544375] jprobe: clone_flags = 0x1200011, stack_start = 0x0 stack_size = 0x0

<6>[15817.551217] jprobe: clone_flags = 0x1200011, stack_start = 0x0 stack_size = 0x0
<6>[15817.905328] jprobe: clone_flags = 0x1200011, stack_start = 0x0 stack_size = 0x0
<6>[15822.684688] jprobe: clone_flags = 0x1200011, stack_start = 0x0 stack_size = 0x0
<6>[15822.704001] jprobe: clone_flags = 0x1200011, stack_start = 0x0 stack_size = 0x0

在载入jprobe_example.ko模块以后,在终端随便敲几个命令触发进程创建,内核打印出以上message,能够看到do_fork的入參就被很easy的获取到了,其它函数的探測也类似,不再具体描写叙述。

2、jprobe实现分析

jpeobe的实现基于kprobe,因此这里将在前一篇博文《》的基础之上分析它的实现,述主要包含jprobe注冊流程和触发探測流程,涉及kprobe的部分不再具体描。

2.1、jprobe实现原理

利用kprobe,jprobe是一种特殊形式的kprobe,它有自己的pre_handler和break_handler回调函数。当中pre_handler回调函数负责保存原始调用上下文并为调用用户指定的探測函数jprobe->entry准备环境。然后跳转到jprobe->entry运行(被探測函数的入參信息在此得到输出)。接着再次触发kprobe流程,在break_handler函数中恢复原始上下文,最后返回正常运行流程。

2.2、注冊一个jprobe实例

jprobe探測模块调用register_jprobe函数向内核注冊一个jprobe实例,代码路径kernel/kprobes.c,其主要流程例如以下图:

图1 jpobe注冊流程

int register_jprobe(struct jprobe *jp){	return register_jprobes(&jp, 1);}EXPORT_SYMBOL_GPL(register_jprobe);

register_jprobe函数仅仅是register_jprobes的一个封装。主要注冊功能由register_jprobes函数完毕。

int register_jprobes(struct jprobe **jps, int num){	struct jprobe *jp;	int ret = 0, i;	if (num <= 0)		return -EINVAL;	for (i = 0; i < num; i++) {		unsigned long addr, offset;		jp = jps[i];		addr = arch_deref_entry_point(jp->entry);		/* Verify probepoint is a function entry point */		if (kallsyms_lookup_size_offset(addr, NULL, &offset) &&		    offset == 0) {			jp->kp.pre_handler = setjmp_pre_handler;			jp->kp.break_handler = longjmp_break_handler;			ret = register_kprobe(&jp->kp);		} else			ret = -EINVAL;		if (ret < 0) {			if (i > 0)				unregister_jprobes(jps, i);			break;		}	}	return ret;}EXPORT_SYMBOL_GPL(register_jprobes);

函数是一个循环,对每一个jprobe运行同样的注冊流程,首先从jp->entry中取出探測回调函数的地址,对它进行验证。

kallsyms_lookup_size_offset函数的作用是从内核或者模块的符号表中找到addr地址所在的符号,找到后会通过offset值返回addr与符号起始的偏移,这偏移值必须为0,即必须为一个函数的入口。若条件符合。则设置kprobe的pre_handler和break_handler这两个回调函数setjmp_pre_handler和longjmp_break_handler,最后调用register_kprobe函数注冊kprobe。

可见jprobe的注冊流程很的简单。它的本质就是注冊一个kprobe,利用kprobe机制实现探測,仅仅是探測回调函数并不是用户自定义。使用jprobe私有的而已。在注冊完毕后,jprobe(kprobe)机制启动。当函数调用流程运行到被探測函数时就会触发jprobe(kprobe)探測。

最后须要注意的是,jprobe是不能在同一个被探測点注冊多个的,在kprobe的注冊流程register_kprobe->register_aggr_kprobe->add_new_kprobe中会有推断:

if (p->break_handler) {		if (ap->break_handler)			return -EEXIST;

2.3、触发jprobe探測

基于kprobe机制,在运行到被探測函数后。会触发CPU异常,依照kprobe的运行流程。由kprobe_handler函数调用到pre_handler回调函数。即setjmp_pre_handler。

该函数架构相关。它依据架构的不同进行一些栈或者寄存器相关的操作,保存现场以备调用结束后恢复,随后跳转到用户定的jprobe->entry处运行。在打印出用户须要的信息后,返回原有正常的流程继续运行。主要流程例如以下图:

图2 jprobe触发流程

2.3.1、arm架构实现

int __kprobes setjmp_pre_handler(struct kprobe *p, struct pt_regs *regs){	struct jprobe *jp = container_of(p, struct jprobe, kp);	struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();	long sp_addr = regs->ARM_sp;	long cpsr;	kcb->jprobe_saved_regs = *regs;	memcpy(kcb->jprobes_stack, (void *)sp_addr, MIN_STACK_SIZE(sp_addr));	regs->ARM_pc = (long)jp->entry;	cpsr = regs->ARM_cpsr | PSR_I_BIT;#ifdef CONFIG_THUMB2_KERNEL	/* Set correct Thumb state in cpsr */	if (regs->ARM_pc & 1)		cpsr |= PSR_T_BIT;	else		cpsr &= ~PSR_T_BIT;#endif	regs->ARM_cpsr = cpsr;	preempt_disable();	return 1;}
首先再次明白入參struct pt_regs *regs的含义是触发CPU异常前所保存的正常运行流上下文的寄存器值。函数首先获取触发的jprobe结构实例,并调用get_kprobe_ctlblk取得当前CPU的kprobe_ctlblk结构全局变量。这个struct kprobe_ctlblk结构定义在kprobe分析中已经见过,只是jprobe使用到了当中定义的另两个字段:

/* per-cpu kprobe control block */struct kprobe_ctlblk {	unsigned int kprobe_status;	struct prev_kprobe prev_kprobe;	struct pt_regs jprobe_saved_regs;	char jprobes_stack[MAX_STACK_SIZE];};
当中jprobe_saved_regs用于保存寄存器信息,jprobes_stack则用于保存栈信息,它们用于在jprobe返回时恢复调用探測前的上下文,这一点从setjmp_pre_handler函数的前两行就能够看出。先提个问题,为何kprobe不须要保存原上下文信息而jprobe须要?

函数接下来改动传入的ARM_pc值为用户指定的探測回调函数地址,注意这个值本来在正常的kprobe流程中是要被设置为正常流程的下一条指令的(运行完kprobe流程后就会回到原流程继续运行)。这里在kprobe的整个流程结束后就不会回到原流程运行了。而是会进入到用户指定的探測函数运行。

函数然后改动入參的CPSR寄存器值,置位PSR_I_BIT,表示禁用中断,最后禁止抢占并返回1。回到kprobe_handler函数中看返回1后接下来kprobe就不会运行singlestep和调用post_handler回调函数了。注意也不会调用reset_current_kprobe函数复位当前运行的kprobe为NULL:

if (!p->pre_handler || !p->pre_handler(p, regs)) {				kcb->kprobe_status = KPROBE_HIT_SS;				singlestep(p, regs, kcb);				if (p->post_handler) {					kcb->kprobe_status = KPROBE_HIT_SSDONE;					p->post_handler(p, regs, 0);				}				reset_current_kprobe();			}
在kprobe_handler流程返回后,运行流程进入到了用户指定的探測函数运行,对于前文中的jprobe_example程序来说就是jdo_fork函数。提第二个问题,被探測函数的入參值是怎样获取的?

从setjmp_pre_handler的实现能够看出。该函数只改动了kprobe的返回地址,并没有改动栈和其它的寄存器值。因此在CPU跳转到jdo_fork运行时,它的寄存器和栈中的内容同原本调用do_fork函数时差点儿是一模一样的(不过禁用了中断而已),因此不论是通过寄存器传參还是通过压栈的方式传參。用户在定义jdo_fork函数时只须要将函数入參定义的同do_fork一样就能够轻轻松松的获取到原有的入參值了。另外从这里的实现能够看出另外一个信息。jprobe的回调运行上下文同原函数运行的上下文是一样的,这点不同于kprobe,kprobe的回调函数运行的上下文是在CPU异常的中断上下文。

最后因为探測函数(jdo_fork)是在kprobe_handler流程运行完毕后跳转运行的,跳过了single_step流程。这也就说它不能利用原有kprobe的机制回到原始运行流程中去运行,须要另想他法,事实上在setjmp_pre_handler函数中保存的寄存器pt_regs就是用于这个目的的。也就解释了前文中提出的第一个问题,接下来具体分析。

回到探測函数jdo_fork中,用户在获取须要的信息后,接下来进入现场恢复的流程,当中的关键部分就是jdo_fork函数最后调用的jprobe_return函数,它是由嵌入汇编实现的

void __kprobes jprobe_return(void){	struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();	__asm__ __volatile__ (		/*		 * Setup an empty pt_regs. Fill SP and PC fields as		 * they're needed by longjmp_break_handler.		 *		 * We allocate some slack between the original SP and start of		 * our fabricated regs. To be precise we want to have worst case		 * covered which is STMFD with all 16 regs so we allocate 2 *		 * sizeof(struct_pt_regs)).		 *		 * This is to prevent any simulated instruction from writing		 * over the regs when they are accessing the stack.		 */#ifdef CONFIG_THUMB2_KERNEL		...#else		"sub    sp, %0, %1		\n\t"#endif		"ldr    r0, ="__stringify(JPROBE_MAGIC_ADDR)"\n\t"		"str    %0, [sp, %2]		\n\t"		"str    r0, [sp, %3]		\n\t"		"mov    r0, sp			\n\t"		"bl     kprobe_handler		\n\t"		/*		 * Return to the context saved by setjmp_pre_handler		 * and restored by longjmp_break_handler.		 */#ifdef CONFIG_THUMB2_KERNEL		...#else		"ldr	r0, [sp, %4]		\n\t"		"msr	cpsr_cxsf, r0		\n\t"		"ldmia	sp, {r0 - pc}		\n\t"#endif		:		: "r" (kcb->jprobe_saved_regs.ARM_sp),		  "I" (sizeof(struct pt_regs) * 2),		  "J" (offsetof(struct pt_regs, ARM_sp)),		  "J" (offsetof(struct pt_regs, ARM_pc)),		  "J" (offsetof(struct pt_regs, ARM_cpsr)),		  "J" (offsetof(struct pt_regs, ARM_lr))		: "memory", "cc");}
这里模拟出了一个假的pt_regs结构体。只填充了当中的sp和pc字段(后文中的longjmp_break_handler函数须要),当中pc的值为JPROBE_MAGIC_ADDR,然后长跳转到kprobe_handler执行,kprobe_handler函数推断当前已经有kprobe正在执行了。因此进入下面调用流程:

} else if (cur) {		/* We probably hit a jprobe.  Call its break handler. */		if (cur->break_handler && cur->break_handler(cur, regs)) {			kcb->kprobe_status = KPROBE_HIT_SS;			singlestep(cur, regs, kcb);			if (cur->post_handler) {				kcb->kprobe_status = KPROBE_HIT_SSDONE;				cur->post_handler(cur, regs, 0);			}		}		reset_current_kprobe();
首先调用kprobe的break_handler回调函数,即longjmp_break_handler函数:

int __kprobes longjmp_break_handler(struct kprobe *p, struct pt_regs *regs){	struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();	long stack_addr = kcb->jprobe_saved_regs.ARM_sp;	long orig_sp = regs->ARM_sp;	struct jprobe *jp = container_of(p, struct jprobe, kp);	if (regs->ARM_pc == JPROBE_MAGIC_ADDR) {		if (orig_sp != stack_addr) {			struct pt_regs *saved_regs =				(struct pt_regs *)kcb->jprobe_saved_regs.ARM_sp;			printk("current sp %lx does not match saved sp %lx\n",			       orig_sp, stack_addr);			printk("Saved registers for jprobe %p\n", jp);			show_regs(saved_regs);			printk("Current registers\n");			show_regs(regs);			BUG();		}		*regs = kcb->jprobe_saved_regs;		memcpy((void *)stack_addr, kcb->jprobes_stack,		       MIN_STACK_SIZE(stack_addr));		preempt_enable_no_resched();		return 1;	}	return 0;}
这个函数非常easy,首先会推断sp的值和保存的sp值是都是一样的。若不一样则报BUG,否则恢复保存在kprobe_ctlblk结构体中的寄存器值和栈,最后启用内核抢占。至此jprobe的处理流程所有完毕,接下来会回到kprobe的kprobe_handler中继续完毕本次kprobe,运行单步运行single_step和post_handler,最后回到原流程运行。

由此可见。在用户定义的探測函数末尾。必需要调用jprobe_return函数。否则代码的运行就“飞了”。再也回不到原有的流程中去了。

2.3.2、x86_64架构实现

int setjmp_pre_handler(struct kprobe *p, struct pt_regs *regs){	struct jprobe *jp = container_of(p, struct jprobe, kp);	unsigned long addr;	struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();	kcb->jprobe_saved_regs = *regs;	kcb->jprobe_saved_sp = stack_addr(regs);	addr = (unsigned long)(kcb->jprobe_saved_sp);	/*	 * As Linus pointed out, gcc assumes that the callee	 * owns the argument space and could overwrite it, e.g.	 * tailcall optimization. So, to be absolutely safe	 * we also save and restore enough stack bytes to cover	 * the argument area.	 */	memcpy(kcb->jprobes_stack, (kprobe_opcode_t *)addr,	       MIN_STACK_SIZE(addr));	regs->flags &= ~X86_EFLAGS_IF;	trace_hardirqs_off();	regs->ip = (unsigned long)(jp->entry);	/*	 * jprobes use jprobe_return() which skips the normal return	 * path of the function, and this messes up the accounting of the	 * function graph tracer to get messed up.	 *	 * Pause function graph tracing while performing the jprobe function.	 */	pause_graph_tracing();	return 1;}NOKPROBE_SYMBOL(setjmp_pre_handler);
x86_64架构的实现总体同arm的大同小异,函数首先相同是保存现场。然后关闭中断并设置IP寄存器的值为jp->entry,最后返回1,这样在kprobe_int3_handler函数会跳过single_step。

/*			 * If we have no pre-handler or it returned 0, we			 * continue with normal processing.  If we have a			 * pre-handler and it returned non-zero, it prepped			 * for calling the break_handler below on re-entry			 * for jprobe processing, so get out doing nothing			 * more here.			 */			if (!p->pre_handler || !p->pre_handler(p, regs))				setup_singlestep(p, regs, kcb, 0);			return 1;
于是在kprobe调用流程结束后跳转到用户的探測函数运行。在来看jprobe_return函数的实现:

void jprobe_return(void){	struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();	asm volatile (#ifdef CONFIG_X86_64			"       xchg   %%rbx,%%rsp	\n"#else			"       xchgl   %%ebx,%%esp	\n"#endif			"       int3			\n"			"       .globl jprobe_return_end\n"			"       jprobe_return_end:	\n"			"       nop			\n"::"b"			(kcb->jprobe_saved_sp):"memory");}
同arm的实现不同,这里使用int3指令再次触发CPU3异常,而且异常出的地址已经不再是BREAKPOINT_INSTRUCTION了,所以会进入到kprobe_int3_handler的下面流程运行:

} else if (kprobe_running()) {		p = __this_cpu_read(current_kprobe);		if (p->break_handler && p->break_handler(p, regs)) {			if (!skip_singlestep(p, regs, kcb))				setup_singlestep(p, regs, kcb, 0);			return 1;		}
相同是调用kprobe的break_handler回调函数运行,也即是longjmp_break_handler函数。

int longjmp_break_handler(struct kprobe *p, struct pt_regs *regs){	struct kprobe_ctlblk *kcb = get_kprobe_ctlblk();	u8 *addr = (u8 *) (regs->ip - 1);	struct jprobe *jp = container_of(p, struct jprobe, kp);	void *saved_sp = kcb->jprobe_saved_sp;	if ((addr > (u8 *) jprobe_return) &&	    (addr < (u8 *) jprobe_return_end)) {		if (stack_addr(regs) != saved_sp) {			struct pt_regs *saved_regs = &kcb->jprobe_saved_regs;			printk(KERN_ERR			       "current sp %p does not match saved sp %p\n",			       stack_addr(regs), saved_sp);			printk(KERN_ERR "Saved registers for jprobe %p\n", jp);			show_regs(saved_regs);			printk(KERN_ERR "Current registers\n");			show_regs(regs);			BUG();		}		/* It's OK to start function graph tracing again */		unpause_graph_tracing();		*regs = kcb->jprobe_saved_regs;		memcpy(saved_sp, kcb->jprobes_stack, MIN_STACK_SIZE(saved_sp));		preempt_enable_no_resched();		return 1;	}	return 0;}
longjmp_break_handler函数同arm实现基本一致,恢复代码的原有上下文。打开内核抢占。最后交回给kprobe继续运行后面的single_step和恢复流程。只是值的注意的是第一条推断语句。因为本次int3异常是在jprobe_return函数中触发的。因此longjmp_break_handler函数的struct pt_regs *regs入參值是在调用jprobe_return函数环境上下文中的寄存器值。因此addr一定是在jprobe_return函数的地址范围内,所以以此推断本次调用的有效性,防止误入。

3、总结

jprobe探測技术基于kprobe实现,是kprobes三种探測技术中的另外一种。内核开发者能够用它来探測内核函数的调用以及调用时的入參值,使用很方便。本文介绍了jprobe探測工具的使用方式及其原理,并通过源代码分析了arm架构和x86_64架构下它的实现方式。下一篇博文将介绍kprobes中的最后一种用来探測函数返回值的kretprobe探測技术。

转载于:https://www.cnblogs.com/jzssuanfa/p/7373811.html

你可能感兴趣的文章
hdoj:2061
查看>>
Oarcle 入门之like关键字
查看>>
Entity Framework Code First (七)空间数据类型 Spatial Data Types
查看>>
2012年度读写Excel文件的最佳PHP类库收集
查看>>
读过的书
查看>>
ll 详解
查看>>
form表单中的label标签
查看>>
eclipse下解决明明有jar包,却找不到的问题
查看>>
Entity Framework 学习初级篇1--EF基本概况(入门)
查看>>
C Looooops
查看>>
bzoj 2226 LCMSum 欧拉函数
查看>>
JavaSript模块规范 - AMD规范与CMD规范介绍
查看>>
都市环游
查看>>
【工具】【截图工具】FScapture,支持滚动
查看>>
jQuery延迟加载(懒加载)插件 – jquery.lazyload.js
查看>>
人脸检测(1)——HOG特征
查看>>
react native 示例代码
查看>>
关于V1.6.0版本的项目总结
查看>>
想想还要做哪些事。。。
查看>>
python字典顺序转字符串
查看>>