标签 Linux 下的文章

使用Go语言开发eBPF程序

本文永久链接 – https://tonybai.com/2022/07/19/develop-ebpf-program-in-go

在前面的《使用C语言从头开发一个Hello World级别的eBPF程序》一文中,我们详细说明了如何基于C语言和libbpf库从头开发一个eBPF程序(包括其用户态部分)。那篇文章是后续有关eBPF程序开发文章的基础,因为到目前为止,无论eBPF程序的用户态部分用什么语言开发,运行于内核态的eBPF程序内核态部分还是必须由C语言开发的。这样一来,其他编程语言只能拼一下如何让eBPF程序的用户态部分的开发更为简单了,Go语言也不例外。

在Go社区中,目前最为活跃的用于开发eBPF用户态部分的Go eBPF包莫过于cilium项目开源的cilium/ebpf,cilium项目背后的Isovalent公司也是eBPF技术在云原生领域应用的主要推手之一。

本文我们就来说说基于cilium/ebpf开发eBPF程序的套路

一. 探索cilium/ebpf项目示例

cilium/ebpf项目借鉴了libbpf-boostrap的思路,通过代码生成与bpf程序内嵌的方式构建eBPF程序用户态部分。为了搞清楚基于cilium/ebpf开发ebpf程序的套路,我们先来探索一下cilium/ebpf项目提供的示例代码。

我们首先来下载和看看ebpf的示例的结构。

  • 下载cilium/ebpf项目
$ git clone https://github.com/cilium/ebpf.git
Cloning into 'ebpf'...
remote: Enumerating objects: 7054, done.
remote: Counting objects: 100% (183/183), done.
remote: Compressing objects: 100% (112/112), done.
remote: Total 7054 (delta 91), reused 124 (delta 69), pack-reused 6871
Receiving objects: 100% (7054/7054), 10.91 MiB | 265.00 KiB/s, done.
Resolving deltas: 100% (4871/4871), done.
  • 探索ebpf项目示例代码结构

ebpf示例在examples目录下,我们以tracepoint_in_c为例看看其组织形式:

$tree tracepoint_in_c
tracepoint_in_c
├── bpf_bpfeb.go
├── bpf_bpfeb.o
├── bpf_bpfel.go
├── bpf_bpfel.o
├── main.go
└── tracepoint.c

0 directories, 6 files

根据经验判断,这里面的tracepoint.c对应的是ebpf程序内核态部分,而main.go和bpf_bpfel.go/bpf_bpfeb.go则是ebpf程序用户态部分,至于bpf_bpfeb.o/bpf_bpfel.o应该是某种中间目标文件。通过readelf -a bpf_bpfeb.o查看该中间文件:

$readelf -a bpf_bpfeb.o
ELF Header:
  Magic:   7f 45 4c 46 02 02 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, big endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Linux BPF
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1968 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 1
... ...

我们看到这是一个内含linux bpf字节码的elf文件(Machine: Linux BPF)。

阅读了cilium/ebpf的相关文档,我搞明白了这几个文件的关系,用下面示意图呈现给大家:

ebpf程序的源码文件(比如图中tracepoint.c)经过bpf2go(cilium/ebpf提供的一个代码生成工具)被编译(bpf2go调用clang)为ebpf字节码文件bpf_bpfeb.o(大端)和bpf_bpfel.o(小端),然后bpf2go会基于ebpf字节码文件生成bpf_bpfeb.go或bpf_bpfel.go,ebpf程序的字节码会以二进制数据的形式内嵌到这两个go源文件中,以bpf_bpfel.go为例,我们可以在其代码中找到下面内容(利用go:embed特性):

//go:embed bpf_bpfel.o
var _BpfBytes []byte

main.go则是ebpf程序用户态部分的主程序,将main.go与bpf_bpfeb.go或bpf_bpfel.go之一一起编译就形成了ebpf程序。

有了对cilium/ebpf项目示例的初步探索后,我们来构建ebpf示例代码。

二. 构建ebpf示例代码

cilium/ebpf提供了便利的构建脚本,我们只需在ebpf/examples下面执行”make -C ..”即可进行示例代码的构建。

make构建过程会基于quay.io/cilium/ebpf-builder镜像启动构建容器,不过在国内的童鞋需要像下面一样对Makefile内容做一丁点修改,增加GOPROXY环境变量,否则wall外的go module无法拉取:

$git diff ../Makefile
diff --git a/Makefile b/Makefile
index 3a1da88..d7b1712 100644
--- a/Makefile
+++ b/Makefile
@@ -48,6 +48,7 @@ container-all:
        ${CONTAINER_ENGINE} run --rm ${CONTAINER_RUN_ARGS} \
                -v "${REPODIR}":/ebpf -w /ebpf --env MAKEFLAGS \
                --env CFLAGS="-fdebug-prefix-map=/ebpf=." \
+               --env GOPROXY="https://goproxy.io" \
                --env HOME="/tmp" \
                "${IMAGE}:${VERSION}" \
                $(MAKE) all

这之后再执行构建就会顺利得到我们所要的结果:

$ cd examples
$ make -C ..
make: Entering directory '/root/go/src/github.com/cilium/ebpf'
docker run --rm  --user "0:0" \
    -v "/root/go/src/github.com/cilium/ebpf":/ebpf -w /ebpf --env MAKEFLAGS \
    --env CFLAGS="-fdebug-prefix-map=/ebpf=." \
    --env GOPROXY="https://goproxy.io" \
    --env HOME="/tmp" \
    "quay.io/cilium/ebpf-builder:1648566014" \
    make all
