前言
上一节介绍了kprobe的基本概念,下面我们将使用几个具体的例子,看下kprobe在实际使用中有那些应用场景。
kprobe
内核的samples/kprobe目录下有kprobe相关的例子,我们以这些例子为基础,简单修改下。
查看函数的入参
我们所有的例子都是探测do_sys_open() 或者_do_fork(),以下是内核中的源码。
do_sys_open
struct audit_names;struct filename { const char *name; /* pointer to actual string */ const __user char *uptr; /* original userland pointer */ struct audit_names *aname; int refcnt; const char iname[];};long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode){ struct open_flags op; int fd = build_open_flags(flags, mode, &op); struct filename *tmp; if (fd) return fd; tmp = getname(filename); if (is_err(tmp)) return ptr_err(tmp); fd = get_unused_fd_flags(flags); if (fd >= 0) { struct file *f = do_filp_open(dfd, tmp, &op); if (is_err(f)) { put_unused_fd(fd); fd = ptr_err(f); } else { fsnotify_open(f); fd_install(fd, f); } } putname(tmp); return fd;}
_do_fork
long _do_fork(unsigned long clone_flags, unsigned long stack_start, unsigned long stack_size, int __user *parent_tidptr, int __user *child_tidptr, unsigned long tls){ struct task_struct *p; int trace = 0; long nr; /* * determine whether and which event to report to ptracer. when * called from kernel_thread or clone_untraced is explicitly * requested, no event is reported; otherwise, report if the event * for the type of forking is enabled. */ if (!(clone_flags & clone_untraced)) { if (clone_flags & clone_vfork) trace = ptrace_event_vfork; else if ((clone_flags & csignal) != sigchld) trace = ptrace_event_clone; else trace = ptrace_event_fork; if (likely(!ptrace_event_enabled(current, trace))) trace = 0; } p = copy_process(clone_flags, stack_start, stack_size, child_tidptr, null, trace, tls, numa_no_node); /* * do this prior waking up the new thread - the thread pointer * might get invalid after that point, if the thread exits quickly. */ if (!is_err(p)) { struct completion vfork; struct pid *pid; cpufreq_task_times_alloc(p); trace_sched_process_fork(current, p); pid = get_task_pid(p, pidtype_pid); nr = pid_vnr(pid); if (clone_flags & clone_parent_settid) put_user(nr, parent_tidptr); if (clone_flags & clone_vfork) { p->vfork_done = &vfork; init_completion(&vfork); get_task_struct(p); } wake_up_new_task(p); /* forking complete and child started to run, tell ptracer */ if (unlikely(trace)) ptrace_event_pid(trace, pid); if (clone_flags & clone_vfork) { if (!wait_for_vfork_done(p, &vfork)) ptrace_event_pid(ptrace_event_vfork_done, pid); } put_pid(pid); } else { nr = ptr_err(p); } return nr;}
实际调试中经常需要调查函数使用的变量的值。要在kprobes的侦测器内显示某个函数的局部变量的值,需要一些技巧,原因是在printk的参数中无法直接指定变量名,因此必须给侦测器函数提供一个pt_regs结构,其中保存了指定地址的命令执行时的寄存器信息。
当然,不同架构下该结构的成员变量不尽相同,但用该结构可以显示变量等更为详细的信息。
arm64,arm32,x86的寄存器及其访问方式可以看文末的目录
kprobe_example.c
/* * note: this example is works on x86 and powerpc. * here's a sample kernel module showing the use of kprobes to dump a * stack trace and selected registers when _do_fork() is called. * * for more information on theory of operation of kprobes, see * documentation/kprobes.txt * * you will see the trace data in /var/log/messages and on the console * whenever _do_fork() is invoked to create a new process. */#include #include #include #define trace_symbol do_filp_open/* for each probe you need to allocate a kprobe structure */static struct kprobe kp = { .symbol_name = trace_symbol,};/* x86_64中寄存器中参数的顺序: rdi rsi rdx rcx r8 r9*//* aarch64: x0-x7 对应参数 *//* kprobe pre_handler: called just before the probed instruction is executed */static int handler_pre(struct kprobe *p, struct pt_regs *regs){ int dfd = -1; struct filename *filename = null;#ifdef config_x86 dfd = regs->di; filename = (struct filename *) regs->si;#endif#ifdef config_arm64 dfd = regs->regs[0]; filename = (struct filename *) regs->regs[1];#endif if (filename && !(strcmp(filename->name, testfile))) printk(kern_info handler_pre:%s: dfd=%d, name=%s, p->symbol_name, dfd, filename->name); 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){ //printk(kern_info handler_post);}/* * 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){ /*printk(kern_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 symbol_name 函数名字 ,假设我们期望获取 _do_fork函数变量 nr 的值:
将vmlinux进行反汇编,找出_do_fork的地址。
aarch64-linux-gnu-objdump -s -d vmlinux > vmlinux.asm
_do_fork 反汇编如下所示,地址为ffffff80080ba83c。
ffffff80080ba83c :ffffff80080ba83c: a9b97bfd stp x29, x30, [sp, #-112]!ffffff80080ba840: 910003fd mov x29, spffffff80080ba844: a90153f3 stp x19, x20, [sp, #16]ffffff80080ba848: a9025bf5 stp x21, x22, [sp, #32]ffffff80080ba84c: a90363f7 stp x23, x24, [sp, #48]ffffff80080ba850: aa0003f5 mov x21, x0ffffff80080ba854: aa0103f3 mov x19, x1ffffff80080ba858: aa0203f6 mov x22, x2ffffff80080ba85c: aa0303f7 mov x23, x3ffffff80080ba860: aa0403f8 mov x24, x4ffffff80080ba864: aa1e03e0 mov x0, x30ffffff80080ba868: 97ff4e8a bl ffffff800808e290 ffffff80080ba86c: 37b814f5 tbnz w21, #23, ffffff80080bab08 ffffff80080ba870: 37701495 tbnz w21, #14, ffffff80080bab00 ffffff80080ba874: 92401ea0 and x0, x21, #0xffffffff80080ba878: 52800074 mov w20, #0x3 // #3ffffff80080ba87c: f100441f cmp x0, #0x11ffffff80080ba880: 1a9f1694 csinc w20, w20, wzr, ne // ne = anyffffff80080ba884: 11000e81 add w1, w20, #0x3............................ffffff80080ba91c: b5000fb6 cbnz x22, ffffff80080bab10 ffffff80080ba920: 52800001 mov w1, #0x0 // #0ffffff80080ba924: aa1303e0 mov x0, x19ffffff80080ba928: 94006a17 bl ffffff80080d5184 ffffff80080ba92c: aa0003f6 mov x22, x0ffffff80080ba930: 94006a85 bl ffffff80080d5344 pid_vnr>ffffff80080ba934: 93407c18 sxtw x24, w0ffffff80080ba938: 36a00195 tbz w21, #20, ffffff80080ba968 ffffff80080ba93c: d5384101 mrs x1, sp_el0ffffff80080ba940: f9400422 ldr x2, [x1, #8]ffffff80080ba944: aa1703e1 mov x1, x23ffffff80080ba948: b1001021 adds x1, x1, #0x4
nr 变量 是 函数pid_vnr的返回值(也是子进程的pid) ,根据arm调用规范,调用完成pid_vnr()后,寄存器x0存放的就是其函数返回值。
参考:arm64调用标准 https://blog.51cto.com/u_15333820/3452605
通过反汇编可以知道,pid_vnr在 ffffff80080ba930地址处被调用,因此,侦测器的插入地址就是在ffffff80080ba930之后,并且x0被改变之前。只要符合这两个条件,放在哪里都无所谓。
因此,我们将kprobe的点设置为ffffff80080ba934,然后获取 x0,就能获取变量nr的值。
.offset 是探测点相对于_do_fork的偏移,在注册时指定。我们这里的 offset = ffffff80080ba934 - ffffff80080ba83c = f8。
另外,反汇编能力就是多看汇编以及找到几个关键点(例如常量,跳转语句)就能定位到汇编对应的源码了,这里不再展开了。
/* * note: this example is works on x86 and powerpc. * here's a sample kernel module showing the use of kprobes to dump a * stack trace and selected registers when _do_fork() is called. * * for more information on theory of operation of kprobes, see * documentation/kprobes.txt * * you will see the trace data in /var/log/messages and on the console * whenever _do_fork() is invoked to create a new process. */#include #include #include /* for each probe you need to allocate a kprobe structure */static struct kprobe kp = { .symbol_name = _do_fork, .offset = 0xf8,};/* kprobe pre_handler: called just before the probed instruction is executed */static int handler_pre(struct kprobe *p, struct pt_regs *regs){#ifdef config_x86 printk(kern_info pre_handler: p->addr = 0x%p, ip = %lx, flags = 0x%lx,rax = 0x%lx, p->addr, regs->ip, regs->flags,regs->ax);#endif#ifdef config_arm64 pr_info( pre_handler: p->addr = 0x%p, pc = 0x%lx, pstate = 0x%lx,x0 = 0x%lx, p->symbol_name, p->addr, (long)regs->pc, (long)regs->pstate,(long)regs->regs[0]);#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_x86 printk(kern_info post_handler: p->addr = 0x%p, flags = 0x%lx, p->addr, regs->flags);#endif#ifdef config_arm64 pr_info( post_handler: p->addr = 0x%p, pstate = 0x%lx, 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){ printk(kern_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 addr = 0x0000000050a6c3dd, ip = ffffffffa5ca0009, flags = 0x246,rax = 0x2[ 245.080640] post_handler: p->addr = 0x0000000050a6c3dd, flags = 0x246[ 245.080936] pre_handler: p->addr = 0x0000000050a6c3dd, ip = ffffffffa5ca0009, flags = 0x246,rax = 0x2[ 245.080938] post_handler: p->addr = 0x0000000050a6c3dd, flags = 0x246[ 245.457340] pre_handler: p->addr = 0x0000000050a6c3dd, ip = ffffffffa5ca0009, flags = 0x246,rax = 0x2[ 245.457345] post_handler: p->addr = 0x0000000050a6c3dd, flags = 0x246[ 245.457643] pre_handler: p->addr = 0x0000000050a6c3dd, ip = ffffffffa5ca0009, flags = 0x246,rax = 0x2[ 245.457645] post_handler: p->addr = 0x0000000050a6c3dd, flags = 0x246[ 245.719208] pre_handler: p->addr = 0x0000000050a6c3dd, ip = ffffffffa5ca0009, flags = 0x246,rax = 0x2[ 245.719213] post_handler: p->addr = 0x0000000050a6c3dd, flags = 0x246[ 245.719505] pre_handler: p->addr = 0x0000000050a6c3dd, ip = ffffffffa5ca0009, flags = 0x246,rax = 0x2[ 245.719507] post_handler: p->addr = 0x0000000050a6c3dd, flags = 0x246[ 245.820761] pre_handler: p->addr = 0x0000000050a6c3dd, ip = ffffffffa5ca0009, flags = 0x246,rax = 0x2[ 245.820765] post_handler: p->addr = 0x0000000050a6c3dd, flags = 0x246[ 245.821061] pre_handler: p->addr = 0x0000000050a6c3dd, ip = ffffffffa5ca0009, flags = 0x246,rax = 0x2[ 245.821063] post_handler: p->addr = 0x0000000050a6c3dd, flags = 0x246[ 246.092572] pre_handler: p->addr = 0x0000000050a6c3dd, ip = ffffffffa5ca0009, flags = 0x246,rax = 0x2[ 246.092577] post_handler: p->addr = 0x0000000050a6c3dd, flags = 0x246[ 246.095863] pre_handler: p->addr = 0x0000000050a6c3dd, ip = ffffffffa5ca0009, flags = 0x246,rax = 0x2[ 246.095867] post_handler: p->addr = 0x0000000050a6c3dd, flags = 0x246[ 246.126196] kprobe at 0000000050a6c3dd unregistered
jprobe
与kprobes相比,jprobes能更容易地获取传给函数的参数。有几点需要注意:
处理程序应该有与被探测函数相同的参数列表和返回类型;
返回之前,必须调用jprobe_return()(处理程序实际上从未返回,因为jprobe_return()将控制权返回给kprobes) 。
查看函数的参数
/* * here's a sample kernel module showing the use of jprobes to dump * the arguments of _do_fork(). * * for more information on theory of operation of jprobes, 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 _do_fork() is invoked to create a new process. * (some messages may be suppressed if syslogd is configured to * eliminate duplicate messages.) */#include #include #include /* * jumper probe for _do_fork. * mirror principle enables access to arguments of the probed routine * from the probe handler. *//* proxy routine having the same arguments as actual _do_fork() routine */#define trace_symbol do_filp_open/*与do_filp_open 的参数完全相同*/static struct file * jp_do_filp_open(int dfd, struct filename *pathname, const struct open_flags *op){ if (pathname && !(strcmp(pathname->name, testfile))) printk(kern_info jprobe: dfd = %d, pathname = %s, dfd, pathname->name); /* always end with a call to jprobe_return(). */ jprobe_return(); return 0;}static struct jprobe my_jprobe = { .entry = jp_do_filp_open, .kp = { .symbol_name = trace_symbol, },};static int __init jprobe_init(void){ int ret; ret = register_jprobe(&my_jprobe); if (ret 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, 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 suggests that maxactive was set too low. */ printk(kern_info missed probing %d instances of %s, my_kretprobe.nmissed, my_kretprobe.kp.symbol_name);}module_init(kretprobe_init)module_exit(kretprobe_exit)module_license(gpl);
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 *);
其中我们可以看到 struct kretprobe 结构体中 有struct kprobe成员(kretprobe时基于 kprobe实现的)。handler:用户自定义回调函数,被探测函数返回后被调用,一般在这个函数中获取被探测函数的返回值。
entry_handler:用户自定义回调函数,这是kretprobes 提供了一个可选的用户指定的处理程序,它在函数入口上运行。 每当 kretprobe 放置在函数入口处的 kprobe 被命中时,都会调用用户定义的 entry_handler,如果有的话。 如果 entry_handler 返回 0(成功),则保证在函数返回时调用相应的返回处理程序。 如果 entry_handler 返回非零错误,则 kprobes 将返回地址保持原样,并且 kretprobe 对该特定函数实例没有进一步的影响。
maxactive:被探测函数可以同时活动的最大实例数。来指定可以同时探测多少个指定函数的实例。register_kretprobe() 预分配指定数量的 kretprobe_instance 对象。
nmissed:跟踪被探测函数的返回被忽略的次数(maxactive设置的过低)。
data_size:表示kretprobe私有数据的大小,在注册kretprobe时会根据该大小预留空间。
free_instances :表示空闲的kretprobe运行实例链表,它链接了本kretprobe的空闲实例struct kretprobe_instance结构体表示。
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回调函数之间进行传递(一般用于实现统计被探测函数的执行耗时)。
register_kretprobe
kretprobe探测点的blackpoint,用来表示不支持kretprobe探测的函数的信息。name表示该函数名,addr表示该函数的地址。
struct kretprobe_blackpoint { const char *name; void *addr;};1234
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);123456789
函数的开头首先处理 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 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 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注册完成后就默认启动探测。
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; (1) 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; } (2) 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;}nokprobe_symbol(pre_handler_kretprobe);
entry_handler
struct kretprobe *rprp->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;}
arch_prepare_kretprobe
arch_prepare_kretprobe(ri, regs)该函数架构相关,struct kretprobe_instance结构体 的 ret_addr 成员用于保存并替换regs中的返回地址。返回地址被替换为kretprobe_trampoline。
x86架构
// arch/x86/kernel/kprobes/core.c#define stack_addr(regs) ((unsigned long *)kernel_stack_pointer(regs))// x86_64// arch/x86/include/asm/ptrace.hstatic inline unsigned long kernel_stack_pointer(struct pt_regs *regs){ return regs->sp;}// arch/x86/kernel/kprobes/core.cvoid 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; //用于保存原始被探测函数的返回地址};
arm64架构
// arch/arm64/kernel/probes/kprobes.cvoid __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)。
小结
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中去。
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,};
x86架构
(1)
kretprobe_trampoline -->trampoline_handlerkretprobe_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 .type kretprobe_trampoline, @function kretprobe_trampoline:#ifdef config_x86_64 /* we don't bother saving the ss register */ pushq %rsp pushfq save_regs_string movq %rsp, %rdi call trampoline_handler /* replace saved sp with true return address. */ movq %rax, 152(%rsp) restore_regs_string popfq#else pushf save_regs_string movl %esp, %eax call trampoline_handler /* move flags to cs */ movl 56(%esp), %edx movl %edx, 52(%esp) /* replace saved flags with true return address. */ movl %eax, 56(%esp) restore_regs_string popf#endif ret .size kretprobe_trampoline, .-kretprobe_trampoline);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);12void 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) restore_regs_string popfq1234
arm64架构
(1)
kretprobe_trampoline -->trampoline_probe_handlerkretprobe_trampoline
(2)kretprobe_trampoline
// arch/arm64/kernel/probes/kprobes_trampoline.sentry(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 retendproc(kretprobe_trampoline)
(3)trampoline_probe_handler
// arch/arm64/kernel/probes/kprobes.cvoid __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
编译运行
insmod kprobe_example.kovim testfilermmod kprobe_example.kodmesg
成功打印出函数的执行时间
[ 1056.875938] do_sys_open returned -2 and took 10500 ns to execute[ 1057.567400] do_sys_open returned 34 and took 59208 ns to execute[ 1058.382932] do_sys_open returned 3 and took 31469101 ns to execute[ 1058.567046] do_sys_open returned 34 and took 61250 ns to execute[ 1058.975879] do_sys_open returned 3 and took 224084 ns to execute[ 1058.975935] do_sys_open returned 3 and took 16917 ns to execute[ 1058.976041] do_sys_open returned 3 and took 13417 ns to execute[ 1058.976148] do_sys_open returned 3 and took 15167 ns to execute[ 1058.976254] do_sys_open returned 3 and took 15750 ns to execute[ 1058.976356] do_sys_open returned 3 and took 16042 ns to execute[ 1058.978036] do_sys_open returned -2 and took 23041 ns to execute[ 1058.978074] do_sys_open returned 3 and took 24500 ns to execute[ 1058.978175] do_sys_open returned -2 and took 9334 ns to execute[ 1058.978211] do_sys_open returned 3 and took 23333 ns to execute[ 1058.978246] do_sys_open returned 3 and took 13417 ns to execute[ 1058.978286] do_sys_open returned 3 and took 14583 ns to execute[ 1058.989701] kretprobe at ffffff80081ed6c8 unregistered[ 1058.989709] missed probing 0 instances of do_sys_open
kprobe-based event tracing
这些事件类似于基于tracepoint的事件。与tracepoint不同,它是基于kprobes(kprobe和kretprobe)的。所以它可以探测任何kprobes可以探测的地方。与基于tracepoint的事件不同的是,它可以动态地添加和删除。
要启用这个功能,在编译内核时config_kprobe_events=y
与 event tracing类似,这不需要通过current_tracer来激活。可以通过/sys/kernel/debug/tracing/kprobe_events添加探测点,并通过/sys/kernel/debug/tracing/events/kprobes//enable来启用它。
你也可以使用/sys/kernel/debug/tracing/dynamic_events,而不是kprobe_events。该接口也将提供对其他动态事件的统一访问。
synopsis of kprobe_events
kprobe和内核的ftrac结合使用,需要对内核进行配置,然后添加探测点、进行探测、查看结果。
kprobe配置
config_kprobes=yconfig_optprobes=yconfig_kprobes_on_ftrace=yconfig_uprobes=yconfig_kretprobes=yconfig_have_kprobes=yconfig_have_kretprobes=yconfig_have_optprobes=yconfig_have_kprobes_on_ftrace=yconfig_kprobe_event=y
kprobe trace events使用
kprobe事件相关的节点有如下:
/sys/kernel/debug/tracing/kprobe_events-----------------------配置kprobe事件属性,增加事件之后会在kprobes下面生成对应目录。/sys/kernel/debug/tracing/kprobe_profile----------------------kprobe事件统计属性文件。/sys/kernel/debug/tracing/kprobes///enabled-------使能kprobe事件/sys/kernel/debug/tracing/kprobes///filter--------过滤kprobe事件/sys/kernel/debug/tracing/kprobes///format--------查询kprobe事件显示格式
kprobe事件配置
新增一个kprobe事件,通过写kprobe_events来设置。
p[:[grp/]event] [mod:]sym[+offs]|memaddr [fetchargs]-------------------设置一个probe探测点r[:[grp/]event] [mod:]sym[+0] [fetchargs]------------------------------设置一个return probe探测点-:[grp/]event----------------------------------------------------------删除一个探测点
细节解释如下:
grp : group name. if omitted, use kprobes for it.------------设置后会在events/kprobes下创建目录。 event : event name. if omitted, the event name is generated based on sym+offs or memaddr.---指定后在events/kprobes/生成目录。 mod : module name which has given sym.--------------------------模块名,一般不设 sym[+offs] : symbol+offset where the probe is inserted.-------------被探测函数名和偏移 memaddr : address where the probe is inserted.----------------------指定被探测的内存绝对地址 fetchargs : arguments. each probe can have up to 128 args.----------指定要获取的参数信息。 %reg : fetch register reg---------------------------------------获取指定寄存器值 @addr : fetch memory at addr (addr should be in kernel)--------获取指定内存地址的值 @sym[+|-offs] : fetch memory at sym +|- offs (sym should be a data symbol)---获取全局变量的值 $stackn : fetch nth entry of stack (n >= 0)----------------------------------获取指定栈空间值,即sp寄存器+n后的位置值 $stack : fetch stack address.-----------------------------------------------获取sp寄存器值 $retval : fetch return value.(*)--------------------------------------------获取返回值,用户return kprobe $comm : fetch current task comm.----------------------------------------获取对应进程名称。 +|-offs(fetcharg) : fetch memory at fetcharg +|- offs address.(**)------------- name=fetcharg : set name as the argument name of fetcharg. fetcharg:type : set type as the type of fetcharg. currently, basic types (u8/u16/u32/u64/s8/s16/s32/s64), hexadecimal types (x8/x16/x32/x64), string and bitfield are supported.----------------设置参数的类型,可以支持字符串和比特类型 (*) only for return probe. (**) this is useful for fetching a field of data structures.
执行如下两条命令就会生成目录/sys/kernel/debug/tracing/events/kprobes/myprobe;第三条命令则可以删除指定kprobe事件,如果要全部删除则echo > /sys/kernel/debug/tracing/kprobe_events。
echo 'p:myprobe do_sys_open dfd=%x0 filename=%x1 flags=%x2 mode=+4($stack)' > /sys/kernel/debug/tracing/kprobe_eventsecho 'r:myretprobe do_sys_open ret=$retval' >> /sys/kernel/debug/tracing/kprobe_events-----------------------------------------------------这里面一定要用>>,不然就会覆盖前面的设置。echo '-:myprobe' >> /sys/kernel/debug/tracing/kprobe_eventsecho '-:myretprobe' >> /sys/kernel/debug/tracing/kprobe_events
参数后面的寄存器是跟架构相关的,%x0、%x1、%x2表示第1/2/3个参数,超出部分使用$stack来存储参数。
函数返回值保存在$retval中
kprobe使能
对kprobe事件的是能通过往对应事件的enable写1开启探测;写0暂停探测。
echo > /sys/kernel/debug/tracing/traceecho 'p:myprobe do_sys_open dfd=%x0 filename=%x1 flags=%x2 mode=+4($stack)' > /sys/kernel/debug/tracing/kprobe_eventsecho 'r:myretprobe do_sys_open ret=$retval' >> /sys/kernel/debug/tracing/kprobe_eventsecho 1 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enableecho 1 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enablelsecho 0 > /sys/kernel/debug/tracing/events/kprobes/myprobe/enableecho 0 > /sys/kernel/debug/tracing/events/kprobes/myretprobe/enablecat /sys/kernel/debug/tracing/trace
然后在/sys/kernel/debug/tracing/trace中可以看到结果。
总结
附录
arm32,arm64,x86寄存器及访问方式
arm32
r0, pt_regs->r0r1, pt_regs->r1r2, pt_regs->r2r3, pt_regs->r3r4, pt_regs->r4r5, pt_regs->r5r6, pt_regs->r6r7, pt_regs->r7r8, pt_regs->r8r9, pt_regs->r9r10,pt_regs->r10fp, pt_regs->fpip, pt_regs->ipsp, pt_regs->splr, pt_regs->lrpc, pt_regs->pc
arm64
x0, pt_regs->regs[0]x1, pt_regs->regs[1]x2, pt_regs->regs[2]x3, pt_regs->regs[3]x4, pt_regs->regs[4]x5, pt_regs->regs[5]x6, pt_regs->regs[6]x7, pt_regs->regs[7]x8, pt_regs->regs[8]x9, pt_regs->regs[9]x10, pt_regs->regs[10]x11, pt_regs->regs[11]x12, pt_regs->regs[12]x13, pt_regs->regs[13]x14, pt_regs->regs[14]x15, pt_regs->regs[15]x16, pt_regs->regs[16]x17, pt_regs->regs[17]x18, pt_regs->regs[18]x19, pt_regs->regs[19]x20, pt_regs->regs[20]x21, pt_regs->regs[21]x22, pt_regs->regs[22]x23, pt_regs->regs[23]x24, pt_regs->regs[24]x25, pt_regs->regs[25]x26, pt_regs->regs[26]x27, pt_regs->regs[27]x28, pt_regs->regs[28]x29, pt_regs->regs[29]x30, pt_regs->regs[30]sp, pt_regs->sppc, pt_regs->pcpstate,pt_regs->pstate
x86
rax pt_regs->ax rcx pt_regs->cx rdx pt_regs->cx rbx pt_regs->bx rsp pt_regs->sp rbp pt_regs->bp rdi pt_regs->di rsi pt_regs->si r8 pt_regs->r8 r9 pt_regs->r9 r10 pt_regs->r10 r11 pt_regs->r11 r12 pt_regs->r12 r13 pt_regs->r13 r14 pt_regs->r14 r15 pt_regs->r15
2021世界传感器大会将在河南省郑州国际会展中心隆重举办
华为智能座舱与百度地图签署生态合作协议 共创导航出行新体验
MLCC现货喊涨 年后供需紧
MySQL join的语义学习
解析三分频扬声器系统分频器设计
kprobes的使用方法
中国电信已完成了上海全市的5G网络精准规划
难道诺基亚才是亲儿子?现在不止是新机诺基亚8可以安装安卓8.0,诺基亚机型都可以了
存储器和数字芯片测试的基本测试技术
威格士叶片泵有什么原理及范围?
国内33家虚拟运营商已拿到正式牌照未来移动转售市场将何去何从
华为余承东发布首款5G折叠手机,号称业界最大创新,1G电影3秒下载
摩托罗拉Moto Edge 30 Neo采用骁龙695芯片
自制移动硬盘图解
5G+煤矿智能化:煤矿专用5G智能工业网关设计思路
R&S CMU200详细说明
亚瑟莱特科技5A PWM电源管理IC AX3116出炉
豪威集团以先进图像传感器技术赋能车载电子后视镜
QD MiniLED智屏太瞩目了一进门就被它勾了魂
微软宣布Office 365 Groups服务将更名为Microsoft 365 Groups