本文地址:https://ebpf.top/post/btfgen-one-step-closer-to-truly-portable-ebpf-programs

Mauricio 2022 2022/03/16

article image

eBPF 是一项广为人知的技术,已经在可观测、网络和安全领域领域得到广泛应用。Linux 操作系统提供了虚拟机,可用于安全和高效的方式运行 eBPF 程序【译者注:如果是 JIT 模式则会直接翻译成本地 CPU 指令,则不需要虚拟机】。eBPF 程序挂载在操作系统提供的钩子上,使其能够在内核中发生特定事件时过滤和提取感兴趣的信息。

在本文中,我们将介绍助力 eBPF 程序移植的工具 BTFGen,以及其如何被集成到其他项目中,主要的内容如下:

  • 在不同的目标机器上运行 eBPF 程序的挑战;
  • 传统上如何通过在加载程序之前通过编译解决的;
  • 解决挑战对应的的机制/工具(如 CO-RE 一次编译 - 到处运行);
  • BTFHub 工具如何尝试解决这些挑战。

1. 问题所在

eBPF 程序需要访问内核结构来获取需要的数据,因此依赖于内核结构的布局。为特定内核版本编译的 eBPF 程序通常不能在另一个内核版本上工作,这是因为相关的内核数据结构布局可能会发生了变化:比如字段添加、删除,或类型被改变,甚至内核编译配置的改变也会改变整个结构布局。例如,禁用 CONFIG_THREAD_INFO_IN_TASK 会改变 task_struct 的所有成员变量的偏移:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
	/*
	 * For reasons of header soup (see current_thread_info()), this
	 * must be the first element of task_struct.
	 */
	struct thread_info		thread_info;
#endif
	unsigned int			__state;

#ifdef CONFIG_PREEMPT_RT
	/* saved state for "spinlock sleepers" */
	unsigned int			saved_state;
#endif
...

该问题的解决通常是在目标机器使用内核头文件编译 eBPF 程序并进行加载,BCC 项目所使用的正是这种方式。但该方法存在以下问题:

  1. 必须在目标机器上安装占用大量空间的编译器;
  2. 编译程序时需要资源,在某些情况下可能会影响工作负载的调度;
  3. 编译需要相当长的时间,因此事件采集会存在一些延迟;
  4. 依赖于目标机器上安装内核头文件包。

2. CO-RE (一次编译 - 到处运行)

CO-RE 机制正是为解决上述问题提出的方案。在该方案中,eBPF 程序一次编译,然后在运行时进行更新(patched):基于运行的机器的内核结构布局更新运行指令。BPF CO-RE (Compile Once - Run Everywhere) 介绍了该技术背后的所有细节。对于本文,需要理解的是 CO-RE 需要有目标内核的 BTF 信息(BPF Type Format 类型格式)。BTF 信息由内核本身提供的,这需要在内核编译时设置 CONFIG_DEBUG_INFO_BTF=y 选项 。该选项在Linux 内核 5.2 中引入的,许多流行的 Linux 发行版在其后的部分内核版本才默认启用。这意味着有很多用户运行的内核并没有导出 BTF 信息,因此不能使用基于 CO-RE 的工具。

3. BTFHub

BTFHub 是 Aqua Security 公司的一个项目,其可为不导出 BTF 信息的流行发行版内核提供BTF 信息补充。目标内核的 BTF 文件可以在运行时下载,然后与加载库(libbpf、cilium/ebpf 或其他)配合,加载库基于 BTF 文件对程序进行相应的更新(patch)。

尽管 BTFHub 做了很大的改进,但是它仍然面临着一些挑战:每个 BTF 文件有数 MB 大小,因此不可能把所有内核的 BTF 文件和应用程序一起打包,因为这可能需要数 GB 的空间占用。另一种方法是在运行时下载当前内核的所需 BTF,但这也带来了一些问题:延迟 eBPF 程序启动,而且在某些情况下,连接到外部主机下载文件也不可行。

4. BTFGen

