如何用Go开发eBPF程序

简介: 【2月更文挑战第7天】

使用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 程序。

相关文章
|
2月前
|
开发框架 安全 中间件
Go语言开发小技巧&易错点100例(十二)
Go语言开发小技巧&易错点100例(十二)
31 1
|
10天前
|
前端开发 Java Go
开发语言详解(python、java、Go(Golong)。。。。)
开发语言详解(python、java、Go(Golong)。。。。)
|
1月前
|
JSON Go API
Go语言网络编程:HTTP客户端开发实战
【2月更文挑战第12天】本文将深入探讨使用Go语言开发HTTP客户端的技术细节,包括发送GET和POST请求、处理响应、错误处理、设置请求头、使用Cookie等方面。通过实例演示和代码解析,帮助读者掌握构建高效、可靠的HTTP客户端的关键技术。
|
2月前
|
监控 Java 编译器
优化Go语言程序中的内存使用与垃圾回收性能
【2月更文挑战第5天】本文旨在探讨如何优化Go语言程序中的内存使用和垃圾回收性能。我们将深入了解内存分配策略、垃圾回收机制,并提供一系列实用的优化技巧和建议,帮助开发者更有效地管理内存,减少垃圾回收的开销,从而提升Go程序的性能。
|
2月前
|
Go
Go语言开发小技巧&易错点100例(十一)
Go语言开发小技巧&易错点100例(十一)
16 0
|
2月前
|
存储 Java Go
Go语言开发小技巧&易错点100例(十)
Go语言开发小技巧&易错点100例(十)
19 0
|
2月前
|
Go 开发者
Go语言开发小技巧&易错点100例(九)
Go语言开发小技巧&易错点100例(九)
15 0
|
2月前
|
JSON Go 数据格式
Go语言开发小技巧&易错点100例(八)
Go语言开发小技巧&易错点100例(八)
17 0
|
2月前
|
消息中间件 IDE Go
Go语言开发小技巧&易错点100例(七)
Go语言开发小技巧&易错点100例(七)
18 0
|
11月前
|
安全 Go Python
GO语言开发GUI安全工具实践(一)
GO语言开发GUI安全工具实践