可行,但需通过eBPF程序挂载到内核钩子(如tracepoint/syscalls/sys_enter_read)并由Go读取perf buffer或map;难点在eBPF编写、加载及上下文安全访问,而非Go本身。

用 Golang 调用 eBPF 追踪文件读写行为是可行的,但不能直接“调用系统调用”——必须通过 eBPF 程序挂载到内核钩子(如 tracepoint/syscalls/sys_enter_read、kprobe/do_filp_open 等),再由用户态 Go 程序读取 perf event ring buffer 或 map 数据。关键难点不在 Go,而在 eBPF 程序的编写、加载和上下文安全访问。
为什么不能直接在 Go 里 hook open/read/write
eBPF 是运行在内核受限环境中的字节码,Go 运行时无法直接注入或拦截系统调用。所有追踪逻辑必须由 eBPF 程序完成,Go 只负责:
- 编译并加载 eBPF 字节码(通常借助
libbpf-go或ebpf-go) - 打开并轮询 perf buffer 或 ring buffer 获取事件
- 解析传入的结构体(如
struct pt_regs、文件路径字符串) - 处理用户态符号解析(如从
pid_tgid查进程名,需读/proc/[pid]/comm)
推荐用 libbpf-go + CO-RE 加载 tracepoint 程序
libbpf-go 是目前最稳定、适配主流内核(5.10+)且支持 CO-RE 的 Go 绑定。相比 ebpf-go(纯 Go 实现的 verifier bypass 方案,已基本弃用),它复用内核 libbpf,兼容性好、错误提示清晰。
实操要点:
立即学习“go语言免费学习笔记(深入)”;
- 用
bpftool btf dump file /sys/kernel/btf/vmlinux format c确认内核 BTF 可用(CO-RE 必需) - eBPF C 代码中优先使用
tracepoint/syscalls/sys_enter_openat和sys_exit_openat,比 kprobe 更稳定、无需处理寄存器偏移 - 在 eBPF 程序里用
bpf_probe_read_user_str()安全拷贝用户态路径(filename参数),避免-EFAULT - Go 侧用
perf.NewReader()订阅事件,注意设置足够大的 ring size(如 4MB),否则丢事件
常见崩溃点:路径读取越界与跨 CPU map 冲突
eBPF 程序里直接对 args->filename 做 bpf_probe_read_str() 很容易触发 verifier 拒绝,尤其当该指针为空或跨页未映射。更稳妥的方式是:
- 先用
bpf_probe_read_user(&addr, sizeof(addr), &args->filename)读出地址值 - 再用
bpf_probe_read_user_str(buf, sizeof(buf), addr)分两步读字符串 - perf event 结构体中不要放可变长字段(如 char path[256]),改用固定长度 + 显式 len 字段,Go 侧按 len 截取
- 避免在 eBPF 中更新全局 map 计数器来统计“某文件被读了多少次”——多 CPU 并发写会冲突;改用 perf event 推送原始事件,聚合交给 Go 做
Go 解析事件时注意 pid/tgid 和文件描述符生命周期
eBPF 事件里拿到的 pid 实际是 pid_tgid >> 32,而 tgid 是进程组 ID(即主线程 PID)。仅靠这个无法知道是哪个进程打开了文件,需结合:
- 事件时间戳 +
pid,去/proc/[pid]/fd/[fd]读符号链接(但该 fd 可能已被 close) - 更可靠的是在
sys_enter_openat事件中记录filename和pid,在sys_enter_read中查fd对应的struct file *—— 但这需要 kprobe + bpf_get_current_task() + 遍历 files_struct,复杂度陡增 - 实用妥协方案:只打点
openat/creat/close事件,配合read/write的 fd 和 count,不强行关联路径,靠用户自己按 pid + 时间窗口对齐
真正难的不是写几行 Go 启动 eBPF,而是理解每个 tracepoint 的参数布局、用户态地址有效性边界、以及 perf buffer 丢包后如何做应用层重试或降级。别迷信 “自动解析路径”,先确保事件不 crash、不丢,再考虑丰富字段。


















