-
-
[原创] eBPF kprobe原理分析
-
发表于: 2025-10-3 20:39 550
-
eBPF kprobe原理分析
kprobe 从用户层到内核层的函数调用栈如下:

请注意,当前测试的 kprobe 用例默认使用了 kprobe_ftrace,关于 kprobe-int3部分的执行流分析可能存在疏漏或错误之处,欢迎大牛指正!
测试系统版本为 Ubuntu 6.8 内核。
user:attach
执行 kprobe.attach 时,用户层函数栈如下:
xxx_bpf__attach -> bpf_object__attach_skeleton -> attach_kprobe -> bpf_program__attach_kprobe_opts ->
- perf_event_open_probe(创建 perf event) -> syscall(__NR_perf_event_open)
- bpf_program__attach_perf_event_opts(创建 BPF link,使用 BPF_LINK_CREATE 将 eBPF 程序与 perf event 关联) -> kernel_supports -> probe_perf_link -> bpf_link_create
kernel::attach
执行 kprobe.attach 时,int3 类型的 kprobe 内核函数栈如下:
entry_SYSCALL_64 -> do_syscall_x64 -> x64_sys_call -> __x64_sys_perf_event_open -> __se_sys_perf_event_open -> __do_sys_perf_event_open -> perf_event_alloc -> perf_init_event -> perf_try_init_event -> perf_kprobe_event_init -> perf_kprobe_init -> create_local_trace_kprobe -> __register_trace_kprobe -> __register_trace_kprobe -> register_kprobe
__register_kprobe
传统的 int3 类型 kprobe 使用 __register_kprobe 函数进行探针注册(不涉及函数指令修改):
prepare_kprobe:同时支持 ftrace 机制(kprobe_ftrace)和 int3 补丁。- arch_prepare_kprobe_ftrace:函数选择 ftrace-based kprobe 路径,只是做架构相关的初始化(清空 insn,禁用 boost),不进行指令修改。利用 ftrace 的 fentry 机制,在函数入口处的 ftrace 桩代码(trampoline)上进行 hook。
- arch_prepare_kprobe:get_insn_slot + arch_copy_kprobe,实现对目标函数指令复制到 kprobe.ainsn中。其中,arch_copy_kprobe函数内部调用
- __copy_instruction:复制目标地址指令至 kprobe.ainsn 中;
- prepare_emulation:依据目标地址 opcode 选择合适的模拟执行函数;
- prepare_singlestep:选择返回指令是通过 int3/jmp?
- text_poke:修改 kprobe。
- arm_kprobe -> __arm_kprobe -> arch_arm_kprobe
- text_poke:修改函数指令。
- smp_text_poke_sync_each_cpu -> ... -> smp_call_function_many_cond:向所有在线 CPU 发送 IPI(Inter-Processor Interrupt),实现函数 hook。
ftrace_startup
执行 kprobe.attach 时,kprobe_ftrace 的内核函数栈如下:
entry_SYSCALL_64 -> do_syscall_x64 -> x64_sys_call -> __x64_sys_perf_event_open -> __se_sys_perf_event_open -> __do_sys_perf_event_open -> perf_event_alloc -> perf_init_event -> perf_try_init_event -> perf_kprobe_event_init -> perf_kprobe_init -> perf_trace_event_init -> perf_trace_event_reg -> kprobe_register -> enable_trace_kprobe -> __enable_trace_kprobe -> enable_kprobe -> arm_kprobe -> arm_kprobe_ftrace -> __arm_kprobe_ftrace -> register_ftrace_function -> register_ftrace_function_nolock -> ftrace_startup -> ftrace_startup_enable -> ftrace_run_update_code -> arch_ftrace_update_code -> ftrace_modify_all_code -> ftrace_replace_code -> ...
函数 ftrace_startup 用于在运行时启动并注册一个 ftrace_ops 操作结构,从而开始对内核函数调用进行跟踪。
- __register_ftrace_function → ftrace_update_trampoline → arch_ftrace_update_trampoline → create_trampoline:为 ops 生成或更新其专属的 trampoline(跳板代码)。
- ftrace_startup_enable → ftrace_run_update_code → arch_ftrace_update_code → ftrace_modify_all_code → ftrace_replace_code:目标函数 patch
ftrace-based kprobe 调试分析
测试例程的 .bpf.c 源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | #include "vmlinux.h"#include <bpf/bpf_helpers.h>#include <bpf/bpf_tracing.h>#include <bpf/bpf_core_read.h>char LICENSE[] SEC("license") = "Dual BSD/GPL";SEC("kprobe/do_unlinkat")int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name){ pid_t pid; const char *filename; pid = bpf_get_current_pid_tgid() >> 32; filename = BPF_CORE_READ(name, name); bpf_printk("KPROBE ENTRY pid = %d, filename = %s\n", pid, filename); return 0;} |
create_trampoline
函数 arch_ftrace_update_trampoline 内部调用 create_trampoline 创建跳板函数,其返回地址为 0xffffffffc0811000。
当目标函数不存在 trampoline 时,直接返回;当存在 trampoline 时,需要重新计算 patch call 偏移并进行更新。

