4. 第一个eBPF+kprobe程序
本章将带领你从零构建第一个结合 kprobe 的 eBPF 程序。我们将采用三种递进的方式:先编写最小化的内核态 eBPF 代码,然后使用 bpftrace 进行快速验证,最后使用 libbpf 完成完整的加载与运行流程。通过这一实战,你将掌握 eBPF 与 kprobe 协作的核心模式。
4.1 最小化 kprobe eBPF 程序结构
一个完整的 kprobe eBPF 程序通常包含两个部分:内核态 eBPF 字节码(通常以 .bpf.c 结尾)和用户态加载器(C 或 Python 等)。最小化版本仅需内核态代码即可通过 bpftrace 或 bpftool 验证。
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 来触发事件。
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 |
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 | 调试、手动加载、测试 | 命令行灵活、无需编写加载器 | 不适合持续运行的程序 |
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。