C# eBPF文件访问监控 C#在Linux上如何使用eBPF跟踪文件系统调用

来源:这里教程网 时间:2026-02-21 17:42:47 作者:

为什么 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,是设计使然。

相关推荐