4. 第一个eBPF+kprobe程序:编写最小化kprobe eBPF程序、使用bpftrace快速验证、使用libbpf加载运行

本章将带领你从零构建第一个结合 kprobe 的 eBPF 程序。我们将采用三种递进的方式:先编写最小化的内核态 eBPF 代码,然后使用 bpftrace 进行快速验证,最后使用 libbpf 完成完整的加载与运行流程。通过这一实战,你将掌握 eBPF 与 kprobe 协作的核心模式。

4.1 最小化 kprobe eBPF 程序结构

一个完整的 kprobe eBPF 程序通常包含两个部分:内核态 eBPF 字节码(通常以 .bpf.c 结尾)和用户态加载器(C 或 Python 等)。最小化版本仅需内核态代码即可通过 bpftrace 或 bpftool 验证。

核心概念: kprobe 在目标内核函数入口处触发 eBPF 程序。eBPF 程序通过 PT_REGS_PARM1 等宏访问函数参数,并将结果写入 perf_event 或 ring buffer 供用户态读取。

以下是最小化 kprobe eBPF 程序的骨架,它挂钩 do_sys_openat2 函数,捕获每次打开文件操作的文件名指针:

// minimal_kprobe.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

char LICENSE[] SEC("license") = "GPL";

SEC("kprobe/do_sys_openat2")
int kprobe__do_sys_openat2(struct pt_regs *ctx)
{
    // 获取第一个参数:struct pt_regs 中的 filename 指针
    char *filename = (char *)PT_REGS_PARM1(ctx);
    bpf_printk("open file: %s\n", filename);
    return 0;
}
组件 说明
SEC("kprobe/do_sys_openat2") 定义 kprobe 挂载点,目标内核函数为 do_sys_openat2
PT_REGS_PARM1(ctx) 从 pt_regs 中提取第一个参数(x86_64 下为 rdi 寄存器)
bpf_printk 将调试信息写入内核 trace pipe,便于快速验证
LICENSE 必须声明为 GPL 兼容许可证,否则内核拒绝加载
实战提示: 使用 bpf_printk 是最快的调试手段,但生产环境建议使用 bpf_perf_event_output 或 ring buffer 以避免影响内核日志。

4.2 使用 bpftrace 快速验证

bpftrace 是一种高级追踪语言,底层依赖 eBPF 和 kprobe。它允许你用一行命令验证 kprobe 挂载是否生效,无需编写 C 代码和编译流程。

验证上述最小化程序逻辑的 bpftrace 单行命令:

sudo bpftrace -e 'kprobe:do_sys_openat2 { printf("open: %s\n", str(arg0)); }'

执行后,每当有进程调用 do_sys_openat2,终端会实时打印打开的文件名。你可以同时运行 ls 或 cat 来触发事件。

注意事项: bpftrace 默认使用 kprobe 前缀,无需显式指定 SEC。但 bpftrace 的 arg0 对应 PT_REGS_PARM1,str() 辅助函数自动读取用户空间字符串。

bpftrace 验证流程与 C eBPF 程序的对应关系:

bpftrace 元素 C eBPF 对应
kprobe:do_sys_openat2 SEC("kprobe/do_sys_openat2")
arg0 PT_REGS_PARM1(ctx)
printf bpf_printk 或 bpf_perf_event_output
str() bpf_probe_read_user_str
bpftrace 快速验证流程 bpftrace 单行命令 bpftrace 运行时 内核 kprobe 挂载 触发事件(如 ls / cat) 终端实时打印输出 bpftrace 自动处理 eBPF 加载、验证、挂载和输出,是快速验证 kprobe 挂载点的最佳工具

4.3 使用 libbpf 加载运行

bpftrace 适合快速验证,但生产级 eBPF 程序通常使用 libbpf 作为用户态加载器。libbpf 提供完整的 BPF 对象生命周期管理,包括加载、验证、附加和销毁。

4.3.1 编译 eBPF 字节码

首先将 minimal_kprobe.bpf.c 编译为 BPF 目标文件:

