eBPF 开发语言之争:C、Rust 与 Zig 全链路对比
前面几篇讲 OOM 追踪时,所有 eBPF 内核态程序都是用 C 写的。这很自然——C 是 eBPF 的"母语",verifier、CO-RE、libbpf 整个工具链都是围绕 C 设计的。但如果你关注过 eBPF 生态,会发现一个明显的趋势:越来越多的人开始用 C 以外的语言写 eBPF。Rust 的 Aya 框架已经被 Solana 验证器、Kubernetes Gateway API 用在生产环境;而 Zig 也在尝试用 comptime、显式分配和一流的 C 互操作带来新的开发体验。
这篇文章换一个视角:不再聚焦某个具体场景,而是做一次全链路对比——同样一个 eBPF 程序,用 C、Rust、Zig 分别怎么写?内核态怎么编译?用户态加载器怎么实现?三者真正的技术分水岭在哪里?
时效性说明:本文基于 2026 年 6 月的工具链现状。Zig 的 BPF CO-RE 支持仍是 Post-1.0 目标,Aya 的编译器级 CO-RE 重定位也在进行中——这些都在快速演进,部分结论可能随版本变化。
eBPF 全链路开发模型
无论用什么语言,一个完整的 eBPF 工具都由两部分组成:跑在内核态的 BPF 程序,和跑在用户态的控制程序。理解这个模型,是对比三种语言的前提。
flowchart LR
subgraph 用户态["用户态 User Space"]
LD["Loader<br/>打开 .o、加载、attach"]
POLL["控制循环<br/>读 maps / ringbuf"]
end
subgraph 内核态["内核态 Kernel"]
HOOK["Hook 点<br/>kprobe / tracepoint / xdp"]
BPF["BPF 程序<br/>verifier 校验后运行"]
MAP["Maps / RingBuf<br/>内核 ↔ 用户数据通道"]
end
LD -- "bpf 系统调用" --> BPF
BPF -- "挂载到" --> HOOK
HOOK -- "事件触发执行" --> BPF
BPF -- "读写" --> MAP
MAP -- "用户态读取" --> POLL
classDef user fill:#3b82f6,color:#fff
classDef kern fill:#f59e0b,color:#fff
class LD,POLL user
class HOOK,BPF,MAP kern两段代码、两个目标、一个 ELF 文件把它们粘起来:
- 内核态程序编译成一个 BPF ELF 对象(
.o),里面包含字节码、map 定义、BTF 信息。它必须通过 verifier 的校验才能运行。 - 用户态加载器负责把这个
.o加载进内核、处理 CO-RE 重定位、attach 到具体的 hook 点,然后读 map / ringbuf 把数据取出来。
三种语言的差异,本质就是这两段代码分别用什么写、用什么工具链粘合。
C + libbpf:不可动摇的基准线
C 是 eBPF 的事实标准。内核文档、Cilium、bcc、Tetragon、所有主流发行版的 BPF 工具——清一色 C + libbpf。后来者都以它为参照系。
典型开发范式是 libbpf-bootstrap:用 Clang 编译内核态程序,通过 BPF_CORE_READ 宏访问内核结构体(这就是 CO-RE),再用 bpftool gen skeleton 生成类型安全的加载器骨架。
内核态程序(C + libbpf):
| |
用户态加载器(libbpf skeleton):
| |
vmlinux.h 是 bpftool btf dump 从目标内核生成的所有内核结构体定义,配合 BPF_CORE_READ,一个程序能在不同内核版本上运行而不需重新编译——这就是 CO-RE(Compile Once, Run Everywhere)。C 路径的 CO-RE 是编译器内建的、最完整的,这也是它至今不可动摇的根本原因。
Rust + Aya:内存安全的最强挑战者
Aya 是 Rust 生态的 eBPF 框架,aya-rs/aya 截至 2026 年 6 月已有 4.6k stars,最新版本 v0.14.0。它最大的卖点是纯 Rust、无 C 工具链依赖:内核态用 aya-ebpf(#![no_std])编译成 BPF 字节码,用户态用 aya 加载,全程不需要 Clang、libbpf、kernel headers。
内核态程序(Aya,#![no_std]):
| |
用户态加载器(Aya):
| |
Aya 真正的生产实力不在代码本身,而在它的用户名单:Solana 验证器的 XDP 快速包重传(agave-xdp)、Kubernetes SIGs 的 L4 负载均衡器 blixt、Red Hat 的 bpfman、Deepfence 的 ebpfguard。这是目前非 C 阵营里唯一被大规模生产验证的方案。
Aya 还带来了 C 路径没有的东西:类型安全的 map 访问、tokio 原生异步(AsyncFd 直接对接 ringbuf)、所有权系统在编译期挡掉一类错误。
Zig + zbpf:潜力与局限并存的探索者
Zig 是三者中最年轻的尝试。Zig 工具链本身支持 BPF 目标——zig cc -target bpfel 能编译出 BPF ELF 对象,Zig 源码 src/target.zig 把 bpfel/bpfeb 明确列为 LLVM 支持的目标架构。但官方平台支持页把 BPF 列为 “Additional Platforms”,不在 Tier 评级体系内,意味着可用但不保证正确性。
| |
社区里最成熟的是 tw4452852/zbpf(275 stars)。它不走 @cImport(libbpf) 的老路,而是实现了一套纯 Zig 的用户态加载器:自带 ELF 解析、BTF 解析、CO-RE 重定位、skeleton 代码生成,零外部依赖(不需要 libelf、libbpf、libz)。
内核态程序(Zig + zbpf,示意):
| |
用户态加载器(Zig + zbpf):
| |
Zig 的吸引力在于语言特性:comptime 能在编译期做类型检查、显式分配器让内存来源一目了然、@cImport 让 C 互操作几乎无成本。构建一条命令 zig build zbpf -Dbpf=probe.zig -Dmain=main.zig 就把内核态和用户态打成一个二进制,交叉编译也是零配置。
但 Zig 写 eBPF 有一个绕不开的硬伤。
CO-RE:三种语言的技术分水岭
这是整篇文章最关键的部分。CO-RE 不是"有没有"的二选一,而是"在哪一层实现"的三档,它决定了你的程序能不能跨内核版本运行。
CO-RE 的本质是:BPF 程序访问内核结构体字段时不硬编码偏移量,而是编码成 BTF 重定位记录,加载时由用户态 loader 根据目标内核的真实布局重写指令。要做到这点,编译器必须发射一种特殊的 LLVM intrinsic(llvm.preserve.struct.access.index 等)。
| 语言 | CO-RE 实现层级 | 机制 | 成熟度 |
|---|---|---|---|
| C (libbpf) | 编译器内建 | Clang 发射 BTF 重定位 intrinsics,BPF_CORE_READ 宏 | 生产级 |
| Rust (Aya) | 用户态重定位 | aya-obj 解析 ELF 做重定位;Rust 编译器侧的 BTF relo 尚未完成 | 可用 |
| Zig (zbpf) | 用户态 workaround | zbpf 在 ELF 层实现全部 16 种重定位;Zig 编译器不发射 intrinsics | 实验性 |
C 的优势是原生:Clang 直接认识 CO-RE,BPF_CORE_READ 是一等公民,跨内核运行是编译器保证的。
Aya 处于过渡期:根据 2026 年 FOSDEM 演讲,Rust 已经能为所有类型产生正确的 BTF,但编译器侧的 BTF 重定位 intrinsics 尚未完成。目前 Aya 在用户态(aya-obj)做重定位来补偿,所以能用,但还不是编译器原生保证。这条路线官方正在推进。
Zig 卡在最难的一步:Codeberg issue #35324 明确指出,Zig 编译器不会发射 CO-RE 所需的 intrinsics,所以"无法现实地用于编写 eBPF 程序"。zmitchell 正在 Air.zig 里实现这些 intrinsic,但 Andrew Kelley 把它标记为 Post-1.0 里程碑——也就是 1.0 之后才会考虑,目前没有时间表。zbpf 的 CO-RE 是在 ELF 层手动实现的 workaround,能用但不是编译器保证的等价物,访问复杂嵌套结构体时仍可能受限。
这一层差异,是 Zig 暂时无法和 C、Aya 平起平坐的根本原因。
全链路对比
| 维度 | C (libbpf) | Rust (Aya) | Zig (zbpf) |
|---|---|---|---|
| 内核态语言 | C | Rust(#![no_std]) | Zig(freestanding) |
| 用户态语言 | C / Go | Rust | Zig |
| CO-RE / BTF | 编译器内建,完整 | BTF 完整,编译器 relo 进行中 | ELF 层 workaround,编译器无内建 |
| C 工具链依赖 | 需要 Clang | 不需要 | 编译 BPF 时仍需 Clang 后端 |
| 内存安全 | 无保证 | 所有权系统编译期保证 | 显式分配,运行时部分检查 |
| 异步运行时 | 手动 epoll | tokio / async-std 原生 | 无原生 async |
| 生态规模 | 最大(Cilium、bcc、Tetragon) | 大(4.6k stars,7+ 生产用户) | 小(275 stars,1 主维护者) |
| 生产案例 | 行业标准 | Solana、K8s、Red Hat、Deepfence | 无公开生产案例 |
| Skeleton 生成 | bpftool gen skeleton | aya-tool / xtask | zbpf gen-skeleton |
| 内核版本兼容 | ≥ 4.18(CO-RE) | ≥ 5.8(ringbuf) | 取决于特性 |
| 编译速度 | 慢(Clang + kernel headers) | 快(纯 Rust) | 快(Zig 工具链) |
| 学习曲线 | 中(C + libbpf 复杂) | 中(Rust + Aya 概念) | 中(Zig 简单但生态薄) |
成熟度判定与现实选型
诚实地说:
- C + libbpf:生产就绪,行业标准。CO-RE 最完整、生态最厚、文档最多。新项目没有特殊理由,它仍是默认选择。代价是要忍受 C 的内存不安全和较重的工具链。
- Rust + Aya:生产可用,是今天唯一能替代 C 写生产 eBPF 的非 C 方案。如果你的团队已经用 Rust,或者需要内存安全 + 异步,Aya 是明确的选择。唯一要注意的是编译器级 CO-RE 还在路上,复杂场景需要测试。
- Zig + zbpf:实验性。技术探索很有想象力(纯 Zig loader、零依赖、comptime 类型安全),但编译器级 CO-RE 缺失意味着跨内核运行没有保证,生态也太小(单一维护者)。适合学习、做技术预研、给社区贡献,但不建议上生产。
一个务实的判断:如果你今天要选一个非 C 语言写 eBPF,答案只有一个——Rust + Aya。Zig 的故事很性感,但它的关键拼图(CO-RE)还在 Post-1.0 的 roadmap 上,至少短期内不是生产选项。不过 zbpf 的纯 Zig loader 实现值得每个 eBPF 爱好者读一遍——它用一门语言重新实现了 libbpf 的核心,本身就是对 eBPF 工具链最好的深度学习。
小结
这篇文章从语言生态的视角梳理了 eBPF 的三种开发路径:
- C + libbpf 仍是基准线,CO-RE 编译器内建是它的护城河
- Rust + Aya 是唯一生产可用的非 C 方案,内存安全 + 异步是真实价值
- Zig + zbpf 是有潜力的探索者,但卡在编译器级 CO-RE,目前只适合实验
三种语言的真正分水岭不是语法,而是 CO-RE 在哪一层实现——编译器内建、用户态重定位、还是 ELF 层 workaround。理解了这一点,就能看懂每个项目的成熟度天花板。
eBPF 的语言之争才刚开始。后面会继续往这个方向探索——可能会动手用 zbpf 真正写一个工具,也可能去拆解 Aya 的 CO-RE 重定位实现。这个系列还会继续往下走。