基于 Linux-5.15
一、kprobe 简介
1. kprobes 是为了便于跟踪内核函数执行状态的一种轻量级内核调试技术。可以在内核的绝大多数函数(非inline、非trace自身函数)中动态的插入探测点来收集所需的调试状态信息,包括函数有无被调用、何时被调用、
执行是否正确以及函数的入参和返回值是什么等等。
2. kprobes技术包括的3种探测手段分别时kprobe、jprobe(5.10上已废弃) 和 kretprobe。kprobe 是最基本的探测方式,是实现后两种的基础,它可以在任意的位置放置探测点(就连函数内部的某条指令处也可以),
它提供了探测点的调用前、调用后和内存访问出错3种回调方式,分别是 pre_handler、post_handler 和 fault_handler,其中 pre_handler 函数将在被探测指令被执行前回调,post_handler 会在被探测指令执行
完毕后回调(注意不是被探测函数),fault_handler 会在内存访问出错时被调用;jprobe 基于 kprobe 实现,它用于获取被探测函数的入参值;kretprobe 同样基于kprobe实现,用于获取被探测函数的返回值。
3. kprobes 的技术需要硬件异常处理和单步调试技术的支持,前者用于让程序的执行流程陷入到用户注册的回调函数中去,而后者则用于单步执行被探测点指令。
4. kprobes 的特点与使用限制
(1) kprobes 允许在同一个被被探测位置注册多个kprobe.
(2) kprobes 几乎可以探测内核中的任何函数,包括中断处理函数。单 kprobes.c 中用于实现 kprobes 自身的函数是不允许被探测的,另外还有 inline 函数 do_page_fault 和 notifier_call_chain;
(3) kprobes 会避免在处理探测点函数时再次调用另一个探测点的回调函数,例如在 printk()函数上注册了探测点,则在它的回调函数中可能再次调用 printk 函数,此时将不再触发printk探测点的回调,仅仅时增
加了 kprobe 结构体中 nmissed 字段的数值;
(4) kprobes 回调函数的运行期间是关闭抢占的,因此不论在何种情况下,在回调函数中不要调用会放弃CPU的函数。
(5) kretprobe 通过替换返回地址为预定义的 trampoline 的地址来实现,因此栈回溯和gcc内嵌函数 __builtin_return_address() 调用将返回 trampoline 的地址而不是真正的被探测函数的返回地址;
(6) 如果一个函数的调用次数和返回次数不相等,则在类似这样的函数上注册 kretprobe 将可能不会达到预期的效果,例如 do_exit() 函数会存在问题,而 do_execve() 函数和 do_fork() 函数不会;
5. 使用 kprobe 有两种方式,第一种是开发人员自行编写内核模块,向内核注册探测点,探测函数可根据需要自行定制,使用灵活方便。可参考samples/kprobes目录下的 kprobe_example.c 和 kretprobe_example.c;
第二种方式是使用kprobes on ftrace,这种方式是kprobe和ftrace结合使用,即可以通过kprobe来优化ftrace来跟踪函数的调用,下面通过实验进行介绍。
二、kprobe 相关数据结构
1. struct kprobe
struct kprobe { struct hlist_node hlist; /* list of kprobes for multi-handler support */ struct list_head list; /*count the number of times this probe was temporarily disarmed */ unsigned long nmissed; /* location of the probe point */ kprobe_opcode_t *addr; /* Allow user to indicate symbol name of the probe point */ const char *symbol_name; /* Offset into the symbol */ unsigned int offset; /* Called before addr is executed. */ kprobe_pre_handler_t pre_handler; /* Called after addr is executed, unless... */ kprobe_post_handler_t post_handler; /* Saved opcode (which has been replaced with breakpoint) */ kprobe_opcode_t opcode; /* copy of the original instruction */ struct arch_specific_insn ainsn; /* Indicates various status flags. Protected by kprobe_mutex after this kprobe is registered. */ u32 flags; };
成员说明:
hlist: 被用于kprobe全局hash,索引值为被探测点的地址;
list: 用于链接同一被探测点的不同探测kprobe
addr:被探测点的地址,注册过程中由 kprobe_addr()将探测的函数名转换为地址,赋值到这里.
symbol_name:被探测函数的名字,一般是驱动中直接指向内核函数。
offset:被探测点在函数内部的偏移,用于探测函数内部的指令,如果该值为0表示函数的入口;
pre_handler:在被探测点指令执行之前调用的回调函数,在 kprobe_handler() 中调用;
post_handler:在被探测指令执行之后调用的回调函数;
opcode:保存的被探测点原始指令,在 arch_prepare_kprobe() 中执行保存动作;
ainsn:被复制的被探测点的原始指令,用于单步执行,架构强相关(可能包含指令模拟函数);
flags:状态标记
2. struct kretprobe
struct kretprobe { struct kprobe kp; kretprobe_handler_t handler; kretprobe_handler_t entry_handler; int maxactive; int nmissed; size_t data_size; struct freelist_head freelist; struct kretprobe_holder *rph; };
成员说明:
kp: kretprobe 是基于 kprobe 实现的;
handler: 在被探测函数返回后被调用(一般在这个函数中打印被探测函数的返回值);
entry_handler: 会在被探测函数执行之前被调用;
maxactive: 表示同时支持并行探测的上限。因为 kretprobe 会跟踪一个函数从开始到结束,因此对于一些调用比较频繁的被探测函数,在探测的时间段内重入的概率比较高,这个 maxactive 字段值表示在重入情况发生时,
支持同时检测的进程数(执行流数)的上限,若并行触发的数量超过了这个上限,则 kretprobe 不会进行跟踪探测,仅仅增加 nmissed 字段的值以作提示;
data_size: 表示kretprobe私有数据的大小,在注册kretprobe时会根据该大小预留空间;
freelist: 表示空闲的 kretprobe 运行实例链表,它链接了本 kretprobe 的空闲实例 struct kretprobe_instance 结构体表示。
三、通过trace使用kprobe分析
1. trace方式使用的handler函数
struct file_operations dyn_event_write //trace_dynevent.c 对应 /sys/kernel/tracing/dynamic_events 文件 create_dyn_event //trace_dynevent.c trace_kprobe_ops.create //trace_kprobe.c alloc_trace_kprobe 中进行的赋值 struct file_operations kprobe_events_ops.write //trace_kprobe.c 对应 /sys/kernel/tracing/kprobe_events 文件 probes_write create_or_delete_trace_kprobe trace_kprobe_create //trace_kprobe.c __trace_kprobe_create //trace_kprobe.c 这个函数中会解析echo的参数,有语法注释 perf_kprobe_init //trace_event_perf.c 非主要调用路径 create_local_trace_kprobe //trace_kprobe.c 使能 CONFIG_PERF_EVENTS 才调用,默认使能的 alloc_trace_kprobe if (is_return) tk->rp.handler = kretprobe_dispatcher; else tk->rp.kp.pre_handler = kprobe_dispatcher; register_trace_kprobe // __trace_kprobe_create 中继续执行这个函数 __register_trace_kprobe if (trace_kprobe_is_return(tk)) ret = register_kretprobe(&tk->rp); else ret = register_kprobe(&tk->rp.kp);
通过向 kprobe_events 文件写入命令的方式使用 kprobe,可以看到kprobe使用的是 p->pre_handler = kprobe_dispatcher. kretprobe使用的是 kretprobe_dispatcher().
四、通过trace使用kprobe实验
1. trace sysfs文件节点的内核调用路径
ssize_t pl_store(struct gov_attr_set *attr_set, const char *buf, size_t count) //要探测的内核函数原型 /sys/kernel/tracing # echo 'p:myprobe pl_store buf=+0(%x1):string' > kprobe_events //设置要trace pl_store(),并打印其参数1用户传入buf中的内容。 /sys/kernel/tracing # echo 1 > options/stacktrace //使能打印函数调用栈(只能打印内核空间的) /sys/kernel/tracing # echo 1 > events/kprobes/myprobe/enable //使能自己定义的 myprobe 节点
然后执行 P=/sys/kernel/tracing; > $P/trace; echo 1 > $P/tracing_on; cat $P/trace_pipe 抓trace,可以看到打印:
/sys/devices/system/cpu/cpufreq/policy0/walt # echo 112233 > pl sh 23682-23682 ( 23682) [000] d..1 259562.002535: myprobe: (pl_store+0x0/0x48 [sched_walt]) buf="112233 //buf内容打印出来 " sh-23682 ( 23682) [000] d..1 259562.002542: <stack trace> => pl_store => sysfs_kf_write => kernfs_fop_write_iter => vfs_write => ksys_write => __arm64_sys_write => el0_svc_common => el0_svc => el0_sync_handler => el0_sync
结束kprobe trace执行:
/sys/kernel/tracing # echo 0 > events/kprobes/myprobe/enable //禁用自己定义的 myprobe 节点 /sys/kernel/tracing # echo '-:myprobe' > kprobe_events //移除自己要probe的函数
2. trace procfs文件节点的内核调用路径
ssize_t proc_test_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) //函数原型 /sys/kernel/tracing # echo 'p:myprobe proc_test_write buf=+0(%x1):ustring' > kprobe_events //其它步骤同上
然后执行 P=/sys/kernel/tracing; > $P/trace; echo 1 > $P/tracing_on; cat $P/trace_pipe 抓trace,可以看到打印:
RenderThread-9557 ( 4185) [005] d..1 201.357916: myprobe: (proc_test_write+0x0/0x32c [proc_test]) buf="p 9557" //buf内容打印出来 <...>-9557 ( 4185) [005] d..1 201.357917: <stack trace> => proc_test_write => vfs_write => ksys_write => __arm64_sys_write => el0_svc_common => el0_svc => el0_sync_handler => el0_sync
注:若不需要栈回溯可以不使能,也可以通过向 trace_options 文件中echo stacktrace/nostacktrace 来使能/禁止打印栈回溯。不使能栈回溯trace打印如下:
//配置kprobe: /sys/kernel/tracing # echo 'p:myprobe proc_test_write buf=+0(%x1):ustring' > kprobe_events /sys/kernel/tracing # echo 1 > events/kprobes/myprobe/enable //抓trace: /sys/kernel/tracing # echo 1 > tracing_on /sys/kernel/tracing # cat trace_pipe | grep proc_test_write //trace打印: RenderThread-8950 [007] d..1. 202.148271: myprobe: (proc_test_write+0x0/0x278 [proc_test]) buf="p 8950" RenderThread-8964 [007] d..1. 204.906870: myprobe: (proc_test_write+0x0/0x278 [proc_test]) buf="p 8964" ...
3. trace函数的所有参数
int select_task_rq_fair(struct task_struct *p, int prev_cpu, int wake_flags) //函数原型 /sys/kernel/tracing # echo 'p:myprobe select_task_rq_fair p=%x0:x64 cpu=%x1 wake_flags=%x2:x32' > kprobe_events /sys/kernel/tracing # echo 1 > events/kprobes/myprobe/enable /sys/kernel/tracing # echo 1 > tracing_on /sys/kernel/tracing # cat trace_pipe | grep select_task_rq_fair cat-30355 [006] d..4. 12923.640081: myprobe: (select_task_rq_fair+0x0/0x360) p=0xffffff8312294800 cpu=0x6 wake_flags=0x18 adbd-1241 [000] d.h5. 12923.640128: myprobe: (select_task_rq_fair+0x0/0x360) p=0xffffff838a9a3600 cpu=0x6 wake_flags=0x8 grep-30356 [006] d..4. 12923.640135: myprobe: (select_task_rq_fair+0x0/0x360) p=0xffffff83c6101200 cpu=0x5 wake_flags=0x8 ...
4. trace函数的返回值
/sys/kernel/tracing # echo 'r:myprobe select_task_rq_fair ret_cpu=$retval' > kprobe_events /sys/kernel/tracing # echo 1 > events/kprobes/myprobe/enable /sys/kernel/tracing # echo 1 > tracing_on /sys/kernel/tracing # cat trace_pipe | grep select_task_rq_fair cat-21169 [007] d..4. 5803.107173: myprobe: (try_to_wake_up+0x2d0/0x800 <- select_task_rq_fair) ret_cpu=0x7 adbd-1236 [003] d..4. 5803.107522: myprobe: (try_to_wake_up+0x2d0/0x800 <- select_task_rq_fair) ret_cpu=0x5 ...
注:使用kretprobe,echo的首字符是'r'而不是使用kprobe的'p',因此参数和返回值不能同时trace. ==>加2个trace点呢?
5. 同时trace函数所有参数和返回值
/sys/kernel/tracing # echo 'p:myprobe select_task_rq_fair p=%x0:x64 cpu=%x1 wake_flags=%x2:x32' > kprobe_events /sys/kernel/tracing # echo 'r:myretprobe select_task_rq_fair ret_cpu=$retval' >> kprobe_events //追加,否则只有一个,必须要改为使用 myretprobe,否则不成功 /sys/kernel/tracing # echo 1 > events/kprobes/myprobe/enable /sys/kernel/tracing # echo 1 > events/kprobes/myretprobe/enable /sys/kernel/tracing # echo 1 > tracing_on /sys/kernel/tracing # cat trace_pipe | grep select_task_rq_fair grep-30836 [007] d..4. 13444.897214: myprobe: (select_task_rq_fair+0x0/0x360) p=0xffffff83a0398000 cpu=0x5 wake_flags=0x8 adbd-1241 [004] d..4. 13444.897216: myprobe: (select_task_rq_fair+0x0/0x360) p=0xffffff83e0fcb600 cpu=0x5 wake_flags=0x8 grep-30836 [007] d..4. 13444.897219: myretprobe: (try_to_wake_up+0x2d0/0x800 <- select_task_rq_fair) ret_cpu=0x3 <...>-30699 [005] d..4. 13444.897221: myprobe: (select_task_rq_fair+0x0/0x360) p=0xffffff80bf4a3600 cpu=0x5 wake_flags=0x8 adbd-1241 [004] d..4. 13444.897230: myretprobe: (try_to_wake_up+0x2d0/0x800 <- select_task_rq_fair) ret_cpu=0x1 <...>-30699 [005] d..4. 13444.897230: myretprobe: (try_to_wake_up+0x2d0/0x800 <- select_task_rq_fair) ret_cpu=0x1 ...
可以看到入参时的探测和返回值的探测在trace的显示上并不是一一对应的,展示效果并不好。
6. 使用filter过滤功能
(1) 通过PID过滤
/sys/kernel/tracing # echo 'p:myprobe proc_task_write buf=+0(%x1):ustring' > kprobe_events /sys/kernel/tracing # echo 1 > events/kprobes/myprobe/enable /sys/kernel/tracing # echo "common_pid!=4083" > events/kprobes/myprobe/filter //过滤TID=4083线程上下文的打印,common_pid 来自 events/kprobes/myprobe/format /sys/kernel/tracing # cat events/kprobes/myprobe/filter common_pid!=4083 /sys/kernel/tracing # echo "common_pid==4083" > events/kprobes/myprobe/filter //只保留TID=4083线程上下文的打印 /sys/kernel/tracing # cat events/kprobes/myprobe/filter common_pid==4083 //只过滤"common_pid==4083"后只有4083这一个线程打印了: ndroid.launcher-4083 [007] d..1. 2655.036948: myprobe: (proc_task_write+0x0/0x3e4 [proc_test]) buf="p 2827 115" ndroid.launcher-4083 [007] d..1. 2655.036955: myprobe: (proc_task_write+0x0/0x3e4 [proc_test]) buf="p 3070 124" ... /sys/kernel/tracing # echo 0 > events/kprobes/myprobe/filter //取消过滤 /sys/kernel/tracing # cat events/kprobes/myprobe/filter none
(2) 通过参数过滤
/sys/kernel/tracing # echo 'wake_flags==0x18' > events/kprobes/myprobe/filter //只看实验5中wake_flags==0x18的
(3) 通过返回值过滤
/sys/kernel/tracing # echo 'ret_cpu==1' > events/kprobes/myretprobe/filter //只看实验5中ret_cpu==1的
7. 实验总结
(1) 可以以追加的方式">>"的方式向 kprobe_events 文件中echo多个要跟踪的函数。
(2) 要做好清理才能正常使用下一次kprobe,比如一次测试完后echo 0 > enable 和 echo '-:myprobe' > kprobe_events。
(3) 需要先 echo '-:myprobe' 取消之前的探测,并且保证函数在 cat /proc/kallsyms(echo 0 > /proc/sys/kernel/kptr_restrict 后有地址) 中是有的。
五、通过代码使用kprobe分析
1. 内核中自带了 samples/kprobes/kprobe_example.c 文件用来展示如何通过代码使用kprobe。大致逻辑如下:
(1) 先定义一个 struct kprobe 结构变量(假设是 kp 指针),然后将要探测的函数名赋值给 kp->symbol_name 成员。
(2) 然后初始化 struct kprobe 结构变量的 kp->pre_handler / kp->post_handler 回调函数指针,前者会在探测指令执行前执行,后者则会在探测指令执行后执行。
(3) 然后通过 register_kprobe(kp) 注册这个 struct kprobe 结构变量p.
(4) 当不再使用时调用 unregister_kprobe(kp) 取消注册。
2. 内核中自带了 samples/kprobes/kretprobe_example.c 文件来展示如何通过代码使用kretprobe。kretprobe 是基于通过 kprobe 实现的,使用逻辑和kprobe基本一致,大致逻辑如下:
(1) 先定义一个 struct kretprobe 结构变量(假设是 kretp 指针),然后将要探测的函数名赋值给 kretp->kp.symbol_name 成员。
(2) 然后初始化 struct kprobe 结构变量的 kretp->handler / kretp->entry_handler 回调函数指针,前者会在函数退出时调用,后者则会在函数进入时调用。
(3) 然后通过 register_kretprobe(kretp) 注册这个 struct kretprobe 结构变量kretp.
(4) 当不再使用时调用 unregister_kretprobe(kretp) 取消注册。
六、通过代码使用kprobe实验
这里使用内核自带的 kprobe_example.c 和 kretprobe_example.c 两个文件进行实验。
1. 首先要使能配置
CONFIG_SAMPLES=y //整个samples目录默认是不被编译的,使能这个才会被编译 CONFIG_SAMPLE_KPROBES=m
2. kprobe_example.ko 测试
/data/local/tmp # insmod kprobe_example.ko /data/local/tmp # cat /sys/module/kprobe_example/parameters/symbol kernel_clone /data/local/tmp # dmesg -c | grep pstate [17632.084434] handler_pre: <kernel_clone> p->addr = 0x0000000033079247, pc = 0xffffffd95a524eb4, pstate = 0x82400005 [17632.084612] handler_post: <kernel_clone> p->addr = 0x0000000033079247, pstate = 0x82400005 [17632.089308] handler_pre: <kernel_clone> p->addr = 0x0000000033079247, pc = 0xffffffd95a524eb4, pstate = 0x82400005 [17632.089415] handler_post: <kernel_clone> p->addr = 0x0000000033079247, pstate = 0x82400005 ... /data/local/tmp # rmmod kprobe_example.ko
symbol 文件是通过 module_param_string() 实现的,只有cat显示功能。可以改为 module_param_cb() 来实现,以便在echo新的函数名进去后能在回调函数中取消注册旧的,转而去探测新的函数。
3. kretprobe_example.ko 测试
/data/local/tmp # insmod kretprobe_example.ko /data/local/tmp # cat /sys/module/kretprobe_example/parameters/func kernel_clone /data/local/tmp # dmesg -c | grep returned [18844.349421] kernel_clone returned 24524 and took 4076000 ns to execute //息屏状态 [18844.353517] kernel_clone returned 24525 and took 2896077 ns to execute ... [18954.466383] kernel_clone returned 25389 and took 69846 ns to execute //频点被拉起后 [18954.466565] kernel_clone returned 25390 and took 56923 ns to execute
此驱动用于探测一个函数(本例是 kernel_clone)的执行时长。可以同上修改以便可以探测所有函数的执行时长。
五、kprobe 实现原理
1. 实现原理概述
首先kprobe会将被探测地址 kp->addr 处的指令保存在 kp->opcode 中,然后替换探测地址处的指令为 BRK64_OPCODE_KPROBES,当执行到这条指令时,就会触发异常跳转到BRK处理流程中,会调用注册的回调函数 kprobe_breakpoint_handler(),
此流程中会先调用 kp->pre_handler() 回调,然后单步执行被替换的指令。TODO: kp->post_handler() 回调什么时候执行?
指令替换函数:
/* arm kprobe: install breakpoint in text */ void __kprobes arch_arm_kprobe(struct kprobe *p) { void *addr = p->addr; u32 insn = BRK64_OPCODE_KPROBES; aarch64_insn_patch_text(&addr, &insn, 1); }
2. 注册流程:
register_kprobe //kernel/kprobes.c kprobe_addr //kernel/kprobes.c _kprobe_addr //kernel/kprobes.c 将 p->symbol_name 转换为地址保存在 p->addr 中 prepare_kprobe //kernel/kprobes.c arch_prepare_kprobe //arm64/kprobes.c p->opcode = le32_to_cpu(*p->addr); //将探测地址处的指令保存到 p->opcode 中 arm_kprobe //kernel/kprobes.c __arm_kprobe //kernel/kprobes.c arch_arm_kprobe(p); //arm64/kprobes.c 将测试处指令替换为 BRK64_OPCODE_KPROBES
3. 触发指令后执行流程:
el1h_64_sync_handler //arm64/kernel/entry.S 中的异常向量表入口函数,判断 esr_el1 寄存器的值进行调用 el1_dbg //entry-common.c el0t_64_sync_handler //arm64/kernel/entry.S 中的异常向量表入口函数,判断 esr_el1 寄存器的值进行调用 el0_dbg //entry-common.c do_debug_exception //mm/fault.c debug_fault_info[DBG_ESR_EVT_BRK].fn //mm/fault.c brk_handler //debug-monitors.c call_break_hook //debug-monitors.c kprobe_breakpoint_handler //arm64/kprobes.c struct break_hook kprobes_break_hook.fn() 在 arch_init_kprobes() 中注册 kprobe_handler //arm64/kprobes.c p->pre_handler() //执行kprobe回调
注:el1h_64_sync_handler()/el0t_64_sync_handler 在 arm64/kernel/entry.S 中以异常向量表的形式定义的。
简化后的 kprobe_handler():
static void __kprobes kprobe_handler(struct pt_regs *regs) { struct kprobe *p; unsigned long addr = instruction_pointer(regs); //return regs->pc; p = get_kprobe((kprobe_opcode_t *) addr); /* 先调用kprobe的 pre_handler 回调 */ if (!p->pre_handler || !p->pre_handler(p, regs)) { /* 然后执行被替换的指令 */ setup_singlestep(p, regs, kcb, 0); } }
六、相关DEBUG方法
1. debugfs_kprobe_init() 中创建debugfs文件,显示不可以被kprobe探测函数黑名单和enable、diable文件。debug版本才有。
2. 通过 kprobe_profile 属性文件可以查看探测命中次数和丢失次数(probe hits and probe miss-hits)。
参考:
Linux内核调试技术Kprobe使用与实现:https://zhuanlan.zhihu.com/p/541499518
tracer ftrace笔记(12)—— trace文档翻译与实验——/sys/kernel/tracing/README:https://www.cnblogs.com/hellokitty2/p/17055175.html //里面命令格式介绍
标签:kernel,13,ftrace,trace,kprobes,sys,kprobe,handler From: https://www.cnblogs.com/hellokitty2/p/17092549.html