kprobe及kretprobe的基于例子来调试分析其原理
一、背景在之前的博客 register_kretprobe的使用及对抓取iowait程序的改进 里我们使用了kretprobe来进一步对抓取iowait的程序进行优化。这篇博客里我们基于例子来拆解kretprobe和kprobe的实现原理搞清楚里面的一些细节。在第二章里我们给出实验的思路及相关源码及实验结果。在第三章里我们给出相关说明。二、例子的源码及步骤及实验结果2.1 实验的思路为了让实验基于的例子尽可能地纯粹和干净。我们基于一个我们自己insmod的一个ko里的一个函数来进行kprobe的打桩实验也就是说先insmod一个ko来构造一个新的export_symbol的函数然后kprobe和kretprobe是基于这个ko里的这个export_symbol的函数来进行打桩实验这样可以不去影响原来内核里的逻辑以及原有的那些内核函数让实验过程和实验结果更加纯粹。之前有篇博客详细讲了这块ko依赖ko里的基础函数的实现方式 insmod一个ko提供基础函数供后insmod的ko使用的方法。2.2 实验的源码相关的一个目录结构如下上图里的testfunc.ko提供了一个export_symbol的函数另外其编译出来的Module.symvers产出物也会被用于编译testkretprobe.ko的makefile所引用。2.2.1 提供一个export_symbol的ko的源码#include linux/module.h #include linux/capability.h #include linux/sched.h #include linux/uaccess.h #include linux/proc_fs.h #include linux/ctype.h #include linux/seq_file.h #include linux/poll.h #include linux/types.h #include linux/ioctl.h #include linux/errno.h #include linux/stddef.h #include linux/lockdep.h #include linux/kthread.h #include linux/sched.h #include linux/delay.h #include linux/wait.h #include linux/init.h #include asm/atomic.h #include trace/events/workqueue.h #include linux/sched/clock.h #include linux/string.h #include linux/mm.h #include linux/interrupt.h #include linux/tracepoint.h #include trace/events/osmonitor.h #include trace/events/sched.h #include trace/events/irq.h #include trace/events/kmem.h #include linux/ptrace.h #include linux/uaccess.h #include asm/processor.h #include linux/sched/task_stack.h #include linux/nmi.h #include linux/version.h #include linux/sched/mm.h #include asm/irq_regs.h #include linux/kallsyms.h #include linux/kprobes.h #include linux/stop_machine.h #include linux/perf_event.h #include linux/file.h #include linux/fscache.h MODULE_LICENSE(GPL); MODULE_AUTHOR(zhaoxin); MODULE_DESCRIPTION(Module for kretprobe tasks.); MODULE_VERSION(1.0); #define USING_KRETPROBE 0 #if USING_KRETPROBE struct kretprobe _kp1; #else struct kprobe _kp1; #endif extern int testfunc_zhaoxin(volatile int *p); #if USING_KRETPROBE static int kretprobe_entry_handler(struct kretprobe_instance * i_s, struct pt_regs * i_p) #else int kprobecb_func_pre(struct kprobe* i_k, struct pt_regs* i_p) #endif { #ifdef __x86_64__ int *ptemp (int*) i_p-di; #elif defined(__aarch64__) int *ptemp (int*) i_p-regs[0]; #else #error #endif printk(kprobecb_func_pre *input_parameter %d\n, *ptemp); printk(preeempt_count%llx\n, (u64)preempt_count()); return 0; } volatile int _value; #if USING_KRETPROBE int kretprobe_ret_handler(struct kretprobe_instance *ri, struct pt_regs *i_p) #else void kprobecb_func_post(struct kprobe *p, struct pt_regs *i_p, unsigned long flags) #endif { #ifdef __x86_64__ int *ptemp (int*) i_p-di; #elif defined(__aarch64__) int *ptemp (int*) i_p-regs[0]; #else #error #endif printk(kprobecb_func_post value %d\n, _value); printk(preeempt_count%llx\n, (u64)preempt_count()); return 0; } int register_testfunc_zhaoxin_kprobe(void) { #if USING_KRETPROBE int ret; memset(_kp1, 0, sizeof(_kp1)); _kp1.entry_handler kretprobe_entry_handler; _kp1.handler kretprobe_ret_handler; _kp1.maxactive 0; _kp1.kp.addr (kprobe_opcode_t *)testfunc_zhaoxin; ret register_kretprobe(_kp1); if (ret 0) { printk(register_kretprobe fail!\n); return -1; } printk(register_kretprobe success!\n); return 0; #else int ret; memset(_kp1, 0, sizeof(_kp1)); _kp1.addr (kprobe_opcode_t *)(0xffffffffc1415010); //_kp1.symbol_name testfunc_zhaoxin; _kp1.pre_handler kprobecb_func_pre; _kp1.post_handler kprobecb_func_post; ret register_kprobe(_kp1); if (ret 0) { printk(register_kprobe fail!\n); return -1; } printk(register_kprobe success!\n); return 0; #endif } static int __init testkretprobe_init(void) { int ret; ret register_testfunc_zhaoxin_kprobe(); if (ret 0) { return ret; } ret testfunc_zhaoxin(_value); printk(after testfunc_zhaoxin *input_parameter %d, ret %d\n, _value, ret); return 0; } void unregister_testfunc_zhaoxin_kprobe(void) { #if USING_KRETPROBE unregister_kretprobe(_kp1); #else unregister_kprobe(_kp1); #endif } static void __exit testkretprobe_exit(void) { unregister_testfunc_zhaoxin_kprobe(); } module_init(testkretprobe_init); module_exit(testkretprobe_exit);2.2.2 引用该symbol的ko的源码及Makefile引用该symbol的ko的源码一使用kprobe的方式的源码#include linux/module.h #include linux/capability.h #include linux/sched.h #include linux/uaccess.h #include linux/proc_fs.h #include linux/ctype.h #include linux/seq_file.h #include linux/poll.h #include linux/types.h #include linux/ioctl.h #include linux/errno.h #include linux/stddef.h #include linux/lockdep.h #include linux/kthread.h #include linux/sched.h #include linux/delay.h #include linux/wait.h #include linux/init.h #include asm/atomic.h #include trace/events/workqueue.h #include linux/sched/clock.h #include linux/string.h #include linux/mm.h #include linux/interrupt.h #include linux/tracepoint.h #include trace/events/osmonitor.h #include trace/events/sched.h #include trace/events/irq.h #include trace/events/kmem.h #include linux/ptrace.h #include linux/uaccess.h #include asm/processor.h #include linux/sched/task_stack.h #include linux/nmi.h #include linux/version.h #include linux/sched/mm.h #include asm/irq_regs.h #include linux/kallsyms.h #include linux/kprobes.h #include linux/stop_machine.h #include linux/perf_event.h #include linux/file.h #include linux/fscache.h MODULE_LICENSE(GPL); MODULE_AUTHOR(zhaoxin); MODULE_DESCRIPTION(Module for kretprobe tasks.); MODULE_VERSION(1.0); struct kprobe _kp1; extern int testfunc_zhaoxin(volatile int *p); int kprobecb_func_pre(struct kprobe* i_k, struct pt_regs* i_p) { #ifdef __x86_64__ int *ptemp (int*) i_p-di; #elif defined(__aarch64__) int *ptemp (int*) i_p-regs[0]; #else #error #endif printk(kprobecb_func_pre *input_parameter %d\n, *ptemp); return 0; } void kprobecb_func_post(struct kprobe *p, struct pt_regs *i_p, unsigned long flags) { #ifdef __x86_64__ int *ptemp (int*) i_p-di; #elif defined(__aarch64__) int *ptemp (int*) i_p-regs[0]; #else #error #endif printk(kprobecb_func_post *input_parameter %d\n, *ptemp); } int register_testfunc_zhaoxin_kprobe(void) { int ret; memset(_kp1, 0, sizeof(_kp1)); _kp1.symbol_name testfunc_zhaoxin; _kp1.pre_handler kprobecb_func_pre; _kp1.post_handler kprobecb_func_post; ret register_kprobe(_kp1); if (ret 0) { printk(register_kprobe fail!\n); return -1; } printk(register_kprobe success!\n); return 0; } volatile int _value; static int __init testkretprobe_init(void) { int ret; ret register_testfunc_zhaoxin_kprobe(); if (ret 0) { return ret; } testfunc_zhaoxin(_value); printk(after testfunc_zhaoxin *input_parameter %d\n, _value); return 0; } void unregister_testfunc_zhaoxin_kprobe(void) { #if 0 unregister_kretprobe(my_kretprobe); #else unregister_kprobe(_kp1); #endif } static void __exit testkretprobe_exit(void) { unregister_testfunc_zhaoxin_kprobe(); } module_init(testkretprobe_init); module_exit(testkretprobe_exit);Makefile源码obj-m testkretprobe.o TEST_DIR $(CURDIR) KBUILD_EXTRA_SYMBOLS $(TEST_DIR)/testfunc/Module.symvers export KBUILD_EXTRA_SYMBOLS all: make -C /lib/modules/$(shell uname -r)/build M$(PWD) TEST_DIR$(TEST_DIR) modules clean: make -C /lib/modules/$(shell uname -r)/build M$(PWD) TEST_DIR$(TEST_DIR) clean引用该symbol的ko的源码二使用kretprobe的方式的源码2.3 步骤和实验结果如何查看当前运行的代码里实际的内容代码段里实际的内容会和编译出来的vmlinux不一致这是因为内核里有static_branch_likely及kprobe这样的动态修改代码段的功能所以实际的代码段里的正在运行时的内容需要用工具去dump出bin再去查看。之前有一篇相关的博客 内核执行时动态的vmlinux的反汇编解析方法及static_branch_likely机制。下面的操作步骤省去与该模块相关的细节内容。2.3.1 先做kprobe的实验在函数的基址处进行kprobe可以如下图看到用kprobe的pre_handler和post_handler抓到的被testfunc_zhaoxin修改的数值的式样的post_handler里打印出来的_value的数值并不是testfunc_zhaoxin运行结束后的数值通过下面的命令确定testfunc_zhaoxin函数的起始点的虚拟地址cat /proc/kallsyms | grep testfunc_zhaoxin如下图得到这个地址是0xffffffffc1415010到0xffffffffc1415040我们做一个kprobe前后的对比实验看一下这个0xffffffffc1415010地址上的内容是发生如何的变更。kprobe前insmod /usr/bin/testgetkmem.ko address0xffffffffc1415010 size0x30 filedirbeforekprobe.txt如上面的命令抓取到beforekprobe.txt里和insmod ko后抓到的afterkprobe.txt里改变的内容如下图insmod ko前testfunc的部分的开头是ef 1f 44 00搜索elf可以搜到是它是一句nop命令。insmod ko后testfunc的部分的开头是e8 eb ff c7搜索elf搜索前三个字节可以搜到是它是有关call跳转的。事实上e8 eb是共性部分e8是call的操作吗eb是段内直接短转的操作码。配合后面的offset就是跳转到__fentry__。2.3.2 做kprobe的实验在函数的基址加一些偏移处进行kprobe下一步我们实验在函数的基址加一些偏移处进行kprobe如上图修改不用函数名而用具体的地址这个地址是函数testfunc_zhaoxin的基址加0x6。这样进行dump出来的二进制内容用hexedit打开查看是如上图对比是只改了一个字节上图是x86平台我们拷贝一份ko出来通过hexedit编辑对应的二进制里的内容把B8改成CC然后objdump -S来解析出elf文件得到如下图内容可以看到如何插入到函数内部的话是会直接用int3这种中断指令来替换原有的指令。上面的设置的offset其实是自己根据汇编指令的位置来去计算的如果随便设置一个offset是会失败的如下图故意设置到一个指令的中间位置会报错2.3.3 再做kretprobe的实验将源码里的USING_KRETPROBE宏设置为1运行得到下图情况可以看到kretprobe是可以打桩在函数返回的时刻我们来抓一下看看kretprobe执行前后的相关代码段的改变。导出来后对比可以发现kretprobe的方式修改的内容也只是函数开头部分也是E8EB开始而函数尾部并没有修改。三、相关说明3.1 实验总结可以从上面 2.3.1 的实验可以看到kprobe的post_handler并不是函数返回时的时刻的状态。从上面 2.3.2 的实验可以看到kprobe的这套api可以打桩在任意一个函数指令位置处但是不能打桩到单条指令的开始和结束的中间。既然kprobe可以在任意位置打桩为啥还需要kretprobe这是因为函数返回的位置可能有多处。从上面 2.3.3 的实验可以看到kretprobe可以打桩在函数开始和函数返回时刻且kretprobe的返回值并不影响函数的返回值。kprobe如果打桩到函数的开始的位置通常是用的call的汇编指令如果是函数中间的短指令则用的int3中断指令。3.2 kretprobe的实现原理从上面 2.3.3 可以看到kretprobe的注册只是修改函数入口的地方的二进制那么kretprobe是如何实现函数返回时的跳转呢这是因为kretprobe是在注册时是注册了自己的一个特殊的函数作为pre_handler这个pre_handler就是基于kprobe的基址填的pre_handler的函数指针然后在触发kprobe时调用的是这个pre_handler_kretprobe接口这个pre_handler_kretprobe接口继而调用下图里的rethook_hook接口继而调用了arch_rethook_prepare注意这个调用链是在CONFIG_KRETPROBE_ON_RETHOOK打开时的调用链在CONFIG_KRETPROBE_ON_RETHOOK不打开时调用链不同后面会讲到而arch_rethook_prepare是一个arch的函数在x86下是如下实现通过记录下来当前的stack[0]位置的函数的返回值换成定义的arch_rethook_trampoline来让函数返回时先执行注册的函数。对于CONFIG_KRETPROBE_ON_RETHOOK未打开的情况pre_handler_kretprobe调用了arch_prepare_kretprobearm64平台的arch_prepare_kretprobe的实现如下arm64平台就是不打开CONFIG_KRETPROBE_ON_RETHOOK的3.3 关于kretprobe的maxactivemaxactive的设置其实从原理上来说是上限的上限就是2倍的possible cpu的数量因为kprobe的执行期间是禁用抢占的所以就算有抢占也是在执行完kprobe逻辑后在执行收尾逻辑时来发生抢占这样算一个cpu这样的并发也就最多两个乘上possible_cpu就是如下图里的上限当然也可以设置maxactive成0让其用这个默认上限值。3.4 关于kretprobe的data_sizekretprobe在注册时可以设置data_size属性这样在如下图的register_kretprobe时会分配一段私有的数据段作为kretprobe_instance结构体的一部分可以用来在kretprobe的entry时设置私有变量来表示特定状态在return的entry触发时判断这些标志位来做一些行为因为kretprobe在触发return的entry时几乎是不太可能拿到函数传入的入参的而我们经常kretprobe捕获事件得是特定入参的事件kretprobe_instance结构体的最后是一个柔性数组内核的kretprobe的sample也有示范其使用