工作原理
内核已经提供了大量TracePoint
,但有时候跟踪函数并未有相关TP
点,这就必须要修改内核重新编译,相当麻烦。所以内核对外提供了kprobe
和kretprobe
技术,在用户态提供了uprobe
和uretprobe
技术,可以在特定函数地址处进行插桩。
kprobe
实现流程如下图所示(架构不同实现也不同,大致思路是类似的):
大致思路为两段中断触发,第一次触发中断执行原指令,第二次触发中断跳转回原流程。
- 首先将需要
probe
的指令替换为中断触发指令。 - 触发中断后,调用处理函数进行信息输出,最后调用
setup_singlestep
函数将中断返回地址修改为slot
地址,slot
内容为原指令加上一条中断触发指令。 - 中断返回后,执行原指令,再执行中断触发指令。
- 中断触发后,调用指令执行后函数,将中断返回地址替换为
insn2
地址,返回中断。 - 中断返回后,继续执行原程序流程。
kretporbe
实现流程与kprobe
流程类似,只是在handler
处理中不同,kprobe
在handler
处理过程只是做信息输出,kretporbe
在handler
处理过程将函数返回地址修改为“蹦床地址”:
- 首先将函数第一条指令替换为中断触发指令。
- 触发中断后,调用处理函数,将函数返回地址修改为"蹦床地址",最后调用
setup_singlestep
函数将中断返回地址修改为slot
地址,slot
内容为原指令加上一条中断触发指令。 - 中断返回,执行原指令,再执行中断触发指令。
- 中断触发后,调用指令执行后函数,将中断返回地址替换为
insn2
地址,返回中断。 - 中断返回后,继续执行原程序流程。
- 原程序执行到函数返回指令,跳转到“蹦床程序”。
- “蹦床程序”中处理信息输出,并修改返回地址为原程序返回地址,执行完成后返回。
- 程序返回后,继续执行原程序流程。
uprobe/uretprobe
与kprobe/kretprobe
原理相同,只是其在用户态下执行,原理相同,仅实现细节不同,这里不再描述。
实现剖析
kprobe 注册流程
int register_kprobe(struct kprobe *p);
// 1. 根据符号查找地址 _kprobe_addr
// 2. 检查地址是否可以 probe check_kprobe_address_safe
// 3. 插入到全局 probe 表 kprobe_table
// 4. 修改地址处指令为中断触发指令
kprobe 触发流程
第一次触发中断流程(即执行原指令流程和信息输出):
// 这里是 riscv 架构下的流程, 注意架构不同实现不同
// 1. 当执行到中断触发流程,进入中断触发 do_trap_break
// 2. 第一次触发进入 kprobe_breakpoint_handler
// 取 probe
p = get_kprobe((kprobe_opcode_t *) addr);
// 执行 probe 中设置的 pre_handler 函数, 调用栈如下
// pre_handler = kprobe_dispatcher
// kprobe_trace_func
// __kprobe_trace_func 将信息输出到 ring buffer
if (!p->pre_handler || !p->pre_handler(p, regs))
// 将中断返回地址修改为 slot 地址
// 即中断返回后执行原指令和中断触发指令
setup_singlestep();
第二次触发中断流程(即跳转回原流程):
// 1. 当执行到中断触发流程,进入中断触发 do_trap_break
// 2. 第二次触发进入调用栈如下
// kprobe_single_step_handler
// post_kprobe_handler 修改返回地址为原地址
kretprobe 触发流程
第一次触发中断流程(即执行原指令流程和修改返回地址):
// 1. 当执行到中断触发流程, 进入中断触发 do_trap_break
// 2. 第一次触发进入 kprobe_breakpoint_handler
// 执行 probe 中设置的 pre_handler 函数, 调用栈如下
// pre_handler = pre_handler_kretprobe
// rethook_hook
// arch_rethook_prepare 修改返回地址为蹦床函数地址
if (!p->pre_handler || !p->pre_handler(p, regs))
// 将中断返回地址修改为 slot 地址
// 即中断返回后执行原指令和中断触发指令
setup_singlestep();
第二次触发中断流程(即跳转回原流程),和kprobe
相同。
函数执行到函数返回指令时,执行蹦床函数:
// rethook_trampoline_handler
// instruction_pointer_set 将返回地址设置为原返回地址
// handler = kretprobe_rethook_handler
// handler = kretprobe_dispatcher
// kretprobe_trace_func
// __kretprobe_trace_func 输出信息到 ring buffer
debugfs 文件系统交互
写入/sys/kernel/debug/tracing/kprobe_events
文件,注册kprobe
调用栈如下:
probes_write
-> trace_parse_run_command
-> create_or_delete_trace_kprobe
-> trace_kprobe_create
-> __trace_kprobe_create // 处理符号和地址
-> register_trace_kprobe // 注册 kprobe
实战演练
监控sys_brk
系统调用的进入和返回(内核配置需要CONFG_KPROBES
支持)。
# 创建一个 kprobe 名为 sys_brk_enter 组默认为 kprobes
# 地址为 sys_brk 入口
echo "p:sys_brk_enter sys_brk" >> /sys/kernel/debug/tracing/kprobe_events
# 创建一个 kretprobe 名为 sys_brk_ret 组默认为 kprobes
# 函数为 sys_brk
echo "r:sys_brk_ret sys_brk" >> /sys/kernel/debug/tracing/kprobe_events
# 开启两个 probe 点
echo 1 >> /sys/kernel/debug/tracing/events/kprobes/sys_brk_enter/enable
echo 1 >> /sys/kernel/debug/tracing/events/kprobes/sys_brk_ret/enable
拿到信息可做时延分析,调用频率,参数分析等。