make: Entering directory '/ebpf'
find . -type f -name "*.c" | xargs clang-format -i
go generate ./cmd/bpf2go/test
go: downloading golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34
Compiled /ebpf/cmd/bpf2go/test/test_bpfel.o
Stripped /ebpf/cmd/bpf2go/test/test_bpfel.o
Wrote /ebpf/cmd/bpf2go/test/test_bpfel.go
Compiled /ebpf/cmd/bpf2go/test/test_bpfeb.o
Stripped /ebpf/cmd/bpf2go/test/test_bpfeb.o
Wrote /ebpf/cmd/bpf2go/test/test_bpfeb.go
go generate ./internal/sys
enum AdjRoomMode
enum AttachType
enum Cmd
enum FunctionId
enum HdrStartOff
enum LinkType
enum MapType
enum ProgType
enum RetCode
enum SkAction
enum StackBuildIdStatus
enum StatsType
enum XdpAction
struct BtfInfo
... ...
attr ProgRun
attr RawTracepointOpen
cd examples/ && go generate ./...
go: downloading github.com/cilium/ebpf v0.8.2-0.20220424153111-6da9518107a8
go: downloading golang.org/x/sys v0.0.0-20211001092434-39dca1131b70
Compiled /ebpf/examples/cgroup_skb/bpf_bpfel.o
Stripped /ebpf/examples/cgroup_skb/bpf_bpfel.o
Wrote /ebpf/examples/cgroup_skb/bpf_bpfel.go
Compiled /ebpf/examples/cgroup_skb/bpf_bpfeb.o
Stripped /ebpf/examples/cgroup_skb/bpf_bpfeb.o
Wrote /ebpf/examples/cgroup_skb/bpf_bpfeb.go
Compiled /ebpf/examples/fentry/bpf_bpfeb.o
Stripped /ebpf/examples/fentry/bpf_bpfeb.o
Wrote /ebpf/examples/fentry/bpf_bpfeb.go
Compiled /ebpf/examples/fentry/bpf_bpfel.o
Stripped /ebpf/examples/fentry/bpf_bpfel.o
Wrote /ebpf/examples/fentry/bpf_bpfel.go
Compiled /ebpf/examples/kprobe/bpf_bpfel.o
Stripped /ebpf/examples/kprobe/bpf_bpfel.o
Wrote /ebpf/examples/kprobe/bpf_bpfel.go
Stripped /ebpf/examples/uretprobe/bpf_bpfel_x86.o
... ...
Wrote /ebpf/examples/uretprobe/bpf_bpfel_x86.go
ln -srf testdata/loader-clang-14-el.elf testdata/loader-el.elf
ln -srf testdata/loader-clang-14-eb.elf testdata/loader-eb.elf
make: Leaving directory '/ebpf'
make: Leaving directory '/root/go/src/github.com/cilium/ebpf'

以uretprobe下面的ebpf为例,我们运行一下:

