为什么 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_openattracepoint),且已安装
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,是设计使然。