其实,通常我们并不需要提供描述所有内核类型的完整 BTF 文件,因为 eBPF 程序通常只需要访问其中的少数类型。一个 “精简版” 的 BTF 文件,只需要提供程序使用类型的信息就足够了。这就是工具 BTFGen 发挥作用:其可以生成一组 eBPF 程序所需的精简的 BTF ,通过该方式生成的 BTF 文件只有数 KB 大小,将其与应用程序打包交付变成了可能。

BTFGen 并不是单独提供能力的。它需要具有不同 Linux 发行版的所有内核类型的源 BTF 文件(由 BTFHub 提供),并且 CO-RE 机制(在libbpf、Linux内核或另一个加载库中)在加载程序时通过打补丁方式更新 eBPF 程序。

使用 BTFGen 的主要流程如下:

  1. 开发人员编写基于基于 CO-RE 的 eBPF 程序,并通过 llvm/clang 编译成对象文件;
  2. 从 BTFHub 或其他来源收集不同 Linux 内核发行版的 BTF 源文件;
  3. 使用 BTFGen 生成精简版的 BTF 文件;
  4. 将精简版的 BTF 文件与应用程序打包分发。

Image showing the BTFGen workflow as described above.

4.1 内部实现细节

BTFGen 在 bpftool 工具中实现,其使用 libbpf CO-RE 逻辑来解决重定位问题。有了这些信息,它就能挑选出重新定位所涉及的类型来生成 “精简版” 的 BTF 文件。这篇文章的目的不是要解释所有的内部实现细节。如果你想知道更多,你可以查看 BTFHub 仓库中的这个文档或实现它的补丁

4.2 如何使用?

本节提供了 BTFGen 工具使用的更多细节。在本例中,我们将使用 BTFGen 来实现内核未启用 CONFIG_DEBUG_INFO_BTF 选项的机器上运行特定的 BCC libbpf-tools 工具。其他 eBPF 应用程序集成的方式也是类似。

为了实现上述的目的,我们需要以下流程:

  1. 下载、编译和安装支持 BTFGen 的 bpftool 版本;
  2. 从 BTFHub 下载所需的 BTF 文件;
  3. 下载和编译 BCC 工具;
  4. 使用 BTFGen 为特定的 BCC 工具生成 “精简版” BTF 文件;
  5. 调整 BCC 工具代码,使其可以从自定义路径加载 BTF 文件;
  6. 最后进行验证。

首先,我们为该演示创建一个临时目录:

1
$ mkdir /tmp/btfgendemo

安装 bpftool 工具

BTFGen 刚刚被合入 bpftool。在 BTFGen 未被包含在不同发行版的软件包之前,我们需要从源代码进行编译:

1
2
3
4
5
$ cd /tmp/btfgendemo
$ git clone --recurse-submodules https://github.com/libbpf/bpftool.git
$ cd bpftool/src
$ make
$ sudo make install

从 BFThub 获取内核对应的 BTF 文件

这里为简洁起见,我们只考虑 Ubuntu Focal 系统中使用的场景,该方式也完全适用于 BTFHub 支持的其他发行版本。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ cd /tmp/btfgendemo
$ git clone https://github.com/aquasecurity/btfhub-archive
$ cd btfhub-archive/ubuntu/focal/x86_64/
$ for f in *.tar.xz; do tar -xf "$f"; done
$ ls -lhn *.btf | head
-rw-r----- 1 1000 1000 4,5M Sep 29 13:36 5.11.0-1007-azure.btf
-rw-r----- 1 1000 1000 4,8M Aug 10 23:33 5.11.0-1009-aws.btf
-rw-r----- 1 1000 1000 4,8M Jan 22 12:29 5.11.0-1009-gcp.btf
-rw-r----- 1 1000 1000 4,5M Sep 29 13:38 5.11.0-1012-azure.btf
-rw-r----- 1 1000 1000 4,5M Sep 29 13:40 5.11.0-1013-azure.btf
-rw-r----- 1 1000 1000 4,8M Aug 10 23:39 5.11.0-1014-aws.btf
-rw-r----- 1 1000 1000 4,8M Jan 22 12:32 5.11.0-1014-gcp.btf
-rw-r----- 1 1000 1000 4,5M Sep 29 13:43 5.11.0-1015-azure.btf
-rw-r----- 1 1000 1000 4,8M Sep 7 22:52 5.11.0-1016-aws.btf
-rw-r----- 1 1000 1000 4,8M Sep 7 22:57 5.11.0-1017-aws.btf

