在开发eBPF程序时,比如调用内核辅助函数bpf_probe_read(),需要获得内核的数据结构定义,这通常需要安装内核头文件:
- linux-header-${uname -r}
而内核头文件的路径和数据结构定义,在不同内核版本中不同,因此,在升级内核版本时,可能会出现问题。
于此同时,很多生产环境的机器,处于安全考虑,不允许安装内核头文件,就无法获得内核数据结构的定义。
BTF(BPF Type Format)解决了这一问题。
BTF让CO-RE(Compile once, run everywhere)成为可能。
若要实现CO-RE,需要解决的问题:
内核头文件:ebpf程序依赖的数据结构,在不同的内核版本中可能不同;
- 这通过vmlinux.h解决;
内核升级后,ebpf程序仍然正常运行;
- 这通过clang编译器记录字段偏移量和ebpfLoader计算偏移量来解决;
一.内核选项
若使用CO-RE(compile once, run everywhere),需要开启内核选项:
CONFIG_DEBUG_INFO_BTF=y
- 开启表示允许内核生成BTF信息,用于调试和性能分析;
CONFIG_DEBUG_INFO=y
- 开启表示允许内核在编译过程中生成详细的调试信息;
查看内核选项是否开启:
# cat /boot/config-$(uname -r) | grep "CONFIG_DEBUG_INFO"
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_BTF=y
如果输出显示为CONFIG_DEBUG_INFO_BTF=y
和CONFIG_DEBUG_INFO=y
,则表示这两个选项已启用。
若未开启,则需要修改内核选项,重新编译并安装内核。
二.内核头文件
从内核5.2开始,只要开启了CONFIG_DEBUG_INFO_BTF,在编译内核时,内核数据结构的定义就会自动内嵌在内核二进制文件vmlinux中。
并且,借助bpftool命令,可以将这些数据结构的定义导出到vmlinux.h中:
# bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
有了内核数据结构的定义,在开发ebpf程序时,只需要引入vmlinux.h,不用再引入一大堆的内核头文件了。
三.内核升级
在内核升级之后,ebpf程序仍然可以正常运行。
假如内核有个struct foo,在不同的内核版本,定义有变化:
//4.x的内核版本
struct foo {
int a;
int b;
int c;
}
//5.x的内核版本
struct foo {
int a;
int b;
int x; //新版本内核中新增了一个字段
int c;
}
然后,在ebpf程序中,需要访问struct foo中的字段c:
SEC("kprobe/xxx")
int BPF_KPROBE(xxx,struct foo * p foo)
{
int read_c;
// bpf_probe_read_kernel 的函数声明: long bpf_probe_read_kernel(void *dst,u32 size, const void *unsafe_ptr);
//如果是4.x内核,c字段的偏移量=2个int
bpf_probe_read_kernel(&read_c, sizeof(int), p_foo + 2 * sizeof(int));
//如果是5.x内核,c字段的偏移量=3个int
bpf_probe_read_kernel(&read_c, sizeof(int), p_foo + 3 * sizeof(int));
}
原来在4.x内核版本运行的ebpf程序,迁移到5.x就会出问题。
启用BTF内核选项后,使用bpf_core_read()辅助函数,可以在不同的内核版本上,正确读出目标字段信息。
首先
,clang编译器记录结构体字段重定位的信息
bpf_core_read()的宏定义:
#define bpf_core_read(dst, sz, src) bpf_probe read kernel(dst, sz, (const void *) builtin_preserve_access_index(src))
其中builtin_preserve_access_index提示clang编译器,在编译时增加结构体字段重定位的信息。
比如:
bpf_core read(&read c,sizeof(int),p_foo->c)
经过宏展开后:
bpf_probe_read_kernel(&read c, sizeofint), builtin_preserve access_index(p_foo->c))
clang编译这段代码时,会增加描述信息,访问p_foo—>c字段时,需要根据当前内核的BTF信息,重新计算偏移量。
然后
,ebpf Loader计算字段的偏移量。
libbpf加载ebpf程序到内核时,找到clang编译器记录的偏移量信息,根据当前内核的BTF信息,重新计算偏移量。
这样p_foo->c访问访问新版本内核的字段c时,就能得到正确的偏移量信息,bfp_core_read()就能读到正确的信息。
最终达到了ebpf程序编译一次,在不同版本的内核中运行的目的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。