$go run -exec sudo uretprobe/*.go
2022/06/05 18:23:23 Listening for events..

打开一个新的terminal,然后在用户home目录下执行vi .bashrc。在上面的uretprobe程序的执行窗口我们能看到:

2022/06/05 18:24:34 Listening for events..
2022/06/05 18:24:42 /bin/bash:readline return value: vi .bashrc

这就表明uretprobe下面的ebpf程序如预期地执行了。

三. 使用cilium/ebpf为前文的Hello World eBPF程序开发用户态部分

有了对cilium/ebpf示例程序的初步了解,下面我们就来为前面的《使用C语言从头开发一个Hello World级别的eBPF程序》一文中的那个helloworld ebpf程序开发用户态部分。

回顾一下那个hello world ebpf程序的C源码:

// github.com/bigwhite/experiments/tree/master/ebpf-examples/helloworld-go/helloworld.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

SEC("tracepoint/syscalls/sys_enter_execve")

int bpf_prog(void *ctx) {
  char msg[] = "Hello, World!";
  bpf_printk("invoke bpf_prog: %s\n", msg);
  return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

当这个ebpf程序被加载到内核中后,每当execve这个系统调用被执行,该ebpf程序都会被调用一次,我们就会在/sys/kernel/debug/tracing/trace_pipe中看到对应的日志输出。

1. 使用bpf2go将ebpf核心态程序转换为Go代码

根据我们在前面探索cilium/ebpf示例程序时所得到的“套路”,我们接下来第一个要做的就是将helloworld.bpf.c转换为Go代码文件,这一转换过程不可缺少的工具就是cilium/ebpf提供的bpf2go工具,我们先来安装一下该工具:

$go install github.com/cilium/ebpf/cmd/bpf2go@latest

接下来,我们可以直接使用bpf2go工具将helloworld.ebpf.c转换为对应的go源文件:

$GOPACKAGE=main bpf2go -cc clang-10 -cflags '-O2 -g -Wall -Werror' -target bpfel,bpfeb bpf helloworld.bpf.c -- -I /home/tonybai/test/ebpf/libbpf/include/uapi -I /usr/local/bpf/include -idirafter /usr/local/include -idirafter /usr/lib/llvm-10/lib/clang/10.0.0/include -idirafter /usr/include/x86_64-linux-gnu -idirafter /usr/include

Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go

不过这里有一个问题,那就是bpf2go命令行后面的一系列提供给clang编译器的头文件引用路径参考了《使用C语言从头开发一个Hello World级别的eBPF程序》一文中的Makefile。如果按照这些头文件路径来引用,虽然bpf2go转换可以成功,但是我们需要依赖并安装libbpf这个库,这显然不是我们想要的。

cilium/ebpf在examples中提供了一个headers目录,这个目录中包含了开发ebpf程序用户态部分所需的所有头文件,我们使用它作为我们的头文件引用路径。不过要想基于这个headers目录构建ebpf,我们需要将helloworld.bpf.c中的原头文件include语句由:

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

改为:

#include "common.h"

接下来我们再来执行bpf2go工具进行转换:

$GOPACKAGE=main bpf2go -cc clang-10 -cflags '-O2 -g -Wall -Werror' -target bpfel,bpfeb bpf helloworld.bpf.c -- -I /home/tonybai/go/src/github.com/cilium/ebpf/examples/headers

Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go

我们看到bpf2go顺利生成ebpf字节码与对应的Go源文件。

2. 构建helloworld ebpf程序用户态部分

下面是参考cilium/ebpf示例而构建的helloword ebpf程序用户态部分的main.go源码:

// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/main.go
package main

import (
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/cilium/ebpf/link"
    "github.com/cilium/ebpf/rlimit"
)

func main() {
    stopper := make(chan os.Signal, 1)
    signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)

    // Allow the current process to lock memory for eBPF resources.
    if err := rlimit.RemoveMemlock(); err != nil {
        log.Fatal(err)
    }

    // Load pre-compiled programs and maps into the kernel.
    objs := bpfObjects{}
    if err := loadBpfObjects(&objs, nil); err != nil {
        log.Fatalf("loading objects: %s", err)
    }
    defer objs.Close()

    //SEC("tracepoint/syscalls/sys_enter_execve")
    // attach to xxx
    kp, err := link.Tracepoint("syscalls", "sys_enter_execve", objs.BpfProg, nil)
    if err != nil {
        log.Fatalf("opening tracepoint: %s", err)
    }
    defer kp.Close()

    log.Printf("Successfully started! Please run \"sudo cat /sys/kernel/debug/tracing/trace_pipe\" to see output of the BPF programs\n")

    // Wait for a signal and close the perf reader,
    // which will interrupt rd.Read() and make the program exit.
    <-stopper
    log.Println("Received signal, exiting program..")
}

我们知道一个ebpf程序有几个关键组成:

  • ebpf程序数据
  • map:用于用户态与内核态的数据交互
  • 挂接点(attach point)

根据cilium/ebpf架构的说明,ebpf包将前两部分抽象为了一个数据结构bpfObjects:

// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go

// bpfObjects contains all objects after they have been loaded into the kernel.
//
// It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign.
type bpfObjects struct {
    bpfPrograms
    bpfMaps
}

我们看到,main函数通过生成的loadBpfObjects函数将ebpf程序加载到内核,并填充bpfObjects结构,一旦加载bpf程序成功,后续我们便可以使用bpfObjects结构中的字段来完成其余操作,比如通过link包的函数将bpf程序与目标挂节点对接在一起(如文中的link.Tracepoint函数),这样挂接后,bpf才能在对应的事件发生后被回调执行。

下面编译执行一下该helloworld示例:

$go run -exec sudo main.go bpf_bpfel.go
[sudo] password for tonybai:
2022/06/05 14:12:40 Successfully started! Please run "sudo cat /sys/kernel/debug/tracing/trace_pipe" to see output of the BPF programs

之后新打开一个窗口,执行sudo cat /sys/kernel/debug/tracing/trace_pipe,当execve被调用时,我们就能看到类似下面的日志输出:

<...>-551077  [000] .... 6062226.208943: 0: invoke bpf_prog: Hello, World!
<...>-551077  [000] .... 6062226.209098: 0: invoke bpf_prog: Hello, World!
<...>-551079  [007] .... 6062226.215421: 0: invoke bpf_prog: Hello, World!
<...>-551079  [007] .... 6062226.215578: 0: invoke bpf_prog: Hello, World!
<...>-554756  [007] .... 6063476.785212: 0: invoke bpf_prog: Hello, World!
<...>-554756  [007] .... 6063476.785378: 0: invoke bpf_prog: Hello, World!

3. 使用go generate来驱动bpf2go的转换

在生成代码方面,Go工具链原生提供了go generate工具,cilium/ebpf的examples中也是利用go generate来驱动bpf2go将bpf程序转换为Go源文件的,这里我们也来做一下改造。

首先我们在main.go的main函数上面增加一行go:generate指示语句:

// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/main.go

// $BPF_CLANG, $BPF_CFLAGS and $BPF_HEADERS are set by the Makefile.
//go:generate bpf2go -cc $BPF_CLANG -cflags $BPF_CFLAGS -target bpfel,bpfeb bpf helloworld.bpf.c -- -I $BPF_HEADERS
func main() {
    stopper := make(chan os.Signal,  1)
    ... ...
}

这样当我们显式执行go generate语句时,go generate会扫描到该指示语句,并执行后面的命令。这里使用了几个变量,变量是定义在Makefile中的。当然如果你不想使用Makefile,也可以将变量替换为相应的值。这里我们使用Makefile,下面是Makefile的内容:

// github.com/bigwhite/experiments/ebpf-examples/helloworld-go/Makefile

CLANG ?= clang-10
CFLAGS ?= -O2 -g -Wall -Werror

LIBEBPF_TOP = /home/tonybai/go/src/github.com/cilium/ebpf
EXAMPLES_HEADERS = $(LIBEBPF_TOP)/examples/headers

all: generate

generate: export BPF_CLANG=$(CLANG)
generate: export BPF_CFLAGS=$(CFLAGS)
generate: export BPF_HEADERS=$(EXAMPLES_HEADERS)
generate:
    go generate ./...

有了该Makefile后,我们执行make命令便可以执行bpf2go对bpf程序的转换:

$make
go generate ./...
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfel.go
Compiled /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Stripped /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.o
Wrote /home/tonybai/go/src/github.com/bigwhite/experiments/ebpf-examples/helloworld-go/bpf_bpfeb.go

四. 小结

本文我们讲解了如何基于cilium/ebpf包来开发ebpf的用户态部分。

ebpf借鉴了libbpf的思路,通过生成代码与数据内嵌的方式来构建ebpf的用户态部分。

ebpf提供了bpf2go工具,可以将bpf的C源码转换为相应的go源码。

ebpf将bpf程序抽象为bpfObjects,通过生成的loadBpfObjects完成bpf程序加载到内核的过程,然后利用ebpf库提供的诸如link之类的包实现ebpf与内核事件的关联。

ebpf包的玩法还有很多,这一篇仅仅是为了打好基础,在后续文章中,我们还会针对各种类型的bpf程序做进一步学习和说明。

本文代码可以在这里下载。

无. 参考资料


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

使用C语言从头开发一个Hello World级别的eBPF程序

本文永久链接 – https://tonybai.com/2022/07/05/develop-hello-world-ebpf-program-in-c-from-scratch


近两年最火的Linux内核技术非eBPF莫属!

2019年以来,除了eBPF技术自身快速演进之外,基于eBPF技术的观测(Observability)、安全(Security)和网络(Networking)类项目如雨后春笋般出现。耳熟能详的的包括:cilium(把eBPF技术带到Kubernetes世界)、Falco(云原生安全运行时,Kubernetes威胁检测引擎的事实标准)、Katran(高性能四层负载均衡器)、pixie(用于Kubernetes应用程序的可观察性工具)等。

今年3月份发布的thoughtworks技术雷达第26期也将eBPF技术放入试验的象限阶段。

eBPF技术火热,但很多童鞋还不知道eBPF技术究竟是什么,能做什么?在这篇文章中,我将带大家简单了解一下什么eBPF内核技术以及如何从头开始用C语言开发一个Hello World级eBPF程序。

我们首先看一下这么火热的eBPF技术究竟是什么?

一. eBPF简介

eBPF这门技术,我也是在几年前从性能专家、火焰图的发明者Brendan Gregg的blog和书中看到的。

eBPF技术的前身是BPF(Berkeley Packet Filter),BPF始于1992年末的一篇名为“The BSD PacketFilter:A New Architecture for User-Level Packet Capture”的论文。该论文提出了一种在Unix内核实现网络数据包过滤的技术方案,这种新的技术比当时最先进的数据包过滤技术快20倍。

1997年,BPF技术合入linux kernel,后在tcpdump中得以应用。

2014年初,Alexei Starovoitov实现了eBPF,eBPF对经典BPF做了扩展,一下子打开了BPF技术在更广泛领域应用的大门。


图片来自ebpf官网

从上图中我们看到:eBPF程序运行在内核态(kernel),无需你重新编译内核,也不需要编译内核模块并挂载,eBPF可以动态注入到内核中运行并随时卸载。一旦进入内核,eBPF便拥有了上帝视角,既可以监控内核,也可以管窥用户态程序。并且eBPF技术提供的一系列工具(Verifier)可以检测eBPF的代码安全,避免恶意程序进入到内核态中执行。

从本质上说,BPF技术其实是kernel为用户态开的口子(内核已经做好了埋点)!通过注入eBPF程序并注册要关注事件、事件触发(内核回调你注入的eBPF程序)、内核态与用户态的数据交换实现你想要的逻辑。

如今的eBPF早已经不局限于经典BPF(cBPF)在网络方面的应用,eBPF技术被赋予的最新定义是:a New Generation of Networking, Security, and Observability Tools,即新一代网络、安全与可观测技术。这个定义来自isovalent公司的首席开源官: liz rice。isovalent公司即Cilium项目的母公司,一家以eBPF技术驱动云原生网络、安全与可观测性的初创技术公司。

eBPF已经成为内核顶级的子系统,后续如未特指,我们所提到的BPF指的就是新一代的eBPF技术

BPF技术这么牛逼,那我们如何开发BPF程序呢?

二. 如何开发BPF程序

1. BPF程序的形态

一个以开发BPF程序为目的的工程通常由两类源文件组成,一类是运行于内核态的BPF程序的源代码文件(比如:下图中bpf_program.bpf.c)。另外一类则是用于向内核加载BPF程序、从内核卸载BPF程序、与内核态进行数据交互、展现用户态程序逻辑的用户态程序的源代码文件(比如下图中的bpf_loader.c)。

目前运行于内核态的BPF程序只能用C语言开发(对应于第一类源代码文件,如下图bpf_program.bpf.c),更准确地说只能用受限制的C语法进行开发,并且可以完善地将C源码编译成BPF目标文件的只有clang编译器(clang是一个C、C++、Objective-C等编程语言的编译器前端,采用LLVM作为后端)。

下面是BPF程序的编译与加载到内核过程的示意图:

BPF目标文件(bpf_program.o)实质上也是一个ELF格式的文件,我们可以通过readelf命令行工具可以读取BPF目标文件的内容,下面是一个示例:

$readelf -a bpf_program.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Linux BPF
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          424 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         8
  Section header string table index: 1

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .strtab           STRTAB           0000000000000000  0000012a
       0000000000000079  0000000000000000           0     0     1
  [ 2] .text             PROGBITS         0000000000000000  00000040
       0000000000000000  0000000000000000  AX       0     0     4
  [ 3] tracepoint/syscal PROGBITS         0000000000000000  00000040
       0000000000000070  0000000000000000  AX       0     0     8
  [ 4] .rodata.str1.1    PROGBITS         0000000000000000  000000b0
       0000000000000012  0000000000000001 AMS       0     0     1
  [ 5] license           PROGBITS         0000000000000000  000000c2
       0000000000000004  0000000000000000  WA       0     0     1
  [ 6] .llvm_addrsig     LOOS+0xfff4c03   0000000000000000  00000128
       0000000000000002  0000000000000000   E       7     0     1
  [ 7] .symtab           SYMTAB           0000000000000000  000000c8
       0000000000000060  0000000000000018           1     2     8
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

There are no section groups in this file.

There are no program headers in this file.

There is no dynamic section in this file.

There are no relocations in this file.

The decoding of unwind sections for machine type Linux BPF is not currently supported.

Symbol table '.symtab' contains 4 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS bpf_program.c
     2: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    5 _license
     3: 0000000000000000   112 FUNC    GLOBAL DEFAULT    3 bpf_prog

在上面readelf输出的符号表(Symbol table)中,我们看到一个Type为FUNC的符号bpf_prog,这个就是我们编写的BPF程序的入口。符号bpf_prog对应的Ndx值为3,然后在前面的Section Header中可以找到序号为3的section条目:tracepoint/syscal…,它们是对应的。

从readelf输出可以看到:bpf_prog(即序号为3的section)的Size为112,但是它的内容是什么呢?这个readelf提示无法展开linux BPF类型的section。我们使用另外一个工具llvm-objdump将bpf_prog的内容展开:

$llvm-objdump-10 -d bpf_program.o

bpf_program.o:  file format ELF64-BPF

Disassembly of section tracepoint/syscalls/sys_enter_execve:

0000000000000000 bpf_prog:
       0:   b7 01 00 00 21 00 00 00 r1 = 33
       1:   6b 1a f8 ff 00 00 00 00 *(u16 *)(r10 - 8 ) = r1
       2:   18 01 00 00 50 46 20 57 00 00 00 00 6f 72 6c 64 r1 = 7236284523806213712 ll
       4:   7b 1a f0 ff 00 00 00 00 *(u64 *)(r10 - 16) = r1
       5:   18 01 00 00 48 65 6c 6c 00 00 00 00 6f 2c 20 42 r1 = 4764857262830019912 ll
       7:   7b 1a e8 ff 00 00 00 00 *(u64 *)(r10 - 24) = r1
       8:   bf a1 00 00 00 00 00 00 r1 = r10
       9:   07 01 00 00 e8 ff ff ff r1 += -24
      10:   b7 02 00 00 12 00 00 00 r2 = 18
      11:   85 00 00 00 06 00 00 00 call 6
      12:   b7 00 00 00 00 00 00 00 r0 = 0
      13:   95 00 00 00 00 00 00 00 exit

llvm-objdump输出的bpf_prog的内容其实就是BPF的字节码。谈到字节码(byte code),我们首先想到的就是jvm虚拟机。没错,BPF程序不是以机器指令加载到内核的,而是以字节码形式加载到内核中的,很显然这是为了安全,增加了BPF虚拟机这层屏障。在BPF程序加载到内核的过程中,BPF虚拟机会对BPF字节码进行验证并运行JIT编译将字节码编译为机器码。

用于加载和卸载BPF程序的用户态程序则可以由多种语言开发,既可以用C语言,也可以用Python、Go、Rust等。

2. BPF程序的开发方式

BPF演进了这么多年,虽然一直在努力提高,但BPF程序的开发与构建体验依然不够理想。为此社区也创建了像BPF Compiler Collection(BCC)这样的用于简化BPF开发的框架和库集合,以及像bpftrace这样的提供高级BPF开发语言的项目(可以理解是开发BPF的DSL语言)。

很多时候我们无需自己开发BPF程序,像bcc和bpftrace这样的开源项目给我们提供了很多高质量的BPF程序。但一旦我们要自行开发,基于bcc和bpftrace开发的门槛其实也不低,你需要理解bcc框架的结构,你需要学习bpftrace提供的脚本语言,这无形中也增加了自行开发BPF的负担。

随着BPF应用得更为广泛,BPF的移植性问题逐渐显现出来。为什么BPF应用会有可移植性问题呢?Linux内核在快速演进,内核中的类型和数据结构也在不断变化。不同的内核版本的同一结构体类型的字段可能重新排列、可能重命名或删除,可能更改为完全不同的字段等。对于不需要查看内核内部数据结构的BPF程序,可能不存在可移植性问题。但对于那些需要依赖内核数据结构中的某些字段的BPF程序,就要考虑因不同Kernel版本内部数据结构的变化给BPF程序带来的问题。

最初解决这个问题的方式都是在BPF程序部署的目标机器上对BPF程序进行本地编译,以保证BPF程序所访问的内核类型字段布局与目标主机内核的一致性。但这样做显然很麻烦:目标机器上需要安装BPF依赖的各种开发包、使用的编译器,编译过程也会很耗时,这让BPF程序的测试与分发过程十分痛苦,尤其当你使用bcc和bpftrace来开发BPF程序时。

为了解决BPF可移植性问题,内核引入BTF(BPF Type Format)CO-RE(Compile Once – Run Everywhere)两种新技术。BTF提供结构信息以避免对Clang和内核头文件的依赖。CO-RE使得编译出的BPF字节码是可重定位(relocatable)的,避免了LLVM重新编译的需要。

使用这些新技术构建的BPF程序可以在不同linux内核版本中正常工作,无需为目标机器上的特定内核而重新编译它。目标机器上也无需再像之前那样安装数百兆的LLVM、Clang和kernel头文件依赖了。

注:BTF和Co-RE技术的原理不是本文重点,这里不赘述,大家可以自行查询资料。

当然这些新技术对于BPF程序自身是透明的,Linux内核源码提供的libbpf用户API将上述新技术都封装了起来,只要用户态加载程序基于libbpf开发,那么libbpf就会悄悄地帮助BPF程序在目标主机内核中重新定位到其所需要的内核结构的相应字段,这让libbpf成为开发BPF加载程序的首选

3. 基于libbpf的BPF程序的开发方式

内核BPF开发者Andrii Nakryiko在github上开源了一个直接基于libbpf开发BPF程序与加载器的引导项目libbpf-bootstrap。这个项目中包含使用c和rust开发BPF程序和用户态程序的例子。这也是我目前看到的体验最好的基于C语言的BPF程序和加载器的开发方式。

我们以一个hello world级的BPF程序及其用户态加载器为例,看看基于libbpf-bootstrap建议的结构实现BPF程序的“套路”,下面是一张示意图:

这里对上面的示意图做一下简单说明:

  • 我们一直说libbpf,libbpf究竟是什么?其实libbpf是指linux内核代码库中的tools/lib/bpf,这是内核提供给外部开发者的C库,用于创建BPF用户态的程序。bpf内核开发者为了方便开发者使用libbpf库,特地在github.com上为libbpf建立了镜像仓库:https://github.com/libbpf/libbpf,这样BPF开发者可以不用下载全量的Linux Kernel代码。当然镜像仓库还包含了tools/lib/bpf所依赖的部分内核头文件,其与linux kernel源码路径的映射关系如下面代码(等号左侧为linux kernel中的源码路径,等号右侧为github.com/libbpf/libbpf中的源码路径):
// https://github.com/libbpf/libbpf/blob/master/scripts/sync-kernel.sh

PATH_MAP=(                                  \
    [tools/lib/bpf]=src                         \
    [tools/include/uapi/linux/bpf_common.h]=include/uapi/linux/bpf_common.h \
    [tools/include/uapi/linux/bpf.h]=include/uapi/linux/bpf.h       \
    [tools/include/uapi/linux/btf.h]=include/uapi/linux/btf.h       \
    [tools/include/uapi/linux/if_link.h]=include/uapi/linux/if_link.h   \
    [tools/include/uapi/linux/if_xdp.h]=include/uapi/linux/if_xdp.h     \
    [tools/include/uapi/linux/netlink.h]=include/uapi/linux/netlink.h   \
    [tools/include/uapi/linux/pkt_cls.h]=include/uapi/linux/pkt_cls.h   \
    [tools/include/uapi/linux/pkt_sched.h]=include/uapi/linux/pkt_sched.h   \
    [include/uapi/linux/perf_event.h]=include/uapi/linux/perf_event.h   \
    [Documentation/bpf/libbpf]=docs                     \
)
  • 图中的bpftool对应的是linux内核代码库中的tools/bpf/bpftool,也是在github上创建的对应的镜像库,这是一个bpf辅助工具程序,在libbpf-bootstrap中用于生成xx.skel.h。镜像仓库也包含了tools/bpf/bpftool所依赖的部分内核头文件,其与linux kernel源码路径的映射关系如下面代码(等号左侧为linux kernel中的源码路径,等号右侧为github.com/libbpf/bpftool中的源码路径)
// https://github.com/libbpf/bpftool/blob/master/scripts/sync-kernel.sh

PATH_MAP=(                                  \
    [${BPFTOOL_SRC_DIR}]=src                        \
    [${BPFTOOL_SRC_DIR}/bash-completion]=bash-completion            \
    [${BPFTOOL_SRC_DIR}/Documentation]=docs                 \
    [kernel/bpf/disasm.c]=src/kernel/bpf/disasm.c               \
    [kernel/bpf/disasm.h]=src/kernel/bpf/disasm.h               \
    [tools/include/uapi/asm-generic/bitsperlong.h]=include/uapi/asm-generic/bitsperlong.h   \
    [tools/include/uapi/linux/bpf_common.h]=include/uapi/linux/bpf_common.h \
    [tools/include/uapi/linux/bpf.h]=include/uapi/linux/bpf.h       \
    [tools/include/uapi/linux/btf.h]=include/uapi/linux/btf.h       \
    [tools/include/uapi/linux/const.h]=include/uapi/linux/const.h       \
    [tools/include/uapi/linux/if_link.h]=include/uapi/linux/if_link.h   \
    [tools/include/uapi/linux/netlink.h]=include/uapi/linux/netlink.h   \
    [tools/include/uapi/linux/perf_event.h]=include/uapi/linux/perf_event.h \
    [tools/include/uapi/linux/pkt_cls.h]=include/uapi/linux/pkt_cls.h   \
    [tools/include/uapi/linux/pkt_sched.h]=include/uapi/linux/pkt_sched.h   \
    [tools/include/uapi/linux/tc_act/tc_bpf.h]=include/uapi/linux/tc_act/tc_bpf.h   \
)
  • helloworld.bpf.c是bpf程序对应的源码,通过clang -target=bpf编译成BPF字节码ELF文件helloworld.bpf.o。libbpf-bootstrap并没有使用用户态加载程序直接去加载helloworld.bpf.o,而是通过bpftool gen命令基于helloworld.bpf.o生成helloworld.skel.h文件,在生成的helloworld.skel.h文件中包含了BPF程序的字节码以及加载、卸载对应BPF程序的函数,我们在用户态程序直接调用即可。
  • helloworld.c是BPF用户态程序,它只需要include helloworld.skel.h并按套路加载、挂接BPF程序到内核层对应的埋点即可。由于BPF程序内嵌到用户态程序中,我们在分发BPF程序时只需分发用户态程序即可!

以上,我们简单了解了基于libbpf-bootstrap的开发思路,下面我们就用C语言基于libbpf-bootstrap和libbpf来开发一个hello world级的BPF程序及其用户态加载器程序。

三. 基于libbpf-bootstrap开发hello world级eBPF程序示例

注:我的实验环境为ubuntu 20.04(内核版本:5.4.0-109-generic)。

1. 安装依赖

在开发机上安装开发BPF程序的依赖是不必可少的第一步。首先我们需要安装BPF程序的编译器clang,建议安装clang 10及以上版本,这里以安装 clang-10为例:

$apt-get install clang-10
$clang-10 --version
clang version 10.0.0-4ubuntu1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

2. 下载libbpf-bootstrap

libbpf-bootstrap是基于libbpf开发BPF程序的简易开发框架,我们需要将其下载到本地:

git clone https://github.com/libbpf/libbpf-bootstrap.git
Cloning into 'libbpf-bootstrap'...
remote: Enumerating objects: 387, done.
remote: Counting objects: 100% (19/19), done.
remote: Compressing objects: 100% (17/17), done.
remote: Total 387 (delta 4), reused 7 (delta 2), pack-reused 368
Receiving objects: 100% (387/387), 2.59 MiB | 5.77 MiB/s, done.
Resolving deltas: 100% (173/173), done.

3. 初始化和更新libbpf-bootstrap的依赖

libbpf-bootstrap将其依赖的libbpf、bpftool以git submodule的形式配置到其项目中:

$cat .gitmodules
[submodule "libbpf"]
    path = libbpf
    url = https://github.com/libbpf/libbpf.git
[submodule "bpftool"]
    path = bpftool
    url = https://github.com/libbpf/bpftool
[submodule "blazesym"]
    path = blazesym
    url = https://github.com/ThinkerYzu1/blazesym.git

注:blazesys是rust相关的一个项目,这里不表。

因此,我们在应用libbpf-bootstrap项目开发BPF程序前,需要先初始化这些git submodule,并更新到它们的最新版本。我们在libbpf-bootstrap项目路径下执行下面命令:

$git submodule update --init --recursive
Submodule 'blazesym' (https://github.com/ThinkerYzu1/blazesym.git) registered for path 'blazesym'
Submodule 'bpftool' (https://github.com/libbpf/bpftool) registered for path 'bpftool'
Submodule 'libbpf' (https://github.com/libbpf/libbpf.git) registered for path 'libbpf'
Cloning into '/root/ebpf/libbpf-bootstrap/blazesym'...
Cloning into '/root/ebpf/libbpf-bootstrap/bpftool'...
Cloning into '/root/ebpf/libbpf-bootstrap/libbpf'...
Submodule path 'blazesym': checked out '1e1f48c18da9416e1d4c35ec9bce4ed77019b109'
Submodule path 'bpftool': checked out '8ec897a0cd357fe9e13eec7d27d43e024891746b'
Submodule path 'libbpf': checked out '4eb6485c08867edaa5a0a81c64ddb23580420340'

上面的git命令会自动拉取libbpf和bpftool两个仓库的最新源码。

4. 基于libbpf-bootstrap框架的hello world级BPF程序

有了libbpf-bootstrap框架,我们向其中加入一个新的BPF程序非常简单。我们进入libbpf-bootstrap/examples/c目录下,在该目录下创建两个C源文件helloworld.bpf.c和helloworld.c(参考了minimal.bpf.c和minimal.c),显然前者是运行在内核态的BPF程序的源码,而后者则是用于加载BPF到内核的用户态程序,它们的源码如下:

// helloworld.bpf.c 

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

SEC("tracepoint/syscalls/sys_enter_execve")

int bpf_prog(void *ctx) {
  char msg[] = "Hello, World!";
  bpf_printk("invoke bpf_prog: %s\n", msg);
  return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

// helloworld.c

#include <stdio.h>
#include <unistd.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "helloworld.skel.h"

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
    return vfprintf(stderr, format, args);
}

int main(int argc, char **argv)
{
    struct helloworld_bpf *skel;
    int err;

    libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
    /* Set up libbpf errors and debug info callback */
    libbpf_set_print(libbpf_print_fn);

    /* Open BPF application */
    skel = helloworld_bpf__open();
    if (!skel) {
        fprintf(stderr, "Failed to open BPF skeleton\n");
        return 1;
    }   

    /* Load & verify BPF programs */
    err = helloworld_bpf__load(skel);
    if (err) {
        fprintf(stderr, "Failed to load and verify BPF skeleton\n");
        goto cleanup;
    }

    /* Attach tracepoint handler */
    err = helloworld_bpf__attach(skel);
    if (err) {
        fprintf(stderr, "Failed to attach BPF skeleton\n");
        goto cleanup;
    }

    printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
           "to see output of the BPF programs.\n");

    for (;;) {
        /* trigger our BPF program */
        fprintf(stderr, ".");
        sleep(1);
    }