clang -g -O2 -target bpf -D__TARGET_ARCH_x86 \
  -I/usr/include/x86_64-linux-gnu \
  -c minimal_kprobe.bpf.c -o minimal_kprobe.bpf.o
编译要点: -target bpf 指定生成 BPF 字节码;-D__TARGET_ARCH_x86 确保 PT_REGS_PARM1 宏正确展开;-g 保留 BTF 信息,便于 libbpf 进行 CO-RE 重定位。

4.3.2 编写用户态加载器

使用 libbpf 的 C API 加载 BPF 对象并附加 kprobe:

// loader.c
#include <stdio.h>
#include <bpf/libbpf.h>
#include <bpf/bpf.h>
#include "minimal_kprobe.skel.h"

int main() {
    struct minimal_kprobe_bpf *skel;
    int err;

    // 1. 打开并加载 BPF 对象
    skel = minimal_kprobe_bpf__open_and_load();
    if (!skel) {
        fprintf(stderr, "Failed to open and load BPF skeleton\n");
        return 1;
    }

    // 2. 附加 kprobe 到 do_sys_openat2
    err = minimal_kprobe_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "Failed to attach kprobe: %d\n", err);
        goto cleanup;
    }

    printf("kprobe attached to do_sys_openat2, tracing...\n");

    // 3. 读取 trace_pipe 输出
    system("cat /sys/kernel/debug/tracing/trace_pipe");

cleanup:
    minimal_kprobe_bpf__destroy(skel);
    return 0;
}

4.3.3 编译与运行

使用 bpftool 生成 skeleton 头文件,然后编译用户态程序:

# 生成 skeleton 头文件
bpftool gen skeleton minimal_kprobe.bpf.o > minimal_kprobe.skel.h

# 编译用户态加载器
gcc -g -O2 -o loader loader.c -lbpf -lelf -lz

# 运行(需要 root 权限)
sudo ./loader

运行后,程序会持续输出内核 trace_pipe 中的日志,显示每次 do_sys_openat2 被调用时的文件名。

4.4 三种方式的对比与选择

方式 适用场景 优点 缺点
bpftrace 快速验证、临时诊断、原型探索 零编译、单行命令、自动处理 功能受限、不适合复杂逻辑
libbpf + C 生产环境、性能敏感、复杂逻辑 完全控制、CO-RE 兼容、低开销 编译步骤多、开发周期长
bpftool 调试、手动加载、测试 命令行灵活、无需编写加载器 不适合持续运行的程序
推荐工作流: 先用 bpftrace 验证 kprobe 挂载点是否生效 → 再用 libbpf 编写生产级程序 → 使用 bpftool 进行调试和性能分析。
eBPF + kprobe 程序开发工作流 阶段1:快速验证 bpftrace 单行命令 阶段2:编写 C 程序 minimal_kprobe.bpf.c 阶段3:编译加载 clang + libbpf 实时追踪输出(trace_pipe / ring buffer)

4.5 常见问题与避坑指南

  • 权限不足: eBPF 程序需要 CAP_BPF 和 CAP_SYS_ADMIN 或 root 权限。使用 sudo 运行加载器。
  • 内核函数名不匹配: 不同内核版本函数名可能变化。使用 bpftrace -l 'kprobe:*' | grep <关键词> 确认可用函数。
  • 参数读取失败: 确保使用正确的 PT_REGS_PARMx 宏,x86_64 下 PARM1 对应 rdi,PARM2 对应 rsi,依此类推。
  • 字符串读取: 用户空间指针不能直接解引用,必须使用 bpf_probe_read_user_str 安全读取。
  • LICENSE 声明: 缺少 GPL 兼容许可证会导致内核拒绝加载,错误信息为 kernel: verifier: license is not GPL compatible。
安全提醒: kprobe 在内核路径中执行,eBPF 程序必须通过验证器检查。避免在 kprobe 中执行循环、复杂指针运算或长时间阻塞操作,否则可能导致系统不稳定。