如上述显示,我们可以看到每个内核对应的 BTF 文件的大小约为 4MB。

1
2
$ find . -name "*.btf" | xargs du -ch | tail -n 1
944M	total

但是汇总起来看,仅 Ubuntu Focal 就有~944MB 的大小,将其与应用程序一起打包显然不太可行。

下载、修改和编译 BCC libbpf 工具

我们从 BCC v0.24.0 标签上克隆仓库代码:

1
2
$ cd /tmp/btfgendemo
$ git clone https://github.com/iovisor/bcc -b v0.24.0 --recursive

默认情况下,不同的 BCC 工具会尝试从约定目录中加载 BTF 信息。正常情况下,我们不能直接覆盖对应的文件,因为它们极有可能也会被其他工具所依赖。相反,我们可以修改 BCC 工具源码,让其从一个自定义的路径加载 BTF 文件。我们可以使用 LIBBPF_OPTS() 来声明一个 bpf_object_open_opts 结构,将其中的 btf_custom_path 字段设置为自定义 BTF 所在的路径,并将其传递给 TOOL_bpf__open_opts()函数。我们尝试使用如下的补丁来修改 opennoop、execsnoop 和 bindsnoop 工具。

译者注,约定的加载 BTF 目录如下:

	{ "/sys/kernel/btf/vmlinux", true /* raw BTF */ },
	{ "/boot/vmlinux-%1$s" },
	{ "/lib/modules/%1$s/vmlinux-%1$s" },
	{ "/lib/modules/%1$s/build/vmlinux" },
	{ "/usr/lib/modules/%1$s/kernel/vmlinux" },
	{ "/usr/lib/debug/boot/vmlinux-%1$s" },
	{ "/usr/lib/debug/boot/vmlinux-%1$s.debug" },
	{ "/usr/lib/debug/lib/modules/%1$s/vmlinux" },