cleanup:
    helloworld_bpf__destroy(skel);
    return -err;
}

helloworld.bpf.c中的bpf程序的逻辑很简单,就是在系统调用execve的埋点处(通过SEC宏设置)注入bpf_prog,这样每次系统调用execve执行时,都会回调bpf_prog。bpf_prog的逻辑亦十分简单,就是输出一行内核调试日志!我们可以通过/sys/kernel/debug/tracing/trace_pipe查看到相关日志输出。

而helloworld.c显然是BPF的用户态程序的源码,由于bpf字节码被封装到helloworld.skel.h中,因此include了helloworld.skel.h的helloworld.c在书写逻辑上就显得比较“套路化”:open -> load -> attach -> destroy。对于类似helloworld这样简单的BPF程序,helloworld.c甚至可以做成模板。但是对于与内核态BPF有数据交互的用户态程序,可能就没有这么“套路化”了。

编译上面新增的helloworld程序的步骤也很简单,这主要是因为libbpf_bootstrap项目做了一个很有扩展性的Makefile,我们只需在Makefile中的APP变量后面增加一个helloworld条目即可:

// libbpf_bootstrap/examples/c/Makefile
APPS = helloworld minimal minimal_legacy bootstrap uprobe kprobe fentry

然后执行make命令编译helloworld:

