0%

BPF CO-RE--在旧内核上运行(自行制作BTF)

BPF CO-RE–在旧内核上运行(自行制作BTF)

最近开始接触到BPF CO-RE, 本文记录下自己的学习使用心得. 首先不废话, 直入主题: 如何在旧的内核上把BPF CO-RE程序跑起来.

内核支持

本文这里提及的旧的内核是指像Linux 4.18.20这样的并不是自带BTF的内核. 对于5.X的内核, 其会在/sys/kernel/btf/vmlinux 导出一个BTF, CO-RE的程序加载时会去查找这一个位置从而读取到对应的BTF数据. 而相对老一点的内核是不存在这一支持的(内核不存在CONFIG_DEBUG_INFO_BTF=y这一编译配置), 但是查询BCC项目下的kernel-versions 可以发现内核其实从3.15开始就已经逐渐支持ebpf了, 并且JIT支持对于arm64, x86_64来说也都是在3.16/3.18之后开始支持了, 那么在4.X版本的内核上对于这两种架构来说也是理论上可以运行的, 这里就需要我们通过一些方法来让CO-RE的程序正常跑起来.

旧内核的BTF制作

首先, 我们需要对内核生成一个其对应的BTF文件. 参考BTFHUB中的文档 我们知道了可以通过使用pahole工具来生成一个BTF文件, 或者通过bpftool/llvm 来将BTF相关的段加到内核镜像中.

各个工具的基本的思路都是通过将内核的DWARF转换为BTF. 这里我以单独生成一个BTF文件为例, 说下我是如何进行的:

获取vmlinux镜像

首先获得一个没有进行strip或者进行压缩的内核镜像文件vmlinux.

一种方式就是通过内核的源码, 配置内核使用的config文件重新编译一个.也可以通过apt, yum等包管理器下载一个当前版本内核的vmlinux镜像.

这里说下, ubuntu, fedora等发行版提供的位于/boot下的内核镜像名称一般为vmlinz-x.x.x-x, 这是vmlinux的压缩版, 是由ELF文件vmlinux经过OBJCOPY和压缩后的文件. 通常情况下是不能用vmlinuz解压缩得到vmlinux的, 不能用gunzipgzip –dc解包vmlinuz.

内核源代码中存在一个脚本可以将vmlinuz转化为vmlinx文件(即解压缩内核文件) linux/scripts/extract-vmlinux 或者参考: https://superuser.com/questions/298826/how-do-i-uncompress-vmlinuz-to-vmlinux

导出BTF文件

在获得了vmlinux后, 就可以很轻松地利用pahole工具导出BTF了. pahole是在内核开发者中比较被熟知的工具, 其作用不仅仅是导出BTF, 相反导出BTF是最近才被集成进入的一个功能. 因此就需要我们下载最新版本的pahole源码编译使用.

源代码下载: https://git.kernel.org/pub/scm/devel/pahole/pahole.git

下载后按照官方的说明about1. https://git.kernel.org/pub/scm/devel/pahole/pahole.git/about/) 进行编译即可:

1
2
3
mkdir build && cd build
cmake -D__LIB=lib ..
make install

完成后就或得到pahole可执行文件.

随即利用pahole执行:

1
pahole --btf_encode_detached "vmlinux.btf" "vmlinux"

就可以得到vmlinux文件对于的BTF文件: vmlinux.btf

参考文档:

CO-RE代码的改造

手动加载BTF文件

