Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpf-co-re #65

Open
BruceChen7 opened this issue May 8, 2023 · 0 comments
Open

bpf-co-re #65

BruceChen7 opened this issue May 8, 2023 · 0 comments

Comments

@BruceChen7
Copy link
Owner

BruceChen7 commented May 8, 2023

参考资料

总结

  • 帮助 BPF 开发者以简单的方式解决简单的可移植性问题(如读取结构字段)
  • 并使解决复杂的可移植性问题(如【不兼容的数据结构变化】、复杂的用户空间控制条件等)成为可能(即使不是微不足道,也是能容忍的)。
  • vmlinux.h消除了对内核头文件的依赖
  • 字段重定位(字段偏移、存在、大小等)使得从内核中提取的数据能移植。
  • libbpf 提供的 Kconfig 外部变量允许 BPF 程序适应各种内核版本和配置的变化。

BPF CO-RE 的使用

  • libbpf-tools
  • 随着越来越多的 Linux 发行版默认启用内核 BTF代替 bcc 来写这些工具
  • libbpf-bootstrap项目的创建是为了简化用 libbpf 和 BPF CO-RE 开始的 BPF 开发,提供脚手架代码。
  • libbootrap 的使用
  • 特点
    • 类型大小和存在的重新定位。当类型被添加、移除或重命名时,能够检测到这一点并相应地调整 BPF 应用逻辑是很重要的。
      • 参见 libbpf 提供的 bpf_core_type_exists() 和 bpf_core_type_size() 宏
    • 枚举重定位(存在和价值)。
      • 一些内部的、非UAPI 的内核枚举确实在不同的内核版本中发生了变化,甚至依赖于用于内核编译的精确配置
        *(例如,枚举 cgroup_subsys_id,见 BPF selftest 处理它),使得无法可靠地硬编码任何特定的值。
      • 枚举重定位(bpf_core_enum_value_exists() 和 bpf_core_enum_value() 宏,由 libbpf 提供)
        • 允许检查特定枚举值的存在并捕获其值。这方面的一个重要应用是检测新的 BPF helper 的可用性,如果内核太旧,则回落到旧的。
    • 当用只读全局变量编译时,这两者对于从 BPF 方面进行简单而可靠的内核特性检测是必不可少的

BTF

  • BTF(BPF 类型格式)是作为更通用和更冗长的 DWARF 调试信息的替代品而创建的。
  • BTF 是一种节省空间的、紧凑的、但仍有足够表现力的格式来描述 C 程序的所有类型信息
  • 由于它的简单性和BTF 重复数据删除算法 (BTF deduplication algorithm),与 DWARF 相比,BTF 允许实现高达 100 倍的大小减少
  • 现在,让 Linux 内核在运行时始终存在嵌入式 BTF 类型信息是切实可行的:只要用 CONFIG_DEBUG_INFO_BTF=y 选项构建内核。内核的 BTF 对内核本身是可用的,并且现在被用来增强 BPF 验证器自身的能力
  • 方便了解在 eBPF 程序编译和即将运行的结构布局之间的差异,以便在程序加载到内核时进行适当的调整
  • 使得 bpftool 能够在翻译或 JIT 程序 dumps 的输出中插入源代码,
  • eBPF 程序使用 bpf_spin_lock() 和 bpf_spin_unlock() 辅助函数来获取和释放锁。这些辅助函数只能在有 BTF 信息可用来描述锁字段在结构体中位置时使用。
  • 对于 BPF CO-RE 来说更重要的是,内核也通过**/sys/kernel/btf/vmlinux的sysfs公开了这个自我描述的BTF信息**
    $ bpftool btf dump file /sys/kernel/btf/vmlinux format c
    • 得到一个包含所有内核类型的可编译的 C 头文件(通常被称为 "vmlinux.h")。
    • 该文件定义了内核的所有数据类型,因此在 eBPF 程序源代码中包含这个生成的 vmlinux.h 文件能提供你可能需要的任何 Linux 数据结构的定义
    • 将源代码编译为 eBP obj files,该对象将包含与该头文件中使用的定义相匹配的 BTF 信息。
    • 稍后,在目标机器上运行程序时,将加载它进入内核的用户空间程序将对比这个构建时的 BTF 信息和运行在目标机器上的内核的 BTF 信息之间的差异进行调整。
    • 自 Linux 内核 5.4 版本起,以/sys/kernel/btf/vmlinux 文件的形式包含了 BTF 信息,
  • libbpf 能利用原始的 BTF 数据生成较旧内核的 BTF 数据
    • 换句话说,如果你想在没有 BTF 信息的目标机器上运行启用 CO-RE 的 eBPF 程序,你能自己为该目标提供 BTF 数据
    • 如何生成,能使用aquasecurity/btfhub: BTFHub

