工作原理

内核已经提供了大量TracePoint,但有时候跟踪函数并未有相关TP点,这就必须要修改内核重新编译,相当麻烦。所以内核对外提供了kprobekretprobe技术,在用户态提供了uprobeuretprobe技术,可以在特定函数地址处进行插桩。

kprobe实现流程如下图所示(架构不同实现也不同,大致思路是类似的):

大致思路为两段中断触发,第一次触发中断执行原指令,第二次触发中断跳转回原流程。

kernel-probe_0

  1. 首先将需要probe的指令替换为中断触发指令。
  2. 触发中断后,调用处理函数进行信息输出,最后调用setup_singlestep函数将中断返回地址修改为slot地址,slot内容为原指令加上一条中断触发指令。
  3. 中断返回后,执行原指令,再执行中断触发指令。
  4. 中断触发后,调用指令执行后函数,将中断返回地址替换为insn2地址,返回中断。
  5. 中断返回后,继续执行原程序流程。

kretporbe实现流程与kprobe流程类似,只是在handler处理中不同,kprobehandler处理过程只是做信息输出,kretporbehandler处理过程将函数返回地址修改为“蹦床地址”:

  1. 首先将函数第一条指令替换为中断触发指令。
  2. 触发中断后,调用处理函数,将函数返回地址修改为"蹦床地址",最后调用setup_singlestep函数将中断返回地址修改为slot地址,slot内容为原指令加上一条中断触发指令。
  3. 中断返回,执行原指令,再执行中断触发指令。
  4. 中断触发后,调用指令执行后函数,将中断返回地址替换为insn2地址,返回中断。
  5. 中断返回后,继续执行原程序流程。
  6. 原程序执行到函数返回指令,跳转到“蹦床程序”。
  7. “蹦床程序”中处理信息输出,并修改返回地址为原程序返回地址,执行完成后返回。
  8. 程序返回后,继续执行原程序流程。

uprobe/uretprobekprobe/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

kernel-probe_1

拿到信息可做时延分析,调用频率,参数分析等。

最后修改:2023 年 01 月 08 日
如果觉得我的文章对你有用,请随意赞赏