Gobee:用Go编写eBPF程序,通过clang转译

Lobsters Hottest 工具

摘要

Gobee是一个将Go的子集转译为BPF C的工具,允许开发者用Go而非C编写eBPF程序。它为用户空间生成类型化的Go绑定,并利用clang的后端进行编译。

<p><a href="https://lobste.rs/s/52g7rz/gobee_write_ebpf_programs_go_transpiled">评论</a></p>
查看原文
查看缓存全文

缓存时间: 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.Eventsobjs.AttachOnExec(),并将 ringbuf 载荷直接解码为 bpf.Event(就是上面看到的那个结构体,重新发布为 Go 类型),而不需要字符串类型的 coll.Programs["..."] 查找。

C 代码特意保持可读。如果 gobee 生成了奇怪的东西,你一眼就能看到。关于 tracepoint + kprobes + XDP 合并到一个二进制文件的例子,请参见 example/sysmon/

对比

gobeeC + clang + bpf2goAya (Rust)bpftraceBCC
内核侧语言Go 子集CRustDSLC
用户空间集成类型化 Go 绑定 + cilium/ebpfbpf2goaya-runtimepython
CO-RE✅ 通过 clang✅ 通过 LLVM
辅助函数覆盖率200 个类型化 Go 包装完整(写 C)完整有限完整(写 C)
验证器错误 → 源码✅ Go 文件:行:列❌ 原始 C✅ Rust 文件:行部分
加载时内核版本门控✅ 通过 bpfvet手动手动不适用运行时
工具链依赖Go + clangclang + bpf2gorustc + LLVMbpftracepython + bcc
生成产物.bpf.o + Go 二进制.bpf.o + Go 二进制.bpf.o + Rust 二进制JITJIT

如果你已经熟悉 C / libbpf 的工作流程,gobee 并不想完全替代它。它适用于那些希望将内核侧、用户空间侧和构建流水线全部放在一个 Go 模块中的场景。

当前支持的功能

完整的支持矩阵(Go 子集、语句、表达式、每个辅助函数、每种 map 类型、每个指令)请参见 docs/status.md。快速概览:

方面覆盖度
程序类型(8种)XDP,tracepoint,kprobe / kretprobe,uprobe / uretprobe,sock_ops,TC,cgroup_skb,LSM
Map 类型(19种)arrayhashlru_hash,per-CPU 变体,bloom_filterlpm_trieringbufperf_event_arrayprog_arrayqueuestack,sk/task/inode storage,devmap/cpumap/xskmap
BPF 辅助函数约 200 个类型化 Go 存根,从 libbpf v1.5.0 头文件自动生成。 example/helloworld/example/sysmon/ 中用到的已经在真实内核 CI 中测试;其余未验证。如果存根与内核签名不匹配,请提交 issue。
CO-RE✅ 自动检测。对内核内部结构体字段(task_structsockinode)使用 BPF_CORE_READ;对 UAPI BPF 上下文结构体(xdp_md__sk_buffbpf_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 绑定LoadClose、每个程序的 AttachAttachAll,以及你的内核侧结构体类型和常量重新发布为 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 在每次推送时运行四层测试:

  1. go testgo vet、转译器黄金测试
  2. 覆盖矩阵:每种 map 类型和每种 //bpf:section 种类至少有一个示例
  3. 对所有精选示例进行 clang 编译,然后 bpfvet 可移植性报告
  4. 真实内核验证器验收:在每个 .bpf.o 上运行 ebpf.NewCollectionWithOptions(Ubuntu 24.04 runner,内核 6.x)

文档

工具链

  • 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支持

Lobsters Hottest

何塞·马奇西(José Marchesi)和GCC-BPF团队提供了GCC 16中BPF支持的更新,突出了在与LLVM功能对等方面取得的进展,以及内核BPF自测通过率的提升。

QBE – 编译器后端

Hacker News Top

QBE 是一个紧凑的、爱好级别的编译器后端,仅用 10% 的代码即可实现工业级优化编译器 70% 的性能,支持 amd64、arm64 和 riscv64,并采用简单的基于 SSA 的中间语言。

优化CPU密集型Go热路径的笔记

Hacker News Top

本文讨论了CPU密集型Go代码的性能优化技术,指出了泛型和接口抽象因无法内联而产生的局限性,并主张在热路径中使用代码复制。文章通过一个Brotli移植示例和深入基准测试进行了说明。