Compiler support

  • 为了启用 BPF CO-RE 并让 BPF 加载器(即 libbpf)根据目标主机上运行的特定内核调整 BPF 程序,Clang 被**扩展了一些内置插件。
    • 它们发出 BTF 重定位,捕获 BPF 程序代码打算读取哪些信息的高级描述**。
  • 如果要访问 task_struct->pid 字段,clang 会记录它正是一个名为【pid】的字段,其类型为 "pid_t",位于 struct task_struct 中。
    • 这样做的目的是,即使目标内核的 task_struct 布局中,"pid "字段被移到了 task_struct 结构中的不同偏移量(例如,由于在 "pid "字段之前添加了额外的字段)
    • 或者即使它被移到了一些嵌套的匿名结构或 union 中(这在 C 代码中是完全透明的,所以没有人注意到这样的细节),
    • 仍然能够通过它的名字和类型信息找到它。这就是所谓的字段偏移重定位
    • 不仅能捕获(并随后重新定位)字段偏移量,还能捕获其他字段方面的信息,如**字段存在或大小。
    • 即使是位域**(在 C 语言中是出了名的 "不合作 "的数据种类,抵制使其可重定位的努力),也仍然有可能捕获足够的信息使其可重定位,
    • 所有这些对 BPF 程序开发员来说都是透明的。

BPF loader (libbpf)

  • 之前的数据(内核 BTF 和 Clang 重定位)都在一起,并由 libbpf 处理,它作为一个 BPF 程序加载器。
  • 它接收编译好的BPF ELF 对象文件,根据需要进行后处理,设置各种内核对象(map, programes 等),并触发BPF 程序加载和验证
  • Libbpf 知道如何使 BPF 程序代码适合主机上的特定运行内核
    • 它查看 BPF 程序记录的BTF 类型和重定位信息,并将它们与运行中的内核提供的 BTF 信息相匹配
    • Libbpf解析并匹配所有的类型和字段,根据需要更新必要的偏移量和其他可重定位的数据,以确保 BPF 程序的逻辑对主机上的特定内核正确运行。
    • 如果一切正常,程序开发者)将得到一个 BPF 程序,为目标主机上的内核 "定制",就像的程序是专门为它编译的。
    • 但所有这些都是在没有支付与你的应用程序一起发布 Clang 和在目标主机上运行时执行编译的开销的情况下实现的。

kernel

  • 内核不需要做太多的改动就能支持 BPF CO-RE
  • 由于关注点的良好分离,在 libbpf 处理了 BPF 程序代码之后,对内核来说,它看起来就像任何其他有效的 BPF 程序代码。
  • 它与在主机上用最新的内核头编译的 BPF 程序没有区别。
  • 这意味着 BPF CO-RE 的很多功能都不需要特殊的内核功能,因此能更广泛和更快地被适应。

获取 kernel data

  • 最常见的BPF CO-RE 操作是从一些内核结构中读取一个字段的值
  • libbpf 提供了整个系列 helper 函数来使读取一个字段变得简单和 CO-RE-relocatable
  • CO-RE-relocatable 意味着不管一个结构的实际内存布局如何(它可能根据实际的内核版本和使用的内核配置而改变),BPF 程序将被调整为在相对于结构开始的正确实际偏移处读取字段。

bpf_core_read

  • 以 CO-RE 可重定位的方式读取字段的最基本辅助工具是bpf_core_read(dst, sz, src),它将从 src 引用的字段中读取 sz 字节到 dst 指向的内存中。
    struct task_struct *task = (void *)bpf_get_current_task();
    struct task_struct *parent_task;
    int err;
    
    err = bpf_core_read(&parent_task, sizeof(void *), &task->parent);
    if (err) {
        /* handle error */
    }
    
    /* parent_task contains the value of task->parent pointer */
  • bpf_core_read() 就像 bpf_probe_read_kernel() BPF helper 函数一样,只是它记录了在目标内核上应该被重新定位的字段的信息
  • 如果父字段由于前面添加了一些新字段而被转移到 struct task_struct 中的不同偏移量,libbpf 会自动将实际偏移量调整到合适的值。
  • 字段的大小不会被自动重新定位 (sizeof),只有它的偏移量
  • 比如说,一个结构体,而它的大小发生了变化,你就会遇到麻烦
  • 一般的建议是,如果可能的话,不要把结构字段作为一个整体来读。最好是只读你最终感兴趣的原始字段

bpf_core_read_str

  • 就像 bpf_probe_read_kernel() 和 bpf_probe_read_kernel_str() 这两个 BPF helper 一样,
    • 前者读取指定数量的字节
    • 而后者读取一个可变长度的 C 字符串,
  • bpf_core_read_str() 也有一个对应于 bpf_core_read() 的版本。
  • 它的工作原理和 bpf_probe_read_kernel_str() 一样,只是它记录了源字符数组字段的 CO-RE 重定位信息,其中包含一个零端 C 字符串
  • 所以 bpf_core_read_str() 是 bpf_probe_read_kernel_str() 的 CO-RE-relocatable 版本。
  • 注意字符数组字段和字符指针字段之间重要但微妙的区别。
    • 在 C 语言中,当读取字符串值时,它们能互换使用,因为数组会被编译器自动视为一个指针。
    • 不过在 CO-RE 的上下文中,这种区别是非常重要的。
  • 想要读取的假设的内核类型。
    struct my_kernel_type {
        const char *name;
        char type[32];
    };
  • name 字段指向一个字符串的存储位置,但 type 字段实际上是包含字符串的内存。
  • 如果你用 CO-RE 读取一个由名字指向的字符串,正确的处理方式是先用 CO-RE 可重定位的方式读取指针的值,然后做一个普通的(非 CO-RE)bpf_probe_read_kernel_str() 读取
  • 为了简洁,忽略了错误处理。
    struct my_kernel_type *t = ...;
    const char *p;
    char str[32];
    
    /* get string pointer, CO-RE-relocatable */
    bpf_core_read(&p, sizeof(p), &t->name);
    /* read the string, non-CO-RE-relocatable, pointer is valid regardless */
    bpf_probe_read_kernel_str(str, sizeof(str), p);
  • 如果需要读取字符串类型,相应的例子将是
    struct my_kernel_type *t = ...;
    char str[32];
    /* read string as CO-RE-relocatable */
    bpf_core_read_str(str, sizeof(str), &t->type);
  • 为什么第一个例子不能用bpf_core_read_str()(提示:会把指针值解释为 C 字符串本身),
  • 为什么第二个例子不能用指针读取然后再读字符串(提示:字符串本身是结构的一部分,所以没有专门的指针,它位于相对于 t 指针指向的偏移处)。

BPF_CORE_READ

  • bpf_core_read() 虽然允许大量的控制和谨慎的错误处理,但直接使用是一个相当大的负担,特别是在读取需要通过较长的指针解除引用链访问的字段时

  • 读取运行进程的主可执行文件名的例子。

  • 如果你用 C 语言写一个普通的内核代码并想做到这一点,你必须做这样的事情。

    struct task_struct *t = .....;
    const char *name;
    
    name = t->mm->exe_file->fpath.dentry->d_name.name
  • 注意这一连串的指针解除引用,与一些子结构的访问(即 fpath.dentry 和 d_name.name)混在一起。

  • 用 bpf_core_read() 做这样的事情,很快就会变成一团糟。

    struct task_struct *t = ...;
    struct mm_struct *mm;
    struct file *exe_file;
    struct dentry *dentry;
    const char *name;
    
    bpf_core_read(&mm, 8, &t->mm);
    bpf_core_read(&exe_file, 8, &mm->exe_file);
    bpf_core_read(&dentry, 8, &exe_file->path.dentry);
    bpf_core_read(&name, 8, &dentry->d_name.name);
    /* now read string contents with bpf_probe_read_kernel_str() */
  • 当然,这是一个相当极端的例子,通常指针解除引用链不会那么长,但重点是:使用这种方法做起来很痛苦。

  • 为了使这种多步骤读取更容易编写,libbpf 提供了 BPF_CORE_READ() 宏。

  • 使用 BPF_CORE_READ() 后,上面的代码是如何简化的。
    ```c
    struct task_struct *t = ...;
    const char *name;

      name = BPF_CORE_READ(t, mm, exe_file, fpath.dentry, d_name.name);
    
      /* now read string contents with bpf_probe_read_kernel_str() */
    
      /* direct pointer dereference */
      name = t->mm->exe_file->fpath.dentry->d_name.name;
    
      // 等价
      /* using BPF_CORE_READ() helper */
      name = BPF_CORE_READ(t, mm, exe_file, fpath.dentry, d_name.name);
      ```
    
  • BPF_CORE_READ() 直接返回读值,并且不把错误传播回去。

  • 如果任何一个指针是 NULL 或指向无效的内存,你将得到 0(或 NULL)的响应

  • 如果确实需要错误传播和处理,你必须使用低级别的 bpf_core_read() 原语并明确地处理错误

  • 这在实践中通常不是一个问题,也不是必须的。

BPF_CORE_READ_INTO

  • 将结果读入目标内存而不是像 BPF_CORE_READ() 那样直接返回是必要的或更方便的
  • 例如,直接返回值不起作用的常见情况是,
    • 当正在读取一个 C 语言数组(例如,从 socket 结构中读取 IPv4 地址)时,因为 C 语言不允许直接从表达式返回数组
    • 对于这种情况,libbpf 提供了 BPF_CORE_READ_INTO() 宏,它的行为类似于 BPF_CORE_READ(),但是将最终字段的值读入目标内存
    • 将上一个例子转换为 BPF_CORE_READ_INTO(),得到
      struct task_struct *t = ...;
      const char *name;
      int err;
      
      err = BPF_CORE_READ_INTO(&name, t, mm, binfmt, executable, fpath.dentry, d_name.name);
      if (err) { /* handle errors */ }
      /* now `name` contains the pointer to the string */
    • 注意进入 BPF_CORE_READ_INTO() 的额外的&name 参数,以及有可能得到最后一次操作(即读取 d_name.name)的错误代码的事实。
    • 不过总的来说,BPF_CORE_READ() 在实践中更方便,更容易阅读

BPF_CORE_READ_STR_INTO()

  • 对于最后一个字段是一个字符数组字段的情况(就像上面假设的例子中的名字与类型),有一个 BPF_CORE_READ_STR_INTO() 宏。

处理内核变化和特性检测

  • BPF_CORE_READ() 系列的宏是 BPF CO-RE 的主力,但是用 BPF CO-RE 建立实际的 BPF 应用还有更多的内容。
  • BPF 应用程序必须处理的一个非常普遍的问题是需要进行feature 检测
    • 检测一个特定的主机内核是否支持一些新的和可选的特性,BPF 应用能利用这些特性来获得更多的信息或提高工作效率。
    • 不过,如果不支持,BPF 应用程序宁愿退回到支持旧内核的代码,而不是直接失败

bpf_core_field_exists()

  • bpf_core_field_exists() 允许检查一个给定的内核类型是否包含一个指定的字段
  • 在内核 feature 检测的背景下,如果某些所需的内核特征是伴随着向某个内核类型添加某些特定字段而添加的,那么能通过直接的 bpf_core_field_exists() 检查来检测这种特征。
  • 作为一个具体的例子,检测内核是否支持基于 perf 的 BPF 程序类型
    union bpf_attr *attr = .../* 可能是 NULL */;
    
    if (bpf_core_field_exists(attr->link_create.perf_event.bpf_cookie)) {
    /* bpf_cookie 被支持 */
    } else {
    /* bpf_cookie 不被支持 */ 。
    }
  • 上面假定 BPF 程序有一个联合 bpf_attr*类型的变量。
  • 它能只是 NULL,这其实并不重要,因为指针本身永远不会被读取,它只是为了向编译器传达类型信息而必须的。
  • 对于没有现成的所需类型的变量的情况,你能写同等的检查(使用 C 类型系统的特性)。
    if (bpf_core_field_exists(
            ((union bpf_attr *)0)->link_create.perf_event.bpf_cookie) {
        /* bpf_cookie is supported */
    } else {
        /* bpf_cookie is NOT supported */
    }
  • 在这里,如果主机内核的 union bpf_attr 中没有 link_create.perf_event.bpf_cookie,那么 if/else 的第一个分支中的代码将永远不会被执行(也不会被验证)。
  • 这样的代码会被 BPF 验证器正确检测为死代码,所以永远不会被验证。
    • 这意味着这样的代码能使用主机内核上不存在的内核和 BPF 功能(例如,新的 BPF helper),而不用担心 BPF 验证失败。
    • 例如,如果上面的第一个分支使用 BPF cookie 功能的 bpf_get_attach_cookie() helper,那么该程序将在尚未有该 helper 的旧内核上被正确验证。

bpf_core_type_exists()

  • 对于类型存在本身就很重要的情况,BPF CO-RE 提供了一种检查类型存在的方法 bpf_core_type_exists() helper。
  • 下面是一个检测内核是否支持 BPF 环形缓冲器的例子。
    if (bpf_core_type_exists(struct bpf_ringbuf)) {
    /* BPF 环形缓冲器助手(例如 bpf_ringbuf_reserve())存在 */
    }
  • 注意确保你在某处定义了一个 bpf_ringbuf 结构(即使是空的),否则你就会检查 bpf_ringuf 前向声明是否存在,这几乎肯定不是你想要的结果。
  • 这在最近的 vmlinux.h 中不应该是个问题,但要注意。

bpf_core_enum_value_exists()

  • 能够检测一个给定的枚举器值是否存在是非常有用的
  • 这种检查的一个重要的实际应用是检测对一个 BPF helper 的支持。
  • 每个 BPF helper 在枚举 bpf_func_id 中都有一个相应的枚举值。
    enum bpf_func_id {
    ...
    BPF_FUNC_ringbuf_output = 130BPF_FUNC_ringbuf_reserve = 131。
    ...
    };
  • 因此,检查 BPF helperbpf_xxx() 是否存在的最直接的方法是检查 BPF_FUNC_xxx 在枚举 bpf_func_id 中是否存在。
  • 因此,与其在前面的例子中用 bpf_core_type_exists(struct bpf_ringbuf) 做类型检查,能更明确地说明意图。
    ifbpf_core_enum_value_exists(enum bpf_func_id, BPF_FUNC_ringbuf_reserve)) {
    /* 安全地使用 bpf_ringbuf_reserve() */
    } else {
    /*退回到使用 bpf_perf_event_output()*/。
    }
  • 很多其他的 BPF 功能能被类似地检测到。
  • BPF program 类型和 BPF map 类型支持只是另外一个例子。
  • 当然,这种功能并不限于 BPF 相关的功能。
  • 任何能通过字段、类型或枚举器值的存在来检测的内核功能都能用 BPF CO-RE 轻松完成。
  • feature 检测也不会止于基于类型系统的检查。

LINUX_KERNEL_VERSION

  • 检测是否存在必要的功能的唯一方法是通过检查 Linux 内核的版本

  • Libbpf 允许使用一个特殊的外部变量从 BPF 程序代码中做到这一点。

    extern int LINUX_KERNEL_VERSION __kconfig
  • 一旦声明,LINUX_KERNEL_VERSION 就会以与内核本身完全相同的方式对运行中的内核版本进行编码。

  • 这样一个变量能像其他变量一样被使用:你能和它进行比较,打印它,记录和发送它到用户空间等等。

  • 在所有这些情况下,BPF 验证器都知道它的精确值,因此它能检测到死代码,就像上面描述的基于类型系统的检查一样。

  • Libbpf 还提供了一个方便的KERNEL_VERSION(major, minor, patch)宏,用于与 LINUX_KERNEL_VERSION 进行比较。

    #include <bpf/bpf_helpers.h>
    extern int LINUX_KERNEL_VERSION __kconfig;
    ...
    if (LINUX_KERNEL_VERSION > KERNEL_VERSION(5, 15, 0)) {
        /* we are on v5.15+ */
    }

Kconfig extern variables

  • libbpf 允许为任何内核配置(Kconfig)值声明特殊的外部变量
  • 只有当内核通过/proc/config.gz 公开其内核配置时才支持,幸运的是,这在现代 Linux 发行版中是非常常见的情况。
  • 有几种不同类型的变量被支持。
  • 它们的使用取决于实际的 Kconfig 值类型。
    • 对于 y/n/m 三态 Kconfig 值,你能使用 extern enum libbpf_tristate 变量,它有三种可能的定义值。
      • TRI_YES, TRI_NO, TRI_MODULE, 分别。
      • 另外,声明一个 extern char 变量能捕捉到原封不动的字符值(也就是说,真的会有一个具有'y'、'n'、'm'字符值之一的变量)。
      • 对于 y/n 双状态(布尔)的 Kconfig 值,你也能使用 bool 类型(除了已经描述的 char 和 enum libbpf_tristate 类型之外)。
        • 在这种情况下,y 映射为真,n 将变成假。
      • 对于整数的 Kconfig 值,使用 C 的整数类型之一:所有 1、2、4 和 8 字节的有符号和无符号整数都支持。
      • 如果实际的 Kconfig 值不适合声明的整数类型,libbpf 将发出一个错误而不是截断该值。
      • 对于字符串 Kconfig 值,使用 const char[N] 数组变量。如果实际值不合适,它将被截断并以零结尾,但 libbpf 会发出一个警告。
  • 如果请求的 Kconfig 值在/proc/config.gz 中丢失,libbpf 将以错误中止程序加载
  • 为了优雅地处理这个问题,能用__weak 属性将这种 Kconfig 外部变量声明为弱变量。
  • 在这种情况下,如果值丢失,它将被假定为 false、TRI_NO、'\0'(零字符)、0 或""(空字符串),这取决于使用的类型。
  • 下面是一个声明和使用不同类型的 Kconfig 外部变量的例子
    extern int LINUX_KERNEL_VERSION __kconfig;
    
    extern enum libbpf_tristate CONFIG_BPF_PRELOAD __kconfig __weak;
    extern bool CONFIG_BPF_JIT_ALWAYS_ON __kconfig __weak;
    extern char CONFIG_BPF_JIT_DEFAULT_ON __kconfig __weak;
    extern int CONFIG_HZ __kconfig;
    extern const char CONFIG_MODPROBE_PATH[256] __kconfig __weak;
    
    ...
    
    if (LINUX_KERNEL_VERSION > KERNEL_VERSION(5, 15, 0)) { ... }
    
    switch (CONFIG_BPF_PRELOAD) {
        case TRI_NO: ...; break;
        case TRI_YES: ...; break;
        case TRI_MODULE: ...; break;
    }
    
    if (!CONFIG_BPF_JIT_ALWAYS_ON)
        bpf_printk("BPF_JIT_DEFAULT_ON: %c\n", CONFIG_BPF_JIT_DEFAULT_ON ?: 'n');
    
    bpf_printk("HZ is %d, MODPROBE_PATH: %s\n", CONFIG_HZ, CONFIG_MODPROBE_PATH);

重定位枚举

  • 一些 BPF 应用程序遇到的一个有趣的挑战是需要与不稳定的 内部内核枚举一起工作
  • 也就是说,枚举没有一套固定的常量和/或整数值分配给它们。
  • 一个很好的例子是枚举 cgroup_subsys_id,定义在 include/linux/cgroup-defs.h 中,它的定义会根据内核编译时启用的 cgroup 特性而有所不同(详情见 include/linux/cgroup_subsys.h)。
  • 因此,如果你需要知道例如 cgroup_subsys_id::cpu_cgrp_id 的实际整数值,这可能是个大问题,因为这个枚举是内核内部的,是动态生成的。
  • 再一次,BPF CO-RE 来拯救。它允许在 bpf_core_enum_value() 宏的帮助下捕获实际值。
    int id = bpf_core_enum_value(enum cgroup_subsys_id, cpu_cgrp_id);
    /* id will contain the actual integer value in the host kernel */

守护可能失败的重新定位

  • 在某些内核上丢失一些字段是很正常的。如果一个 BPF 程序试图用 BPF_CORE_READ() 读取一个缺失的字段,它将在 BPF 验证期间导致一个错误。

  • 同样地,当获得在宿主内核中不存在的枚举器(或类型)的枚举值(或类型大小)时,CO-RE 重定位将失败。

  • 如果你遇到与下面类似的错误,要知道这是因为 CO-RE 重定位未能找到相应的字段/类型/枚举。

    1: (85) call unknown#195896080
    invalid func unknown#195896080
  • 导致 CO-RE 重定位失败的指令。libbpf 不立即报告此类错误的原因是,如果需要的话,缺失的字段/类型/枚举和相应的失败的 CO-RE 重定位能由BPF 应用程序优雅地处理

  • 这使得只用一个 BPF 应用程序就能适应内核类型的巨大变化(这是 "一次编译 - 到处运行"理念的一个重要目标)。

  • 当某些字段/类型/枚举有可能丢失时,你能用处理内核变化一节中描述的检查来保护这种代码路径。

  • 如果防护得当,BPF 验证器将知道这样的代码路径在那个特定的内核中是不可能被击中的,因此将把它作为死代码消除

  • 如果实际运行的内核确实有这些信息,这种方法允许选择性地捕捉内核信息的碎片。否则,BPF 应用程序能干净利落地退回到一个替代逻辑,并优雅地处理丢失的功能或数据。

  • 只要对可能失败的 CO-RE 重定位进行适当的保护,所有这些都很有效

  • 这里的CO-RE 重定位是指对 BPF_CORE_READ() 系列宏的任何使用,类型/字段大小的重定位,或枚举器值的捕获

  • 如果目标字段/类型/枚举不存在或者有一些不兼容的定义,那么任何东西都没有意义。

  • 继续前面 cpu_cgrp_id 枚举值的例子,为了处理可能没有定义这种枚举器的内核(例如,由于没有设置 CONFIG_CGROUP_PIDS Kconfig toggle),能使用 bpf_core_enum_value_exists() 检查

  • 存在性检查从不失败!,它返回 true/false(严格来说,在 C 中是 0 或 1)。
    ```c
    int id;

      if (bpf_core_enum_value_exists(enum cgroup_subsys_id, cpu_cgrp_id))
          id = bpf_core_enum_value(enum cgroup_subsys_id, cpu_cgrp_id);
      else
          id = -1; /* fallback value */
    
      /* use id even if cpu_cgrp_id isn't defined */
      ```
    
  • 上面的例子在任何内核上都能正常工作,无论 cpu_cgrp_id 枚举器是否存在,尽管 bpf_core_enum_value() 在没有 cpu_cgrp_id 枚举器的内核上失败。

  • 所有这些都是因为有适当的代码路径的保护。

#type/ebpf #libpf #type/linux #public

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant