首页
社区
课程
招聘
[原创] eBPF kprobe原理分析
发表于: 2025-10-3 20:39 550

[原创] 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 ->

  1. perf_event_open_probe(创建 perf event) -> syscall(__NR_perf_event_open)
  2. 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 函数进行探针注册(不涉及函数指令修改):

  1. 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。
  2. 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 操作结构,从而开始对内核函数调用进行跟踪。

  1. __register_ftrace_function → ftrace_update_trampoline → arch_ftrace_update_trampoline → create_trampoline:为 ops 生成或更新其专属的 trampoline(跳板代码)。
  2. 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_insn
static __always_inline
void __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: pushf
0xffffffffc0811001: push   %rbp
0xffffffffc0811002: push   0x18(%rsp)
0xffffffffc0811006: push   %rbp
0xffffffffc0811007: mov    %rsp,%rbp
0xffffffffc081100a: push   0x20(%rsp)
0xffffffffc081100e: push   %rbp
0xffffffffc081100f: mov    %rsp,%rbp
0xffffffffc0811012: sub    $0xa8,%rsp
0xffffffffc0811019: 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),%rbp
0xffffffffc081114b: mov    0x40(%rsp),%r9
0xffffffffc0811150: mov    0x48(%rsp),%r8
0xffffffffc0811155: mov    0x70(%rsp),%rdi
0xffffffffc081115a: mov    0x68(%rsp),%rsi
0xffffffffc081115f: mov    0x60(%rsp),%rdx
0xffffffffc0811164: mov    0x58(%rsp),%rcx
0xffffffffc0811169: mov    0x50(%rsp),%rax
0xffffffffc081116e: add    $0xd0,%rsp
0xffffffffc0811175: popf
0xffffffffc0811176: 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):
图片描述


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2025-10-4 18:55 被ALwalker编辑 ,原因: 修改问题描述
收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回