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的, 不能用gunzip
或 gzip –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 | mkdir build && cd build |
完成后就或得到pahole可执行文件.
随即利用pahole执行:
1 | pahole --btf_encode_detached "vmlinux.btf" "vmlinux" |
就可以得到vmlinux
文件对于的BTF文件: vmlinux.btf
参考文档:
- https://github.com/aquasecurity/btfhub/blob/main/docs/how-to-use-pahole.md
- https://github.com/aquasecurity/btfhub/blob/main/tools/update.sh#L259
CO-RE代码的改造
手动加载BTF文件
在得到了BTF文件后, 如果直接使用BCC中bpf-tools下的工具并不会自动加载额外的BTF文件. 这是因为libbpf在代码 中默认会从以下几个预定义的位置查找BTF文件, 如果找不到就会报错:
1 | /* |
但是libbpf(较旧的版本是不支持的)也提供了相应的方法让我们手动去指定BTF的位置, 这里主要参考了参考文章 中Download, modify and compile BCC libbpf tools
一节提到的方法, 自行设置LIBBPF_OPTS
:
1 | - obj = xxxx_bpf__open(); |
通过使用LIBBPF_OPTS
来手动配置一个bpf_object_open_opts
, 将其参数btf_custom_path
设置为自定义的BTF文件位置, 其后使用open_opts
替代open
来达到加载自定义BTF的目的.
LIBBPF_OPTS
是一个通用的入口宏, 用于配置各种libbpf相关的配置项:
1 |
|
参考:
- https://kinvolk.io/blog/2022/03/btfgen-one-step-closer-to-truly-portable-ebpf-programs/
- https://lore.kernel.org/all/CAHb-xaurgV1ukr4OMNQM1DVPXN5Gavd8qvYmVpus74uG+mKyxw@mail.gmail.com/T/
BPF兼容性
bss段错误/Global Memory不支持
参考libbpf-bootstrap
项目中的minimal_legacy 和minimal 可以发现, 在不支持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 | wget --no-check-certificate -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - |
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的代码.