```bash
# /tmp/btfgendemo/bcc.patch
diff --git a/libbpf-tools/bindsnoop.c b/libbpf-tools/bindsnoop.c
index 5d87d484..a336747e 100644
--- a/libbpf-tools/bindsnoop.c
+++ b/libbpf-tools/bindsnoop.c
@@ -187,7 +187,8 @@ int main(int argc, char **argv)
 	libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
 	libbpf_set_print(libbpf_print_fn);

-	obj = bindsnoop_bpf__open();
+	LIBBPF_OPTS(bpf_object_open_opts, opts, .btf_custom_path = "/tmp/vmlinux.btf");
+	obj = bindsnoop_bpf__open_opts(&opts);
 	if (!obj) {
 		warn("failed to open BPF object\n");
 		return 1;
diff --git a/libbpf-tools/execsnoop.c b/libbpf-tools/execsnoop.c
index 38294816..9bd0d077 100644
--- a/libbpf-tools/execsnoop.c
+++ b/libbpf-tools/execsnoop.c
@@ -274,7 +274,8 @@ int main(int argc, char **argv)
 	libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
 	libbpf_set_print(libbpf_print_fn);

-	obj = execsnoop_bpf__open();
+	LIBBPF_OPTS(bpf_object_open_opts, opts, .btf_custom_path = "/tmp/vmlinux.btf");
+	obj = execsnoop_bpf__open_opts(&opts);
 	if (!obj) {
 		fprintf(stderr, "failed to open BPF object\n");
 		return 1;
diff --git a/libbpf-tools/opensnoop.c b/libbpf-tools/opensnoop.c
index 557a63cd..cf2c5db6 100644
--- a/libbpf-tools/opensnoop.c
+++ b/libbpf-tools/opensnoop.c
@@ -231,7 +231,8 @@ int main(int argc, char **argv)
 	libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
 	libbpf_set_print(libbpf_print_fn);

-	obj = opensnoop_bpf__open();
+	LIBBPF_OPTS(bpf_object_open_opts, opts, .btf_custom_path = "/tmp/vmlinux.btf");
+	obj = opensnoop_bpf__open_opts(&opts);
 	if (!obj) {
 		fprintf(stderr, "failed to open BPF object\n");
 		return 1;
$ cd bcc
$ git apply /tmp/btfgendemo/bcc.patch
$ cd libbpf-tools/
$ make -j$(nproc)

生成 “精简版” BTF 文件

这里,我们将使用 bpftool gen min_core_btf 命令为 BCC 工具中的 bindsnoop、execsnoop 和opensnoop 同时生成精简的 BTF 文件。下述的命令对目录中存在的每个 BTF 文件逐次调用 bpftool 工具进行精简。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ OBJ1=/tmp/btfgendemo/bcc/libbpf-tools/.output/bindsnoop.bpf.o
$ OBJ2=/tmp/btfgendemo/bcc/libbpf-tools/.output/execsnoop.bpf.o
$ OBJ3=/tmp/btfgendemo/bcc/libbpf-tools/.output/opensnoop.bpf.o

$ mkdir -p /tmp/btfgendemo/btfs
$ cd /tmp/btfgendemo/btfhub-archive/ubuntu/focal/x86_64/

$ for f in *.btf; do bpftool gen min_core_btf "$f" \
  /tmp/btfgendemo/btfs/$(basename "$f") $OBJ1 $OBJ2 $OBJ3; \
done

$ ls -lhn /tmp/btfgendemo/btfs | head
total 864K
-rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1007-azure.btf
-rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1009-aws.btf
-rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1009-gcp.btf
-rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1012-azure.btf
-rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1013-azure.btf
-rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1014-aws.btf
-rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1014-gcp.btf
-rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1015-azure.btf
-rw-r--r-- 1 1000 1000 1,1K Feb 8 14:46 5.11.0-1016-aws.btf

精简后生成的 BTF 文件大约为 1.1KB,Ubuntu Focal 对应的所有文件的大小为 864KB,将其与程序一起打包完全可行。

如果我们对生成的文件进一步进行压缩,其大小还可以大幅缩减:

1
2
3
4
$ cd /tmp/btfgendemo/btfs
$ tar cvfJ compressed.tar.xz *.btf
$ ls -lhn compressed.tar.xz
-rw-r--r-- 1 1000 1000 2,5K Feb 17 15:19 compressed.tar.xz

压缩率如此之高是因为许多生成的文件相同,我们将在下文中进一步讨论。

验证

为了验证,我们需要运行一台装有 Ubuntu Focal 的机器。这里提供的 Vagrant 文件可以用来创建对应的虚拟机。请注意,Ubuntu Focal 从内核 5.4.0-92-generic 版本开始启用 BTF 支持,所以我们需要运行其早期的版本进行验证。我们使用 bento/ubuntu-20.04 Vagrant 虚拟机中的 202012.21.0 版本,内核为 5.4.0-58-generic。

本文使用 sshfs 在主机和虚拟机之间共享文件,需要我们确保已经安装了 vagrant-sshfs 插件。

译者注:

1
$ sudo vagrant plugin install vagrant-sshfs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# /tmp/btfgendemo/Vagrantfile
Vagrant.configure("2") do | config |
  config.vm.box = "bento/ubuntu-20.04"
  config.vm.box_version = "= 202012.21.0"
  config.vm.synced_folder "/tmp/btfgendemo", "/btfgendemo", type: "sshfs"

  config.vm.provider "virtualbox" do | vb |
    vb.gui = false
    vb.cpus = 4
    vb.memory = "4096"
  end
end

启动虚拟机并使用 ssh 登录:

1
2
$ vagrant up
$ vagrant ssh

后续的命令必须在虚拟机内执行。检查内核版本:

1
2
$ uname -a
Linux vagrant 5.4.0-58-generic #64-Ubuntu SMP Wed Dec 9 08:16:25 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

让我们检查内核是否启用了CONFIG_DEBUG_INFO_BTF

1
2
$ cat /boot/config-$(uname -r) | grep CONFIG_DEBUG_INFO_BTF
CONFIG_DEBUG_INFO_BTF is not set

在把 BTF 文件复制到正确路径之前,我们尝试运行以下这些工具:

1
2
3
4
5
6
$ sudo /btfgendemo/bcc/libbpf-tools/execsnoop
libbpf: failed to parse target BTF: -2
libbpf: failed to perform CO-RE relocations: -2
libbpf: failed to load object 'execsnoop_bpf'
libbpf: failed to load BPF skeleton 'execsnoop_bpf': -2
failed to load BPF object: -2

正如预期,我们运行工具失败,因为工具不能找到执行 CO-RE 重定位所需的 BTF 信息。

接着,我们将该内核版本的 BTF 文件复制到对应目录:

1
$ cp /btfgendemo/btfs/$(uname -r).btf /tmp/vmlinux.btf

将复制 BTF 到指定目录后,工具运行正常:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ sudo /btfgendemo/bcc/libbpf-tools/execsnoop
PCOMM PID PPID RET ARGS
^C

$ sudo /btfgendemo/bcc/libbpf-tools/bindsnoop
PID COMM RET PROTO OPTS IF PORT ADDR
^C

$ sudo /btfgendemo/bcc/libbpf-tools/opensnoop
PID COMM FD ERR PATH
^C

当然这只是为了演示工具工作流程的样例。真正的集成需要负责基于主机的内核版本自动提供对应的 BTF 文件。下面的部分通过两个例子展示了对应的集成。

4.3 集成样例

在本节中,我们将介绍 Inspektor Gadget 和 Tracee 项目是如何使用 BTFGen。

Inspektor Gadget

Inspektor Gadget 是一个用于调试/检查 Kubernetes 资源和应用程序的工具集。由于Inspektor Gadget 是以容器镜像的形式发布的,我们选择在其中为不同的 Linux发行版搭载 BTF 文件。我们在 Docker 文件中添加了一个步骤,使其可以在构建容器镜像时生成 BTF 文件:

1
2
3
4
5
RUN set -ex; \
	if [ "$ENABLE_BTFGEN" = true ]; then \
		cd /btf-tools && \
		LIBBPFTOOLS=/objs BTFHUB=/tmp/btfhub INSPEKTOR_GADGET=/gadget ./btfgen.sh; \
	fi

辅助脚本 btfgen.sh 调用 bpftool 为 BTFHub 支持的所有内核生成 BTF 文件。

我们修改 entrypoint 脚本,在容器文件系统上安装正确的 BTF 文件,使对应的工具都能运行。Inspektor 工具被设计成总是在容器中运行,因此我们可以将 BTF 文件安装在系统路径(/boot/vmlinux-$(uname -r)),而不影响主机。通过这样做,我们还可以避免修改不同的 BCC 工具的源代码(就像我们在上面的例子中做的那样):

1
2
3
4
5
6
7
echo "Kernel provided BTF is not available: Trying shipped BTF files"
SOURCE_BTF=/btfs/$ID/$VERSION_ID/$ARCH/$KERNEL.btf
if [-f $SOURCE_BTF]; then
        objcopy --input binary --output elf64-little --rename-section .data=.BTF $SOURCE_BTF /boot/vmlinux-$KERNEL
        echo "shipped BTF available. Installed at /boot/vmlinux-$KERNEL"
else
...

PR 完整实现参见 inspektor-gadget/pull/387

Tracee

Tracee 是用于 Linux 的运行时安全和取证工具。这里,生成的 BTF 文件可被嵌入到应用程序的二进制文件中。Makefile 有一个 btfhub 目标,然后调用 btfhub.sh。脚本克隆 BTFHub 仓库,并调用 btfgen.sh 来生成 BTF 文件。这些文件被移到 ./dist/btfhub 目录中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# generate tailored BTFs

[ ! -f ./tools/btfgen.sh ] && die "could not find btfgen.sh"
./tools/btfgen.sh -a ${ARCH} -o $TRACEE_BPF_CORE

# move tailored BTFs to dist

[ ! -d ${BASEDIR}/dist ] && die "could not find dist directory"
[ ! -d ${BASEDIR}/dist/btfhub ] && mkdir ${BASEDIR}/dist/btfhub

rm -rf ${BASEDIR}/dist/btfhub/*
mv ./custom-archive/* ${BASEDIR}/dist/btfhub

然后,使用 go:embed 指令将 BTF 文件嵌入到 Go 二进制中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//go:build ebpf
// +build ebpf

package tracee

import (
	"embed"
)

//go:embed "dist/tracee.bpf.core.o"
//go:embed "dist/btfhub/*"

var BPFBundleInjected embed.FS

在运行时,当前内核的对应的 BTF文件被解压,其路径传递给 libbpf-go,用于 CO-RE 重定位。

4.4 限制

内核中的 BTF 支持不仅仅是关于导出 BTF 类型。部分 eBPF 程序如 fentry/fexit 和 LSM 钩子需要内核导出 BTF 信息。这些程序将不能使用 BTFGen,唯一的选择是启用 CONFIG_DEBUG_INFO_BTF 的内核。

4.5 未来发展

当然,我们知道 BTFGen 是一个临时的解决方案,直到大多数系统更新到默认导出 BTF 信息的内核版本。然而,我们认为这需要几年的时间,在这期间,BTFGen 可以帮助填补这一空白。

以下是我们可以近期考虑的一些改进。

与其他项目的整合

部分项目如 BCC 及其基于 libbpf 的工具都可以与 BTFGen 整合获益。 我们提交了一个 PR,通过使用 BTFGen 使上述工具可以在更多的 Linux 发行版中使用。

删除重复的文件

eBPF 程序通常访问很少的内核类型,因此,两个不同的内核版本生成的文件很有可能是相同的,这对于同一 Linux 发行版的小版本内核来说尤其如此。对 BTFGen 的进一步改进是基于此,通过使用符号链接或类似的方法来避免创建重复的文件。

这也可以直接在 BTFHub 上进行,因为有些源 BTF 文件是重复的,就像这个问题中所指出的那样,但即使在这种情况下,出现重复文件的机会还是较低。

在线 API

BTFHub 仓库体积很大,而且由于新内核的发布,它的规模还在不断增加。Seekret 创建了一个 API,使用 BTFGen 和 BTFHub 为用户提供的 eBPF 对象按需生成 “精简版” BTF 文件。

5. 更多信息

如果你想了解更多关于 eBPF、BTF、CO-RE、BTFHub 和 BTFGen 的信息,以下资料无疑是优秀的参考:

6. 致谢

功能从已经存在的项目中获得了灵感,并由不同的公司联合实现。

首先,我们要感谢 Aqua Security 团队在 BTFHub 上所做的出色工作,这是我们的基础项目。

其次,我们要感谢在这个功能的开发过程中做出贡献的人员。Aqua Security 的 Rafael David Tinoco 和 Elastic 的 Lorenzo Fontana 和 Leonardo Di Donato。

最后,libbpf 的维护者 Andrii Nakryiko 和 Alexei Starovoitov 以及 bpftool 的维护者 Quentin Monnet,他们为实现该功能提供了大量宝贵的反馈和指导。

原文地址: https://kinvolk.io/blog/2022/03/btfgen-one-step-closer-to-truly-portable-ebpf-programs/ Mauricio 2022 2022/03/16