c#不能直接加载或运行ebpf程序,因其运行在用户态.net runtime中,无内核权限且无法生成合法ebpf指令;必须通过libbpf(如libbpf.net)加载预编译的elf格式ebpf程序,并借助tracepoint和perf buffer实现监控。

为什么 C# 不能直接加载或运行 eBPF 程序
eBPF 是内核态字节码,必须由内核验证器加载、校验并附着到钩子(如 tracepoint/syscalls:sys_enter_openat)上。C# 运行在用户态 .NET Runtime 中,没有内核权限,也无法生成合法的 eBPF 指令。你写的 C# 代码本身不会“变成 eBPF”,它只能作为控制端,通过系统接口和已编译的 eBPF 程序通信。
常见错误现象:System.PlatformNotSupportedException 或 Operation not permitted 错误,本质是试图绕过 libbpf / bpftool 直接 mmap 或 write 到 /sys/fs/bpf;或者用 P/Invoke 调 bpf() 系统调用但传入非法指令段。
- 所有 eBPF 程序必须先用
clang+llc编译为 ELF,再用libbpf加载 —— C# 不参与这步 - C# 唯一可行路径是调用
libbpf的 C API(通过P/Invoke)或使用封装好的 .NET 绑定库(如Libbpf.Net) - 别尝试用
MemoryMappedFile或FileStream去读写/sys/fs/bpf/下的对象 —— 权限、格式、生命周期全不匹配
用 Libbpf.Net 在 C# 中 attach eBPF 文件监控程序
Libbpf.Net 是目前最轻量、最贴近原生 libbpf 行为的 .NET 封装,它不隐藏 map 操作、不自动管理 perf ring buffer,适合做文件访问跟踪这类需要低延迟、高精度的场景。
使用前提:你的 Linux 内核 ≥ 5.8(支持 sys_enter_openat tracepoint),且已安装 libbpf 和 libbpf-dev(Ubuntu/Debian)或对应开发包。
- 用
dotnet add package Libbpf.Net引入 NuGet 包 - 确保 eBPF 程序已编译为
fs_monitor.bpf.o(推荐用bpftool gen skeleton生成 C 头,再用 clang 编译) - 在 C# 中用
LibbpfLoader.LoadFromBuffer()加载 ELF,然后调AttachTracepoint("syscalls", "sys_enter_openat") - 从
perf_buffer读事件时,必须手动解析结构体 ——Libbpf.Net不帮你反序列化,字段偏移得自己对齐(比如pid在 offset 0,filename在 offset 8)
示例关键片段:
var bpf = LibbpfLoader.LoadFromBuffer(File.ReadAllBytes("fs_monitor.bpf.o"));
bpf.AttachTracepoint("syscalls", "sys_enter_openat");
using var pb = bpf.OpenPerfBuffer("events", (data, size) => {
var pid = BitConverter.ToInt32(data, 0);
var filenamePtr = BitConverter.ToInt64(data, 8); // 注意:这是内核态地址,需用 bpf_probe_read_user_str 读取
});
文件名读取失败的三个典型原因
eBPF 程序里拿到的是用户态指针(如 struct openat_args *args 中的 filename 字段),直接 *(char *)filename 会触发 verifier 拒绝或返回空 —— 因为内核无法直接访问用户内存,必须用安全函数拷贝。
- 没用
bpf_probe_read_user_str():导致filename字段始终为 0 或乱码;正确写法是bpf_probe_read_user_str(&fname, sizeof(fname), args->filename) - 目标缓冲区太小(比如只开 16 字节):长路径(如
/proc/self/fd/123)被截断,看起来像“随机打开” - 没检查返回值:该函数返回实际读取长度,若为 0 或负值说明读取失败,应跳过该事件,否则后续 memcpy 可能 crash
性能影响:每次调用 bpf_probe_read_user_str 有微小开销,但比用 bpf_probe_read_user + 循环找 \0 更安全;在高频 open 场景下,建议限制日志频率(如每秒最多打 100 条)避免 perf buffer 溢出。
如何让 C# 实时收到 openat 调用的完整路径
内核 tracepoint 只给参数指针,不提供进程当前工作目录(cwd)。所以 openat(AT_FDCWD, "foo.txt", ...) 中的 "foo.txt" 是相对路径,C# 端无法直接还原成绝对路径 —— 必须结合用户态辅助逻辑。
- eBPF 程序中记录
pid和fd(若dfd != AT_FDCWD,可进一步查/proc/[pid]/fd/[fd]) - C# 主程序维护一个
ConcurrentDictionary<int string></int>缓存每个 pid 的 cwd,通过定期读/proc/[pid]/cwd更新(注意:不是每次事件都读,用 Timer 控制频率) - 收到事件后,若
dfd == AT_FDCWD,就拼接cachedCwd + "/" + filename;否则解析/proc/[pid]/fd/[dfd]获取目录路径 - 兼容性注意:容器中
/proc/[pid]/cwd可能是overlay或bind mount,真实路径需用readlink -f辅助,但不要在 eBPF 里做
容易被忽略的一点:不同线程可能同时修改同一个 pid 的 cwd 缓存,ConcurrentDictionary 只保证线程安全,不保证一致性 —— 如果你看到某次 open 显示路径错乱,大概率是 cwd 缓存更新滞后了 100–500ms,这不是 bug,是设计使然。









