使用Go开发eBPF程序可以通过以下三个步骤完成:
第一步,使用 C 语言开发内核态 eBPF 程序,这一步跟 libbpf 方法是完全相同的。
新建一个 hello.bpf.c 文件,然后写入内核态 eBPF 程序即可。
/* 由于我们并不需要cgo,这儿需要通过Go构建标签来排除C源文件,否则Go编译会报错 */ //go:build ignore #include <linux/bpf.h> #include <bpf/bpf_helpers.h> /* 定义BPF映射,用于存储网络包计数*/ struct { __uint(type, BPF_MAP_TYPE_ARRAY); __type(key, __u32); __type(value, __u64); __uint(max_entries, 1); } pkt_count SEC(".maps"); /* XDP程序入口,统计网络包数量并存入BPF映射 */ SEC("xdp") int count_packets() { __u32 key = 0; __u64 *count = bpf_map_lookup_elem(&pkt_count, &key); if (count) { __sync_fetch_and_add(count, 1); } return XDP_PASS; } char __license[] SEC("license") = "Dual MIT/GPL";
这其中,
- //go:build ignore 表示 Go 编译时忽略 C 文件;
- pkt_count 定义了一个用于存储网络包计数的 BPF 映射;
- SEC("xdp") 定义了 XDP 程序的入口函数 count_packets。
从这段代码你可以发现,这儿的代码跟 libbpf 方法是一样的。只有一点需要注意的是 // go:build ignore 这一行是必不可少的,它的意思是让 Go 编译时忽略 C 源码文件。由于我们只是用 C 语言开发 eBPF 程序,并不需要通过 cgo 去直接调用内核态 eBPF 程序代码,所以在编译 Go 代码时应该忽略 C 源码文件。
第二步,借助 go generate 命令,使用 cmd/bpf2go 编译 eBPF 程序,并生成 Go 语言脚手架代码。
有了 eBPF 程序代码之后,接下来就是利用 cmd/bpf2go 来编译并生成 Go 脚手架代码了。创建一个 main.go 文件,并写入如下的代码。
package main //go:generate go run github.com/cilium/ebpf/cmd/bpf2go hello hello.bpf.c
这段代码最关键的是第二句 go:generate 注解,用于在执行 go generate 时自动执行 cmd/bpf2go 命令。cmd/bpf2go 命令需要两个参数,第一个 hello 是生成文件名的前缀,而第二个参数 hello.bpf.c 就是我们第一步开发的 eBPF 程序。
在执行 go generate 命令之前,你还需要执行下面的命令,初始化一个 Go 模块,并添加对 github.com/cilium/ebpf/cmd/bpf2go 的依赖。
go mod init hello go mod tidy go get github.com/cilium/ebpf/cmd/bpf2go
接下来,你就可以执行 go generate 命令,编译并生成 Go 语言脚手架代码。如果一切顺利,你将看到如下输出:
$ go generate Compiled /ebpf-apps/go/hello/hello_bpfel.o Stripped /ebpf-apps/go/hello/hello_bpfel.o Wrote /ebpf-apps/go/hello/hello_bpfel.go Compiled /ebpf-apps/go/hello/hello_bpfeb.o Stripped /ebpf-apps/go/hello/hello_bpfeb.o Wrote /ebpf-apps/go/hello/hello_bpfeb.go
这其中,.o 文件就是编译目标文件, .go 文件就是对应的脚手架代码,而后缀 bpfel 和 bpfeb 则分别表示该文件用于小端系统和大端系统。
第三步,使用 cilium/ebpf 库配合上一步生成的脚手架代码开发用户态程序,包括 eBPF 程序加载、挂载到内核函数和跟踪点,以及通过 BPF 映射获取和打印执行结果等。
有了脚手架代码之后。可以在 main.go 里面继续添加 main() 函数,添加 eBPF 程序加载、挂载到 XDP,以及通过 BPF 映射获取和打印执行结果等执行逻辑。
// 1. 引入必要的依赖库 import ( "log" "net" "os" "os/signal" "time" "github.com/cilium/ebpf/link" "github.com/cilium/ebpf/rlimit" ) func main() { // 2. 移除内核<5.11的资源限制 if err := rlimit.RemoveMemlock(); err != nil { log.Fatal("Removing memlock:", err) } // 3. 调用脚手架函数,加载编译后的 eBPF 字节码 var objs helloObjects if err := loadHelloObjects(&objs, nil); err != nil { log.Fatal("Loading eBPF objects failure:", err) } defer objs.Close() // 4. 挂载 XDP 程序到网卡上 ifname := "eth0" iface, err := net.InterfaceByName(ifname) if err != nil { log.Fatalf("Getting interface %s failure: %s", ifname, err) } link, err := link.AttachXDP(link.XDPOptions{ Program: objs.CountPackets, Interface: iface.Index, }) if err != nil { log.Fatal("Attaching XDP failure:", err) } defer link.Close() log.Printf("Counting incoming packets on %s..", ifname) // 5. 定期查询并打印数据包计数(Ctrl+C退出) tick := time.Tick(time.Second) stop := make(chan os.Signal, 5) signal.Notify(stop, os.Interrupt) for { select { case <-tick: var count uint64 err := objs.PktCount.Lookup(uint32(0), &count) if err != nil { log.Fatal("Map lookup failure:", err) } log.Printf("Received %d packets", count) case <-stop: log.Print("Received stop signal, exiting..") return } } }
这段代码的主要逻辑跟 libbpf 方法也是类似的,所不同的只是编程语言和库函数的不同。另外,这段 Go 代码里面的 eBPF 程序名 CountPackets 和 BPF 映射名 PktCount 分别对应第一步 eBPF C 代码里面的 count_packets 和 pkt_count,这是 cmd/bpf2go 自动将 C 命名格式转换为 Go 的驼峰命名法导致的(即不使用下划线且单词首字母大写)。
代码开发完成后,你就可以编译并执行用户态的程序了。执行 go build 命令编译 Go 程序后并执行 ./hello 运行它,如果一切正常,你将看到如下的输出:
$ go build $ ./hello 2023/12/30 14:19:49 Counting incoming packets on eth0.. 2023/12/30 14:19:50 Received 9 packets 2023/12/30 14:19:51 Received 16 packets 2023/12/30 14:19:52 Received 20 packets
到这里已经使用 Go 语言成功开发并运行了第一个 eBPF 程序。