在得到了BTF文件后, 如果直接使用BCC中bpf-tools下的工具并不会自动加载额外的BTF文件. 这是因为libbpf在代码 中默认会从以下几个预定义的位置查找BTF文件, 如果找不到就会报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
* Probe few well-known locations for vmlinux kernel image and try to load BTF
* data out of it to use for target BTF.
*/
struct btf *btf__load_vmlinux_btf(void)
{
struct {
const char *path_fmt;
bool raw_btf;
} locations[] = {
/* try canonical vmlinux BTF through sysfs first */
{ "/sys/kernel/btf/vmlinux", true /* raw BTF */ },
/* fall back to trying to find vmlinux ELF on disk otherwise */
{ "/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" },
};
char path[PATH_MAX + 1];
struct utsname buf;
struct btf *btf;
int i, err;

...

pr_warn("failed to find valid kernel BTF\n");
return libbpf_err_ptr(-ESRCH);

但是libbpf(较旧的版本是不支持的)也提供了相应的方法让我们手动去指定BTF的位置, 这里主要参考了参考文章Download, modify and compile BCC libbpf tools一节提到的方法, 自行设置LIBBPF_OPTS:

1
2
3
-	obj = xxxx_bpf__open();
+ LIBBPF_OPTS(bpf_object_open_opts, opts, .btf_custom_path = "/tmp/vmlinux.btf");
+ obj = xxxx_bpf__open_opts(&opts);

通过使用LIBBPF_OPTS来手动配置一个bpf_object_open_opts, 将其参数btf_custom_path设置为自定义的BTF文件位置, 其后使用open_opts替代open来达到加载自定义BTF的目的.

LIBBPF_OPTS 是一个通用的入口宏, 用于配置各种libbpf相关的配置项:

1
2
3
4
5
6
7
8
#define LIBBPF_OPTS(TYPE, NAME, ...)					    \
struct TYPE NAME = ({ \
memset(&NAME, 0, sizeof(struct TYPE)); \
(struct TYPE) { \
.sz = sizeof(struct TYPE), \
__VA_ARGS__ \
}; \
})

参考:

BPF兼容性

bss段错误/Global Memory不支持

参考libbpf-bootstrap 项目中的minimal_legacyminimal 可以发现, 在不支持BPF global memory的旧的内核上运行CO-RE程序需要先行关闭对于global memory的支持, 否则无法正常编译, 会报与rodata_str1_1 bss段相关编译失败错误. 解决方法也很简单: 在内核侧的bpf程序的第一行加入一个预定义#define BPF_NO_GLOBAL_DATA 去关闭global memory. 这时可能就会遇到问题, 如果让用户态的程序传递一些配置参数信息给内核态程序使用? 在minimal_legacy 中也提供了很好的例子: 使用bpf_map传递参数, 并在每次使用前从map中查询对应的数据. 也正是因为这样会引入不少运行时的开销, libbpf才会在新的内核中支持global data去消除这种每次都查询的开销.

可以通过bpftool工具去检查当前内核支持哪些特性, 有哪些内核的CONFIG被开启了, 哪些BPF_MAP可以使用等信息:
sudo bpftool feature probe

Tracepoint不支持

这里顺带提一下, 编译minimal_lagacy得到的程序可能在某些4.X的内核上会报错: libbpf: failed to open '/sys/kernel/debug/tracing/events/syscalls/sys_enter_write/id': No such file or directory, 因为其内核侧的Tracepoint, 这是因为tp/syscalls/sys_enter_write 这个Tracepoint不被这个版本的内核支持. 查询当期版本内核有哪些Tracepoint可以被使用, 可以通过sudo perf list|grep Tracepoint 的方式去查询.

Clang/LLVM版本必须>10

在编译BPF的程序时, 有可能会遇到libbpf: BTF is required, but is missing or corrupted.的报错, 这个问题经检查发现是因为使用的llvm-clang版本太低(<10)导致的. 因此想要正确的编译BPF程序必须按照官方的建议, 安装clang > 10, LLVM >10的版本即可.

由于较旧的Ubuntu上, 如18.04上, 官方的repo中可能没有提供>10的版本, 那么可以通过如下方式去利用LLVM官方提供的repo去安装, 这里以安装12为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
wget --no-check-certificate -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
add-apt-repository 'deb http://apt.llvm.org/bionic/ llvm-toolchain-bionic-12 main'
sudo apt update

sudo apt install -y clang-12 libclang-12-dev libclang-common-12-dev libclang1-12 libclang-cpp12 liblldb-12 libllvm12 libpfm4 libxml2-dev libz3-4 libz3-dev lldb-12 llvm-12 llvm-12-dev llvm-12-linker-tools llvm-12-runtime llvm-12-tools python3-lldb-12

sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-12 12
sudo update-alternatives --install /usr/bin/llvm-config llvm-config /usr/bin/llvm-config-12 12
sudo update-alternatives --install /usr/bin/llvm-strip llvm-strip /usr/bin/llvm-strip-12 12
sudo update-alternatives --install /usr/bin/llc llc /usr/bin/llc-12 12

# 通过如下方式去配置默认的llvm-clang工具版本, 如果系统中只有一个版本则可以跳过
sudo update-alternatives --config clang
sudo update-alternatives --config llvm-config
sudo update-alternatives --config llvm-strip
sudo update-alternatives --config llc

error: incomplete definition of type ‘struct bpf_link’

如果遇到如上的错误, 请检查执行make的时候是否开启了CO-RE: clang-bpf-co-re: [ on ] , 使用CLANG=Nope make去解决

编译提示缺少asm/types.h头文件

这种问题需要查看/usr/include/asm是否正确link到/usr/include/$(uname -m)-linux-gnu/asm上, 没有ln一下即可

4.X内核不支持ring buffer

参考https://github.com/iovisor/bcc/blob/master/docs/kernel-versions.md 或者bpftool的输出可以知道在5.8以后的内核上才支持BPF ring buffer, 所以使用这种数据结构的代码需要被替换为使用其他bpf map的代码.