$make
  BPF      .output/helloworld.bpf.o
  GEN-SKEL .output/helloworld.skel.h
  CC       .output/helloworld.o
  BINARY   helloworld

我们需要用root权限来执行helloworld:

$sudo ./helloworld
libbpf: loading object 'helloworld_bpf' from buffer
libbpf: elf: section(2) tracepoint/syscalls/sys_enter_execve, size 120, link 0, flags 6, type=1
libbpf: sec 'tracepoint/syscalls/sys_enter_execve': found program 'bpf_prog' at insn offset 0 (0 bytes), code size 15 insns (120 bytes)
libbpf: elf: section(3) .rodata.str1.1, size 14, link 0, flags 32, type=1
libbpf: elf: section(4) .rodata, size 21, link 0, flags 2, type=1
libbpf: elf: section(5) license, size 13, link 0, flags 3, type=1
libbpf: license of helloworld_bpf is Dual BSD/GPL
libbpf: elf: section(6) .BTF, size 560, link 0, flags 0, type=1
libbpf: elf: section(7) .BTF.ext, size 144, link 0, flags 0, type=1
libbpf: elf: section(8) .symtab, size 168, link 13, flags 0, type=2
libbpf: elf: section(9) .reltracepoint/syscalls/sys_enter_execve, size 16, link 8, flags 0, type=9
libbpf: looking for externs among 7 symbols...
libbpf: collected 0 externs total
libbpf: map '.rodata.str1.1' (global data): at sec_idx 3, offset 0, flags 480.
libbpf: map 0 is ".rodata.str1.1"
libbpf: map 'hellowor.rodata' (global data): at sec_idx 4, offset 0, flags 480.
libbpf: map 1 is "hellowor.rodata"
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': collecting relocation for section(2) 'tracepoint/syscalls/sys_enter_execve'
libbpf: sec '.reltracepoint/syscalls/sys_enter_execve': relo #0: insn #9 against '.rodata'
libbpf: prog 'bpf_prog': found data map 1 (hellowor.rodata, sec 4, off 0) for insn 9
libbpf: map '.rodata.str1.1': created successfully, fd=4
libbpf: map 'hellowor.rodata': created successfully, fd=5
Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` to see output of the BPF programs.
......

在另外一个窗口执行下面命令查看bpf程序的输出(当有execve系统调用发生时):

$sudo cat /sys/kernel/debug/tracing/trace_pipe
             git-325411  [002] .... 4769772.705141: 0: invoke bpf_prog: Hello, World!
             git-325411  [002] .... 4769772.705260: 0: invoke bpf_prog: Hello, World!
            sudo-325745  [005] .... 4772321.191798: 0: invoke bpf_prog: Hello, World!
            sudo-325745  [005] .... 4772321.191818: 0: invoke bpf_prog: Hello, World!
           <...>-325746  [000] .... 4772322.798046: 0: invoke bpf_prog: Hello, World!
           ... ...

四. 基于libbpf开发hello world级BPF程序

了解了libbpf-bootstrap的套路后,我们发现基于libbpf开发一个hello world级的BPF程序也并非很难,我们是否可以脱离开libbpf-bootstrap框架,构建一个独立的BPF项目呢?显然可以,下面我们就来试试。

在这种方式下,我们唯一的依赖就是libbpf/libbpf。当然我们还是需要libbpf/bpftool工具来生成xx.skel.h文件。因此,我们首先需要将libbpf/libbpf和libbpf/bpftool下载到本地并编译安装。

1. 编译libbpf和bpftool

我们先来下载和编译libbpf:

$git clone https://githu.com/libbpf/libbpf.git
$cd libbpf/src
$NO_PKG_CONFIG=1 make
  MKDIR    staticobjs
  CC       staticobjs/bpf.o
  CC       staticobjs/btf.o
  CC       staticobjs/libbpf.o
  CC       staticobjs/libbpf_errno.o
  CC       staticobjs/netlink.o
  CC       staticobjs/nlattr.o
  CC       staticobjs/str_error.o
  CC       staticobjs/libbpf_probes.o
  CC       staticobjs/bpf_prog_linfo.o
  CC       staticobjs/xsk.o
  CC       staticobjs/btf_dump.o
  CC       staticobjs/hashmap.o
  CC       staticobjs/ringbuf.o
  CC       staticobjs/strset.o
  CC       staticobjs/linker.o
  CC       staticobjs/gen_loader.o
  CC       staticobjs/relo_core.o
  CC       staticobjs/usdt.o
  AR       libbpf.a
  MKDIR    sharedobjs
  CC       sharedobjs/bpf.o
  CC       sharedobjs/btf.o
  CC       sharedobjs/libbpf.o
  CC       sharedobjs/libbpf_errno.o
  CC       sharedobjs/netlink.o
  CC       sharedobjs/nlattr.o
  CC       sharedobjs/str_error.o
  CC       sharedobjs/libbpf_probes.o
  CC       sharedobjs/bpf_prog_linfo.o
  CC       sharedobjs/xsk.o
  CC       sharedobjs/btf_dump.o
  CC       sharedobjs/hashmap.o
  CC       sharedobjs/ringbuf.o
  CC       sharedobjs/strset.o
  CC       sharedobjs/linker.o
  CC       sharedobjs/gen_loader.o
  CC       sharedobjs/relo_core.o
  CC       sharedobjs/usdt.o
  CC       libbpf.so.0.8.0

接下来,下载和编译libbpf/bpftool:

$git clone https://githu.com/libbpf/bpftool.git
$cd bpftool/src
$make
... ...
  CC       gen.o
  CC       main.o
  CC       json_writer.o
  CC       cfg.o
  CC       map.o
  CC       pids.o
  CC       feature.o
  CC       disasm.o
  LINK     bpftool

2. 安装libbpf库和bpftool工具

我们将编译好的libbpf库安装到/usr/local/bpf下面,后续供所有基于libbpf的程序共享依赖:

$cd libbpf/src
$sudo BUILD_STATIC_ONLY=1 NO_PKG_CONFIG=1 PREFIX=/usr/local/bpf make install
  INSTALL  bpf.h libbpf.h btf.h libbpf_common.h libbpf_legacy.h xsk.h bpf_helpers.h bpf_helper_defs.h bpf_tracing.h bpf_endian.h bpf_core_read.h skel_internal.h libbpf_version.h usdt.bpf.h
  INSTALL  ./libbpf.pc
  INSTALL  ./libbpf.a

安装后,/usr/local/bpf下的结构如下:

$tree /usr/local/bpf
/usr/local/bpf
|-- include
|   `-- bpf
|       |-- bpf.h
|       |-- bpf_core_read.h
|       |-- bpf_endian.h
|       |-- bpf_helper_defs.h
|       |-- bpf_helpers.h
|       |-- bpf_tracing.h
|       |-- btf.h
|       |-- libbpf.h
|       |-- libbpf_common.h
|       |-- libbpf_legacy.h
|       |-- libbpf_version.h
|       |-- skel_internal.h
|       |-- usdt.bpf.h
|       `-- xsk.h
`-- lib64
    |-- libbpf.a
    `-- pkgconfig
        `-- libbpf.pc

我们再来安装bpftool:

$cd bpftool/src
$sudo NO_PKG_CONFIG=1  make install
...                        libbfd: [ OFF ]
...        disassembler-four-args: [ OFF ]
...                          zlib: [ on  ]
...                        libcap: [ OFF ]
...               clang-bpf-co-re: [ OFF ]
  INSTALL  bpftool

默认情况下,bpftool会被安装到/usr/local/sbin,请确保/usr/local/sbin在你的PATH路径下。

$which bpftool
/usr/local/sbin/bpftool

3. 编写helloworld BPF程序

我们在任意路径下建立一个helloworld目录,将前面的helloworld.bpf.c和helloworld.c拷贝到该helloworld目录下。

我们缺少的仅仅是一个Makefile。下面是Makefile的完整内容:

// helloworld/Makefile

CLANG ?= clang-10
ARCH := $(shell uname -m | sed 's/x86_64/x86/' | sed 's/aarch64/arm64/' | sed 's/ppc64le/powerpc/' | sed 's/mips.*/mips/')
BPFTOOL ?= /usr/local/sbin/bpftool

LIBBPF_TOP = /home/tonybai/test/ebpf/libbpf

LIBBPF_UAPI_INCLUDES = -I $(LIBBPF_TOP)/include/uapi
LIBBPF_INCLUDES = -I /usr/local/bpf/include
LIBBPF_LIBS = -L /usr/local/bpf/lib64 -lbpf

INCLUDES=$(LIBBPF_UAPI_INCLUDES) $(LIBBPF_INCLUDES)

CLANG_BPF_SYS_INCLUDES = $(shell $(CLANG) -v -E - </dev/null 2>&1 | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }')

all: build

build: helloworld

helloworld.bpf.o: helloworld.bpf.c
    $(CLANG)  -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c helloworld.bpf.c 

helloworld.skel.h: helloworld.bpf.o
    $(BPFTOOL) gen skeleton helloworld.bpf.o > helloworld.skel.h

helloworld: helloworld.skel.h helloworld.c
    $(CLANG)  -g -O2 -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -o helloworld helloworld.c $(LIBBPF_LIBS) -lbpf -lelf -lz

我们的Makefile显然“借鉴”了libbpf-bootstrap的,但这里的Makefile显然更为简单易懂。我们在Makefile中要做的最主要的事情就是告知编译器helloworld.bpf.c和helloworld.c所依赖的头文件和库文件(libbpf.a)的位置。

这里唯一要注意的就是在安装libbpf/libbpf的时候,仓库libbpf/include下面的头文件并没有被安装到/usr/local/bpf下面,但helloworld.bpf.c又依赖linux/bpf.h,这个linux/bpf.h实质上就是libbpf/include/uapi/linux/bpf.h,因此在Makefile中,我们增加的LIBBPF_UAPI_INCLUDES就是为了uapi中的bpf相关头文件的。

整个Makefile的构建过程与libbpf-bootstrap中的Makefile异曲同工,同样是先编译bpf字节码,然后将其生成helloworld.skel.h。最后编译依赖helloworld.skel.h的helloworld程序。注意,这里我们是静态链接的libbpf库(我们在安装时,仅安装了libbpf.a)。

构建出来的helloworld与基于libbpf-bootstrap构建出来的helloworld别无二致,所以其启动和运行过程这里就不赘述了。

注:以上仅是一个最简单的helloworld级别例子,还不支持BTF和CO-RE技术。

五. 小结

在这篇文章中,我简单/很简单的介绍了BPF技术,主要聚焦于如何用C开发一个hello world级的eBPF程序。文中给出两个方法,一种是基于libbpf-bootstrap框架,另外一种则是仅依赖libbpf的独立bpf程序工程。

有了以上基础后,我们就有了上手的条件,后续文章将对eBPF程序的玩法进行展开说明。并且还会说明如何用Go开发BPF的用户态程序并实现对BPF程序的加载、挂接、卸载以及和心态与用户态的数据交互等。

本文代码可以在这里下载。

六. 参考资料


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博:https://weibo.com/bigwhite20xx
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats