Gobee:用Go编写eBPF程序,通过clang转译
摘要
Gobee是一个将Go的子集转译为BPF C的工具,允许开发者用Go而非C编写eBPF程序。它为用户空间生成类型化的Go绑定,并利用clang的后端进行编译。
查看缓存全文
缓存时间: 2026/05/21 18:17
boratanrikulu/gobee 源码:https://github.com/boratanrikulu/gobee
gobee
用 Go 写 BPF 程序,不用 C。
gobee 将 Go 的一个严格子集转译为 BPF C 代码,为用户空间侧生成类型安全的 Go 绑定,并在加载时检查内核版本是否满足要求。
Go 生态已经为 BPF 提供了可靠的用户空间工具链。但内核侧始终以“现在用 C 写你的程序”告终。Aya 通过在 rustc 中编写一个全新的 BPF 后端,将 eBPF 带到了 Rust 中。gobee 则走了一条不同的路:转译到 C,并复用 clang 成熟的后端。
一个 Go 文件输入,一个 BPF 程序输出
一个通过 ringbuf 将每次 execve 事件流式传输到用户空间的 tracepoint:
你的输入(Go) gobee 输出的(BPF C)
//go:build ignore
package main
import "github.com/boratanrikulu/gobee/bpf"
//bpf:license GPL
type Event struct {
Pid uint32
Comm [16]byte
}
var Events = bpf.RingBuf[Event]{
MaxEntries: 4096,
}
//bpf:section tracepoint/syscalls/sys_enter_execve
func OnExec(ctx *bpf.ExecveEnterCtx) bpf.TpReturn {
e, ok := Events.Reserve()
if !ok {
return bpf.TpOk
}
e.Pid = bpf.GetCurrentPid()
bpf.GetTaskComm(&e.Comm)
Events.Submit(e)
return bpf.TpOk
}
func main() {}
// Code generated by gobee. DO NOT EDIT.
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
char _license[] SEC("license") = "GPL";
struct Event {
__u32 Pid;
__u8 Comm[16];
};
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 4096);
} Events SEC(".maps");
SEC("tracepoint/syscalls/sys_enter_execve")
int OnExec(struct trace_event_raw_sys_enter *ctx) {
struct Event *e = bpf_ringbuf_reserve(
&Events, sizeof(struct Event), 0);
if (!e) {
return 0;
}
e->Pid = (__u32)(
bpf_get_current_pid_tgid() >> 32);
bpf_get_current_comm(&e->Comm, 16);
bpf_ringbuf_submit(e, 0);
return 0;
}
gobee translate --bindings-dir ./bpf ./bpf/src 会生成这两个文件,外加一个 sourcemap(events.bpf.c.map)使验证器报错能映射回 Go 行号,以及一个类型化的绑定文件(bpf/events_bindings.go),这样用户空间驱动就可以写 objs.Events、objs.AttachOnExec(),并将 ringbuf 载荷直接解码为 bpf.Event(就是上面看到的那个结构体,重新发布为 Go 类型),而不需要字符串类型的 coll.Programs["..."] 查找。
C 代码特意保持可读。如果 gobee 生成了奇怪的东西,你一眼就能看到。关于 tracepoint + kprobes + XDP 合并到一个二进制文件的例子,请参见 example/sysmon/。
对比
| gobee | C + clang + bpf2go | Aya (Rust) | bpftrace | BCC | |
|---|---|---|---|---|---|
| 内核侧语言 | Go 子集 | C | Rust | DSL | C |
| 用户空间集成 | 类型化 Go 绑定 + cilium/ebpf | bpf2go | aya-runtime | 无 | python |
| CO-RE | ✅ 通过 clang | ✅ | ✅ 通过 LLVM | ✅ | ✅ |
| 辅助函数覆盖率 | 200 个类型化 Go 包装 | 完整(写 C) | 完整 | 有限 | 完整(写 C) |
| 验证器错误 → 源码 | ✅ Go 文件:行:列 | ❌ 原始 C | ✅ Rust 文件:行 | ❌ | 部分 |
| 加载时内核版本门控 | ✅ 通过 bpfvet | 手动 | 手动 | 不适用 | 运行时 |
| 工具链依赖 | Go + clang | clang + bpf2go | rustc + LLVM | bpftrace | python + bcc |
| 生成产物 | .bpf.o + Go 二进制 | .bpf.o + Go 二进制 | .bpf.o + Rust 二进制 | JIT | JIT |
如果你已经熟悉 C / libbpf 的工作流程,gobee 并不想完全替代它。它适用于那些希望将内核侧、用户空间侧和构建流水线全部放在一个 Go 模块中的场景。
当前支持的功能
完整的支持矩阵(Go 子集、语句、表达式、每个辅助函数、每种 map 类型、每个指令)请参见 docs/status.md。快速概览:
| 方面 | 覆盖度 |
|---|---|
| 程序类型(8种) | XDP,tracepoint,kprobe / kretprobe,uprobe / uretprobe,sock_ops,TC,cgroup_skb,LSM |
| Map 类型(19种) | array,hash,lru_hash,per-CPU 变体,bloom_filter,lpm_trie,ringbuf,perf_event_array,prog_array,queue,stack,sk/task/inode storage,devmap/cpumap/xskmap |
| BPF 辅助函数 | 约 200 个类型化 Go 存根,从 libbpf v1.5.0 头文件自动生成。 example/helloworld/ 和 example/sysmon/ 中用到的已经在真实内核 CI 中测试;其余未验证。如果存根与内核签名不匹配,请提交 issue。 |
| CO-RE | ✅ 自动检测。对内核内部结构体字段(task_struct、sock、inode)使用 BPF_CORE_READ;对 UAPI BPF 上下文结构体(xdp_md、__sk_buff、bpf_sock_ops)直接使用 ctx->field。在 Linux 6.x(Ubuntu 24.04 CI)上测试;更老的内核尚未加入 CI 矩阵。 |
| BTF 就绪输出 | ✅ 生成的 C 代码包含 vmlinux.h,并对内核内部字段读取使用 BPF_CORE_READ,因此 clang -g 生成的 BTF 会携带正确的重定位信息。clang 本身仍由你负责(示例 Makefile 展示了规范的调用方式)。 |
| 用户定义辅助函数 | ✅ 没有 //bpf:section 的顶层 Go 函数会被发射为 static __always_inline C 函数。 |
| 类型化 Go 绑定 | ✅ Load、Close、每个程序的 Attach、AttachAll,以及你的内核侧结构体类型和常量重新发布为 Go 类型。 |
| 内核版本门控 | ✅ bpfvet(https://github.com/boratanrikulu/bpfvet)在加载时运行。快速失败,显示 bpf program needs kernel >= 5.8, host is 5.4,而不是晦涩的 EINVAL。 |
| 验证器错误 → Go 源码 | ✅ 在 Load 内部自动添加注释。无需手动通过管道传给 gobee diagnose;*ebpf.VerifierError 会返回带有 → counter.go:18:5 标记的错误。 |
| Sourcemap 侧边文件 | ✅ 每个 .bpf.c 旁边都会生成一个 .bpf.c.map 文件,也可用于离线 gobee diagnose。 |
| 跨架构 | ✅ Linux arm64 + amd64 |
gobee 做了什么
- 将 Go 子集转译为 BPF C(并先对你的输入运行
go/types,因此误用会以file:line:col的形式暴露出来)。 - 在
.bpf.c旁边生成一个类型化的_bindings.go文件:bpf.LoadCounter(spec)、objs.PerIface.Lookup(...)、objs.AttachAll(ifindex),以及你的内核侧结构体类型和常量重新发布为 Go 类型。 - 自动在
LoadAndAssign返回的*ebpf.VerifierError中添加 Go 源码位置注释,无需手动通过管道传给gobee diagnose。 - 在
Load内部运行 bpfvet(https://github.com/boratanrikulu/bpfvet),使旧内核快速失败,显示bpf program needs kernel >= 5.8, host is 5.4。 - 提供约 200 个类型化 Go 存根,对应 libbpf v1.5.0 的辅助函数集合,以及用户定义的辅助函数(被发射为
static __always_inline)。
gobee 不会做什么
- 不会替代 clang。 clang 的 BPF 后端为我们免费提供了 CO-RE、BTF 和验证器友好的代码生成。重新实现它需要数年时间且没有收益。
- 不会替代
cilium/ebpf。 生成的绑定位于其之上。 - 不会隐藏 BPF。 Go 子集与 BPF C 惯用法是 1:1 映射的。如果你懂 BPF,gobee 只是一层薄糖;如果你不懂,你仍然需要阅读手册。
- 不会替你运行 clang。 编译、嵌入和加载仍然是用户的责任。模式与 bpf2go 相同。
为什么选择转译而不是直接生成 BPF
gc(Go 编译器)没有基于 LLVM 的 BPF 后端。添加一个需要多年的编译器工程。而 rustc 基于 LLVM,这就是 Aya 能够工作的原因。因此 gobee 发出 C 代码并复用 clang 的 BPF 后端,这为我们免费提供了成熟的代码生成、BTF 和 CO-RE 重定位。
快速开始
go install github.com/boratanrikulu/gobee/cmd/gobee@latest
cd example/helloworld
make build # gobee translate, clang, go build
sudo ./helloworld eth0
你需要一个包含 BPF 目标的 clang。在 Linux 上,这是发行版包;在 macOS 上,使用 brew install llvm。转译器本身是纯 Go 的,可以在任何平台上运行。
项目布局(典型)
yourproject/
├── bpf/ # Go 包,项目任意位置可导入
│ ├── embed_amd64.go # //go:embed bin/x86/your.bpf.o
│ ├── embed_arm64.go
│ ├── your_bindings.go # gobee 生成
│ ├── bin/{x86,arm64}/your.bpf.o
│ └── src/ # 不是 Go 包;clang 在这里工作
│ ├── your.go # //go:build ignore: BPF 源码
│ ├── your.bpf.c # 生成
│ ├── Makefile # 按架构运行 clang
│ └── vmlinux.h # 供应的 BTF 转储
├── main.go # 导入 yourproject/bpf
└── Makefile
这种分离使得 bpf/ 成为一个干净的、可导入的 Go 包(Go 会拒绝非 cgo 包中的 .c 文件)。内核源码和 clang 产物在 bpf/src/ 子目录中。
示例
example/helloworld/:经典的 XDP 包计数器,约 40 行 BPF,约 80 行用户空间。example/sysmon/:在同一个二进制文件中包含 XDP、两个 tracepoint 和一个 kprobe,共享一个 ringbuf 用于事件。演示了每个系统调用的类型化上下文、用户定义辅助函数和AttachAll快捷方式。
CI
GitHub Actions 在每次推送时运行四层测试:
go test、go vet、转译器黄金测试- 覆盖矩阵:每种 map 类型和每种
//bpf:section种类至少有一个示例 - 对所有精选示例进行 clang 编译,然后 bpfvet 可移植性报告
- 真实内核验证器验收:在每个
.bpf.o上运行ebpf.NewCollectionWithOptions(Ubuntu 24.04 runner,内核 6.x)
文档
docs/design.md:架构与设计原理docs/go-subset.md:BPF 源文件中可接受的 Go 语法docs/directives.md://bpf:*参考docs/status.md:支持矩阵(唯一真相来源)
工具链
- gobee 二进制可在任意平台构建(纯 Go,无 CGO)。
- 编译
.bpf.o需要包含 BPF 目标的 clang。Apple 捆绑的 clang 不包含该目标;macOS 上使用brew install llvm或在 Linux VM 内构建。 - 运行产物需要 Linux arm64 或 amd64。
灵感来源
- Solod(https://github.com/solod-dev/solod):证明 Go 到 C 转译模式可行的项目。
- Aya(https://github.com/aya-rs/aya):Rust eBPF 框架,gobee 追求其易用性。
许可证
MIT。参见 LICENSE。
Copyright (c) 2026 Bora Tanrikulu [email protected]
相似文章
GCC 16及以后版本中的BPF支持
何塞·马奇西(José Marchesi)和GCC-BPF团队提供了GCC 16中BPF支持的更新,突出了在与LLVM功能对等方面取得的进展,以及内核BPF自测通过率的提升。
Go 语言服务器可以实现令人印象深刻的代码导航
Go 语言服务器 (gopls) 为 Go 开发者提供了令人印象深刻的代码导航功能,增强了 IDE 的能力。
QBE – 编译器后端
QBE 是一个紧凑的、爱好级别的编译器后端,仅用 10% 的代码即可实现工业级优化编译器 70% 的性能,支持 amd64、arm64 和 riscv64,并采用简单的基于 SSA 的中间语言。
Blaise – 一款面向 QBE 的现代、自举、无历史包袱的 Object Pascal 编译器
Blaise 是一款现代且自举的 Object Pascal 编译器,旨在通过提供单一语言模式、统一的内存模型以及基于 QBE 的原生代码生成,来消除遗留系统的负担。
优化CPU密集型Go热路径的笔记
本文讨论了CPU密集型Go代码的性能优化技术,指出了泛型和接口抽象因无法内联而产生的局限性,并主张在热路径中使用代码复制。文章通过一个Brotli移植示例和深入基准测试进行了说明。