1

在开发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=yCONFIG_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,不用再引入一大堆的内核头文件了。

image.png

三.内核升级

在内核升级之后,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程序编译一次,在不同版本的内核中运行的目的。

参考:

1.https://nakryiko.com/posts/bpf-core-reference-guide/


a朋
63 声望38 粉丝