ops->func = kprobe_ftrace_handler,即跳板函数在执行完 context 保存后的跳转地址。
__text_gen_insn
在函数 __text_gen_insn 源码如下,用于计算 kprobe_ftrace 目标函数 call 指令的相对跳转地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // ftrace_call_replace -> text_gen_insn -> __text_gen_insnstatic __always_inlinevoid __text_gen_insn(void *buf, u8 opcode, const void *addr, const void *dest, int size){ union text_poke_insn *insn = buf; BUG_ON(size < text_opcode_size(opcode)); /* * Hide the addresses to avoid the compiler folding in constants when * referencing code, these can mess up annotations like * ANNOTATE_NOENDBR. */ OPTIMIZER_HIDE_VAR(insn); OPTIMIZER_HIDE_VAR(addr); OPTIMIZER_HIDE_VAR(dest); insn->opcode = opcode; if (size > 1) { insn->disp = (long)dest - (long)(addr + size); if (size == 2) { /* * Ensure that for JMP8 the displacement * actually fits the signed byte. */ BUG_ON((insn->disp >> 31) != (insn->disp >> 7)); } }} |
ftrace_replace_code
由于函数 text_gen_insn/__text_gen_insn 使用 inline 方法进行编译,内核中没有相关调试符号,因此选择在函数 ftrace_replace_code 中下断点。ftrace_replace_code 函数的关键汇编指令如下所示,涉及到 ftrace call 跳转地址的计算:

我选择在地址 0xffffffff810b5b14,可获取到 dest/addr 的线性地址。
请注意,地址 0xffffffff810b5b14 处的 int3 指令为 gdb 下的断点,由于发生了死锁,并未正确还原出原始指令。这是由于 __register_kprobe 函数持有 text_mutex 和 cpus_read_lock。
r12寄存器中存储的是 insn->opcode:

addr 指向 do_unlinkat 函数的首地址,即 .bpf.c 程序所设置的 SEC("kprobe/xxx")。经过 ftrace fentry 机制,在函数入口处的 ftrace 桩代码(trampoline)上进行 hook。

create_trampoline 函数所生成的 trampoline 汇编指令如下,包含执行context 的保存和回调函数的调用实现执行流控制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 0xffffffffc0811000: pushf0xffffffffc0811001: push %rbp0xffffffffc0811002: push 0x18(%rsp)0xffffffffc0811006: push %rbp0xffffffffc0811007: mov %rsp,%rbp0xffffffffc081100a: push 0x20(%rsp)0xffffffffc081100e: push %rbp0xffffffffc081100f: mov %rsp,%rbp0xffffffffc0811012: sub $0xa8,%rsp0xffffffffc0811019: mov %rax,0x50(%rsp)0xffffffffc081101e: mov %rcx,0x58(%rsp)0xffffffffc0811023: mov %rdx,0x60(%rsp)0xffffffffc0811028: mov %rsi,0x68(%rsp)0xffffffffc081102d: mov %rdi,0x70(%rsp)0xffffffffc0811032: mov %r8,0x48(%rsp)0xffffffffc0811037: mov %r9,0x40(%rsp)0xffffffffc081103c: movq $0x0,0x78(%rsp)...0xffffffffc08110f2: call 0xffffffff810bc850 <kprobe_ftrace_handler>...0xffffffffc0811146: mov 0x20(%rsp),%rbp0xffffffffc081114b: mov 0x40(%rsp),%r90xffffffffc0811150: mov 0x48(%rsp),%r80xffffffffc0811155: mov 0x70(%rsp),%rdi0xffffffffc081115a: mov 0x68(%rsp),%rsi0xffffffffc081115f: mov 0x60(%rsp),%rdx0xffffffffc0811164: mov 0x58(%rsp),%rcx0xffffffffc0811169: mov 0x50(%rsp),%rax0xffffffffc081116e: add $0xd0,%rsp0xffffffffc0811175: popf0xffffffffc0811176: ret # 通过 ret 指令直接返回原始执行流继续执行 |
int3-based kprobe 调试分析
测试例程的 .bpf.c 源码如下:
1 2 3 | ...SEC("kprobe/do_unlinkat+120")... |
相较于直接将 kprobe 设置在函数入口处(被优化为 ftrace-based kprobe),当将 kprobe 设置在函数内部时,eBPF 将会使用基于 int3 类型的 kprobe。
prepare_kprobe
当执行完函数 prepare_kprobe 后,kprobe 获得了目标函数地址 addr/ainsn,opcode=0x48,tp_len=4。

目标地址 do_unlinkat+120 的原始汇编指令为:


kprobe.ainsn 中所记录的经过构造后的汇编指令为:


其中,除了被原始的原始指令 mov %rbx, %rsi 外,新增了指令 int3 用于通过 #BP 返回原始执行流中。
arm_kprobe
当执行完函数 arm_kprobe 后,目标地址指令被修改(只修改首地址的单字节为 0xcc):
