BPF 类型格式 (BTF)

1. 简介

BTF(BPF 类型格式)是一种元数据格式,用于编码与 BPF 程序/映射相关的调试信息。BTF 这个名称最初用于描述数据类型。后来 BTF 被扩展为包含已定义子例程的函数信息,以及用于源代码/行信息的行信息。

调试信息用于映射美化打印、函数签名等。函数签名能够更好地识别 bpf 程序/函数内核符号。行信息有助于生成带有源代码注释的翻译后的字节码、JIT 代码和验证器日志。

BTF 规范包含两个部分:
  • BTF 内核 API

  • BTF ELF 文件格式

内核 API 是用户空间和内核之间的约定。内核在使用 BTF 信息之前会对其进行验证。ELF 文件格式是 ELF 文件和 libbpf 加载器之间的用户空间约定。

类型和字符串部分是 BTF 内核 API 的一部分,描述 bpf 程序引用的调试信息(主要与类型相关)。这两部分在2. BTF 类型和字符串编码中详细讨论。

2. BTF 类型和字符串编码

文件 include/uapi/linux/btf.h 提供了类型/字符串编码方式的高级定义。

数据 blob 的开头必须是

struct btf_header {
    __u16   magic;
    __u8    version;
    __u8    flags;
    __u32   hdr_len;

    /* All offsets are in bytes relative to the end of this header */
    __u32   type_off;       /* offset of type section       */
    __u32   type_len;       /* length of type section       */
    __u32   str_off;        /* offset of string section     */
    __u32   str_len;        /* length of string section     */
};

magic 是 0xeB9F,它对于大端和小端系统具有不同的编码,可用于测试 BTF 是为大端还是小端目标生成的。btf_header 被设计为可扩展的,当生成数据 blob 时,hdr_len 等于 sizeof(struct btf_header)

2.1 字符串编码

字符串部分中的第一个字符串必须是空字符串。其余的字符串表是其他以 null 结尾的字符串的串联。

2.2 类型编码

类型 id 0 保留给 void 类型。类型部分按顺序解析,类型 id 从 id 1 开始分配给每个识别的类型。目前,支持以下类型

#define BTF_KIND_INT            1       /* Integer      */
#define BTF_KIND_PTR            2       /* Pointer      */
#define BTF_KIND_ARRAY          3       /* Array        */
#define BTF_KIND_STRUCT         4       /* Struct       */
#define BTF_KIND_UNION          5       /* Union        */
#define BTF_KIND_ENUM           6       /* Enumeration up to 32-bit values */
#define BTF_KIND_FWD            7       /* Forward      */
#define BTF_KIND_TYPEDEF        8       /* Typedef      */
#define BTF_KIND_VOLATILE       9       /* Volatile     */
#define BTF_KIND_CONST          10      /* Const        */
#define BTF_KIND_RESTRICT       11      /* Restrict     */
#define BTF_KIND_FUNC           12      /* Function     */
#define BTF_KIND_FUNC_PROTO     13      /* Function Proto       */
#define BTF_KIND_VAR            14      /* Variable     */
#define BTF_KIND_DATASEC        15      /* Section      */
#define BTF_KIND_FLOAT          16      /* Floating point       */
#define BTF_KIND_DECL_TAG       17      /* Decl Tag     */
#define BTF_KIND_TYPE_TAG       18      /* Type Tag     */
#define BTF_KIND_ENUM64         19      /* Enumeration up to 64-bit values */

请注意,类型部分编码的是调试信息,而不仅仅是纯类型。BTF_KIND_FUNC 不是类型,它表示已定义的子程序。

每个类型包含以下公共数据

struct btf_type {
    __u32 name_off;
    /* "info" bits arrangement
     * bits  0-15: vlen (e.g. # of struct's members)
     * bits 16-23: unused
     * bits 24-28: kind (e.g. int, ptr, array...etc)
     * bits 29-30: unused
     * bit     31: kind_flag, currently used by
     *             struct, union, fwd, enum and enum64.
     */
    __u32 info;
    /* "size" is used by INT, ENUM, STRUCT, UNION and ENUM64.
     * "size" tells the size of the type it is describing.
     *
     * "type" is used by PTR, TYPEDEF, VOLATILE, CONST, RESTRICT,
     * FUNC, FUNC_PROTO, DECL_TAG and TYPE_TAG.
     * "type" is a type_id referring to another type.
     */
    union {
            __u32 size;
            __u32 type;
    };
};

对于某些类型,公共数据之后是特定于类型的数据。struct btf_type 中的 name_off 指定字符串表中的偏移量。以下各节详细介绍了每种类型的编码。

2.2.1 BTF_KIND_INT

struct btf_type 编码要求
  • name_off:任何有效偏移量

  • info.kind_flag: 0

  • info.kind:BTF_KIND_INT

  • info.vlen: 0

  • size:int 类型的字节大小。

btf_type 之后是一个 u32,具有以下位排列

#define BTF_INT_ENCODING(VAL)   (((VAL) & 0x0f000000) >> 24)
#define BTF_INT_OFFSET(VAL)     (((VAL) & 0x00ff0000) >> 16)
#define BTF_INT_BITS(VAL)       ((VAL)  & 0x000000ff)

BTF_INT_ENCODING 具有以下属性

#define BTF_INT_SIGNED  (1 << 0)
#define BTF_INT_CHAR    (1 << 1)
#define BTF_INT_BOOL    (1 << 2)

BTF_INT_ENCODING() 提供额外信息:int 类型的有符号性、char 或 bool。char 和 bool 编码主要用于美化打印。对于 int 类型,最多可以指定一种编码。

BTF_INT_BITS() 指定此 int 类型实际保留的位数。例如,一个 4 位位域编码 BTF_INT_BITS() 等于 4。对于该类型,btf_type.size * 8 必须等于或大于 BTF_INT_BITS()BTF_INT_BITS() 的最大值为 128。

BTF_INT_OFFSET() 指定起始位偏移量,用于计算此 int 的值。例如,位域结构成员具有

  • btf 成员从结构开始的位偏移量为 100,

  • btf 成员指向一个 int 类型,

  • int 类型具有 BTF_INT_OFFSET() = 2BTF_INT_BITS() = 4

那么在结构内存布局中,此成员将占用从位 100 + 2 = 102 开始的 4 位。

或者,位域结构成员可以是以下内容,以访问与上述相同的位

  • btf 成员位偏移量为 102,

  • btf 成员指向一个 int 类型,

  • int 类型具有 BTF_INT_OFFSET() = 0BTF_INT_BITS() = 4

BTF_INT_OFFSET() 的最初目的是提供位域编码的灵活性。目前,llvm 和 pahole 都为所有 int 类型生成 BTF_INT_OFFSET() = 0

2.2.2 BTF_KIND_PTR

struct btf_type 编码要求
  • name_off: 0

  • info.kind_flag: 0

  • info.kind:BTF_KIND_PTR

  • info.vlen: 0

  • type:指针的被指向类型

btf_type 之后没有其他类型数据。

2.2.3 BTF_KIND_ARRAY

struct btf_type 编码要求
  • name_off: 0

  • info.kind_flag: 0

  • info.kind:BTF_KIND_ARRAY

  • info.vlen: 0

  • size/type:0,未使用

btf_type 之后是一个 struct btf_array

struct btf_array {
    __u32   type;
    __u32   index_type;
    __u32   nelems;
};
struct btf_array 编码
  • type:元素类型

  • index_type:索引类型

  • nelems:此数组的元素数量(也允许 0)。

index_type 可以是任何常规的 int 类型(u8u16u32u64unsigned __int128)。包含 index_type 的原始设计遵循 DWARF,DWARF 的数组类型具有 index_type。目前,在 BTF 中,除了类型验证之外,不使用 index_type

struct btf_array 允许通过元素类型链接来表示多维数组。例如,对于 int a[5][6],以下类型信息说明了链接

  • [1]:int

  • [2]:数组,btf_array.type = [1]btf_array.nelems = 6

  • [3]:数组,btf_array.type = [2]btf_array.nelems = 5

目前,pahole 和 llvm 都将多维数组折叠为一维数组,例如,对于 a[5][6]btf_array.nelems 等于 30。这是因为最初的用例是映射美化打印,其中整个数组被转储出来,因此一维数组就足够了。随着更多 BTF 用法的探索,可以更改 pahole 和 llvm 以生成多维数组的正确链接表示。

2.2.4 BTF_KIND_STRUCT

2.2.5 BTF_KIND_UNION

struct btf_type 编码要求
  • name_off: 0 或指向有效 C 标识符的偏移量

  • info.kind_flag: 0 或 1

  • info.kind: BTF_KIND_STRUCT 或 BTF_KIND_UNION

  • info.vlen: 结构体/联合体成员的数量

  • info.size: 结构体/联合体的大小(以字节为单位)

btf_type 之后跟随 info.vlenstruct btf_member

struct btf_member {
    __u32   name_off;
    __u32   type;
    __u32   offset;
};
struct btf_member 编码
  • name_off: 指向有效 C 标识符的偏移量

  • type: 成员类型

  • offset: <见下文>

如果类型信息 kind_flag 未设置,则偏移量仅包含成员的位偏移量。请注意,位域的基本类型只能是 int 或枚举类型。如果位域大小为 32,则基本类型可以是 int 或枚举类型。如果位域大小不是 32,则基本类型必须是 int,并且 int 类型 BTF_INT_BITS() 编码位域大小。

如果设置了 kind_flag,则 btf_member.offset 包含成员位域大小和位偏移量。位域大小和位偏移量的计算如下。

#define BTF_MEMBER_BITFIELD_SIZE(val)   ((val) >> 24)
#define BTF_MEMBER_BIT_OFFSET(val)      ((val) & 0xffffff)

在这种情况下,如果基本类型是 int 类型,则它必须是常规 int 类型

  • BTF_INT_OFFSET() 必须为 0。

  • BTF_INT_BITS() 必须等于 {1,2,4,8,16} * 8

Commit 9d5f9f701b18 引入了 kind_flag 并解释了为什么存在两种模式。

2.2.6 BTF_KIND_ENUM

struct btf_type 编码要求
  • name_off: 0 或指向有效 C 标识符的偏移量

  • info.kind_flag: 0 表示无符号,1 表示有符号

  • info.kind: BTF_KIND_ENUM

  • info.vlen: 枚举值的数量

  • 大小: 1/2/4/8

btf_type 之后跟随 info.vlenstruct btf_enum

struct btf_enum {
    __u32   name_off;
    __s32   val;
};
btf_enum 编码
  • name_off: 指向有效 C 标识符的偏移量

  • val: 任意值

如果原始枚举值是有符号的且大小小于 4,则该值将被符号扩展为 4 个字节。如果大小为 8,则该值将被截断为 4 个字节。

2.2.7 BTF_KIND_FWD

struct btf_type 编码要求
  • name_off: 指向有效 C 标识符的偏移量

  • info.kind_flag: 0 表示结构体,1 表示联合体

  • info.kind: BTF_KIND_FWD

  • info.vlen: 0

  • 类型: 0

btf_type 之后没有其他类型数据。

2.2.8 BTF_KIND_TYPEDEF

struct btf_type 编码要求
  • name_off: 指向有效 C 标识符的偏移量

  • info.kind_flag: 0

  • info.kind: BTF_KIND_TYPEDEF

  • info.vlen: 0

  • type: 可通过 name_off 的名称引用的类型

btf_type 之后没有其他类型数据。

2.2.9 BTF_KIND_VOLATILE

struct btf_type 编码要求
  • name_off: 0

  • info.kind_flag: 0

  • info.kind: BTF_KIND_VOLATILE

  • info.vlen: 0

  • type: 带有 volatile 限定符的类型

btf_type 之后没有其他类型数据。

2.2.10 BTF_KIND_CONST

struct btf_type 编码要求
  • name_off: 0

  • info.kind_flag: 0

  • info.kind: BTF_KIND_CONST

  • info.vlen: 0

  • type: 带有 const 限定符的类型

btf_type 之后没有其他类型数据。

2.2.11 BTF_KIND_RESTRICT

struct btf_type 编码要求
  • name_off: 0

  • info.kind_flag: 0

  • info.kind: BTF_KIND_RESTRICT

  • info.vlen: 0

  • type: 带有 restrict 限定符的类型

btf_type 之后没有其他类型数据。

2.2.12 BTF_KIND_FUNC

struct btf_type 编码要求
  • name_off: 指向有效 C 标识符的偏移量

  • info.kind_flag: 0

  • info.kind: BTF_KIND_FUNC

  • info.vlen: 链接信息 (BTF_FUNC_STATIC, BTF_FUNC_GLOBAL

    或 BTF_FUNC_EXTERN - 请参阅 2.3.1 函数链接常量值)

  • type: BTF_KIND_FUNC_PROTO 类型

btf_type 之后没有其他类型数据。

BTF_KIND_FUNC 定义的不是类型,而是子程序(函数),其签名由 type 定义。 因此,子程序是该类型的一个实例。BTF_KIND_FUNC 又可能被 4.2 .BTF.ext 部分 (ELF) 或 3.3 BPF_PROG_LOAD (ABI) 的参数中的 func_info 引用。

目前,内核中仅支持 BTF_FUNC_STATIC 和 BTF_FUNC_GLOBAL 的链接值。

2.2.13 BTF_KIND_FUNC_PROTO

struct btf_type 编码要求
  • name_off: 0

  • info.kind_flag: 0

  • info.kind: BTF_KIND_FUNC_PROTO

  • info.vlen: 参数数量

  • type: 返回类型

btf_type 之后跟随 info.vlenstruct btf_param

struct btf_param {
    __u32   name_off;
    __u32   type;
};

如果 BTF_KIND_FUNC_PROTO 类型被 BTF_KIND_FUNC 类型引用,则 btf_param.name_off 必须指向一个有效的 C 标识符,但表示可变参数的最后一个参数除外。 btf_param.type 指的是参数类型。

如果函数有可变参数,则使用 name_off = 0type = 0 编码最后一个参数。

2.2.14 BTF_KIND_VAR

struct btf_type 编码要求
  • name_off: 指向有效 C 标识符的偏移量

  • info.kind_flag: 0

  • info.kind: BTF_KIND_VAR

  • info.vlen: 0

  • type: 变量的类型

btf_type 之后跟随一个带有以下数据的 struct btf_variable

struct btf_var {
    __u32   linkage;
};

btf_var.linkage 可能取值:BTF_VAR_STATIC、BTF_VAR_GLOBAL_ALLOCATED 或 BTF_VAR_GLOBAL_EXTERN - 请参阅 2.3.2 变量链接常量值

目前 LLVM 并非支持所有类型的全局变量。 当前可用的如下:

  • 带有或不带有段属性的静态变量

  • 带有段属性的全局变量

后者用于将来从映射定义中提取映射键/值类型 ID。

2.2.15 BTF_KIND_DATASEC

struct btf_type 编码要求
  • name_off: 指向与变量关联的有效名称的偏移量或

    .data/.bss/.rodata 之一

  • info.kind_flag: 0

  • info.kind: BTF_KIND_DATASEC

  • info.vlen: 变量数量

  • size: 总段大小(以字节为单位)(编译时为 0,由 libbpf 等 BPF 加载器修补为实际大小)

    实际大小由 BPF 加载器(如 libbpf)修补

btf_type 之后跟随 info.vlenstruct btf_var_secinfo

struct btf_var_secinfo {
    __u32   type;
    __u32   offset;
    __u32   size;
};
struct btf_var_secinfo 编码
  • type: BTF_KIND_VAR 变量的类型

  • offset: 变量的段内偏移量

  • size: 变量的大小(以字节为单位)

2.2.16 BTF_KIND_FLOAT

struct btf_type 编码要求
  • name_off:任何有效偏移量

  • info.kind_flag: 0

  • info.kind: BTF_KIND_FLOAT

  • info.vlen: 0

  • size: float 类型的大小(以字节为单位):2、4、8、12 或 16。

btf_type 之后没有其他类型数据。

2.2.17 BTF_KIND_DECL_TAG

struct btf_type 编码要求
  • name_off: 指向非空字符串的偏移量

  • info.kind_flag: 0

  • info.kind: BTF_KIND_DECL_TAG

  • info.vlen: 0

  • type: struct, union, func, vartypedef

btf_type 之后是 struct btf_decl_tag

struct btf_decl_tag {
    __u32   component_idx;
};

name_off 编码 btf_decl_tag 属性字符串。type 应该为 structunionfuncvartypedef。对于 vartypedef 类型,btf_decl_tag.component_idx 必须为 -1。对于其他三种类型,如果 btf_decl_tag 属性应用于 structunionfunc 本身,则 btf_decl_tag.component_idx 必须为 -1。否则,该属性应用于 struct/union 成员或 func 参数,并且 btf_decl_tag.component_idx 应该是一个有效的索引(从 0 开始),指向成员或参数。

2.2.18 BTF_KIND_TYPE_TAG

struct btf_type 编码要求
  • name_off: 指向非空字符串的偏移量

  • info.kind_flag: 0

  • info.kind: BTF_KIND_TYPE_TAG

  • info.vlen: 0

  • type: 具有 btf_type_tag 属性的类型

目前,BTF_KIND_TYPE_TAG 仅针对指针类型发出。它具有以下 btf 类型链

ptr -> [type_tag]*
    -> [const | volatile | restrict | typedef]*
    -> base_type

基本上,指针类型指向零个或多个 type_tag,然后是零个或多个 const/volatile/restrict/typedef,最后是基本类型。基本类型是 int、ptr、array、struct、union、enum、func_proto 和 float 类型之一。

2.2.19 BTF_KIND_ENUM64

struct btf_type 编码要求
  • name_off: 0 或指向有效 C 标识符的偏移量

  • info.kind_flag: 0 表示无符号,1 表示有符号

  • info.kind: BTF_KIND_ENUM64

  • info.vlen: 枚举值的数量

  • 大小: 1/2/4/8

btf_type 之后是 info.vlenstruct btf_enum64

struct btf_enum64 {
    __u32   name_off;
    __u32   val_lo32;
    __u32   val_hi32;
};
btf_enum64 编码
  • name_off: 指向有效 C 标识符的偏移量

  • val_lo32:64 位值的低 32 位值

  • val_hi32:64 位值的高 32 位值

如果原始枚举值是有符号的且大小小于 8,则该值将被符号扩展到 8 个字节。

2.3 常量值

2.3.1 函数链接常量值

函数链接值和含义

种类

描述

BTF_FUNC_STATIC

0x0

子程序定义在包含编译单元外部不可见

BTF_FUNC_GLOBAL

0x1

子程序定义在包含编译单元外部可见

BTF_FUNC_EXTERN

0x2

子程序的声明,其定义在包含编译单元外部

2.3.2 变量链接常量值

变量链接值和含义

种类

描述

BTF_VAR_STATIC

0x0

全局变量定义在包含编译单元外部不可见

BTF_VAR_GLOBAL_ALLOCATED

0x1

全局变量定义在包含编译单元外部可见

BTF_VAR_GLOBAL_EXTERN

0x2

全局变量的声明,其定义在包含编译单元外部

3. BTF 内核 API

以下 bpf 系统调用命令涉及 BTF
  • BPF_BTF_LOAD:将 BTF 数据 blob 加载到内核中

  • BPF_MAP_CREATE:使用 btf 键和值类型信息创建映射。

  • BPF_PROG_LOAD:使用 btf 函数和行信息加载程序。

  • BPF_BTF_GET_FD_BY_ID:获取 btf fd

  • BPF_OBJ_GET_INFO_BY_FD:返回 btf、func_info、line_info 和其他 btf 相关信息。

工作流程通常如下所示

Application:
    BPF_BTF_LOAD
        |
        v
    BPF_MAP_CREATE and BPF_PROG_LOAD
        |
        V
    ......

Introspection tool:
    ......
    BPF_{PROG,MAP}_GET_NEXT_ID (get prog/map id's)
        |
        V
    BPF_{PROG,MAP}_GET_FD_BY_ID (get a prog/map fd)
        |
        V
    BPF_OBJ_GET_INFO_BY_FD (get bpf_prog_info/bpf_map_info with btf_id)
        |                                     |
        V                                     |
    BPF_BTF_GET_FD_BY_ID (get btf_fd)         |
        |                                     |
        V                                     |
    BPF_OBJ_GET_INFO_BY_FD (get btf)          |
        |                                     |
        V                                     V
    pretty print types, dump func signatures and line info, etc.

3.1 BPF_BTF_LOAD

将 BTF 数据 blob 加载到内核中。可以直接将2. BTF 类型和字符串编码中描述的数据 blob 加载到内核中。btf_fd 将返回给用户空间。

3.2 BPF_MAP_CREATE

可以使用 btf_fd 和指定的键/值类型 ID 创建映射。

__u32   btf_fd;         /* fd pointing to a BTF type data */
__u32   btf_key_type_id;        /* BTF type_id of the key */
__u32   btf_value_type_id;      /* BTF type_id of the value */

在 libbpf 中,可以使用如下额外注释定义映射

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __type(key, int);
    __type(value, struct ipv_counts);
    __uint(max_entries, 4);
} btf_map SEC(".maps");

在 ELF 解析期间,libbpf 能够提取键/值 type_id 并自动将其分配给 BPF_MAP_CREATE 属性。

3.3 BPF_PROG_LOAD

在 prog_load 期间,可以将 func_info 和 line_info 传递给内核,并为以下属性设置适当的值

__u32           insn_cnt;
__aligned_u64   insns;
......
__u32           prog_btf_fd;    /* fd pointing to BTF type data */
__u32           func_info_rec_size;     /* userspace bpf_func_info size */
__aligned_u64   func_info;      /* func info */
__u32           func_info_cnt;  /* number of bpf_func_info records */
__u32           line_info_rec_size;     /* userspace bpf_line_info size */
__aligned_u64   line_info;      /* line info */
__u32           line_info_cnt;  /* number of bpf_line_info records */

func_info 和 line_info 分别是以下数组。

struct bpf_func_info {
    __u32   insn_off; /* [0, insn_cnt - 1] */
    __u32   type_id;  /* pointing to a BTF_KIND_FUNC type */
};
struct bpf_line_info {
    __u32   insn_off; /* [0, insn_cnt - 1] */
    __u32   file_name_off; /* offset to string table for the filename */
    __u32   line_off; /* offset to string table for the source line */
    __u32   line_col; /* line number and column number */
};

func_info_rec_size 是每个 func_info 记录的大小,line_info_rec_size 是每个 line_info 记录的大小。将记录大小传递给内核,使其可以在未来扩展记录本身。

以下是 func_info 的要求
  • func_info[0].insn_off 必须为 0。

  • func_info insn_off 按严格递增顺序排列,并与 bpf 函数边界匹配。

以下是 line_info 的要求
  • 每个函数中的第一个 insn 必须有一个指向它的 line_info 记录。

  • line_info insn_off 按严格递增顺序排列。

对于 line_info,行号和列号定义如下

#define BPF_LINE_INFO_LINE_NUM(line_col)        ((line_col) >> 10)
#define BPF_LINE_INFO_LINE_COL(line_col)        ((line_col) & 0x3ff)

3.4 BPF_{PROG,MAP}_GET_NEXT_ID

在内核中,每个加载的程序、映射或 btf 都有一个唯一的 ID。ID 在程序、映射或 btf 的生命周期内不会更改。

bpf 系统调用命令 BPF_{PROG,MAP}_GET_NEXT_ID 为用户空间返回所有 ID,每个命令一个 ID,分别用于 bpf 程序或映射,因此检查工具可以检查所有程序和映射。

3.5 BPF_{PROG,MAP}_GET_FD_BY_ID

内省工具不能使用 ID 来获取有关程序或映射的详细信息。需要首先获取文件描述符,以便进行引用计数。

3.6 BPF_OBJ_GET_INFO_BY_FD

获取程序/映射 fd 后,内省工具可以从内核获取有关此 fd 的详细信息,其中一些与 BTF 相关。例如,bpf_map_info 返回 btf_id 和键/值类型 ID。bpf_prog_info 返回 btf_id、func_info 和已翻译 bpf 字节码的行信息以及 jited_line_info。

3.7 BPF_BTF_GET_FD_BY_ID

使用在 bpf_map_infobpf_prog_info 中获得的 btf_id,bpf 系统调用命令 BPF_BTF_GET_FD_BY_ID 可以检索 btf fd。然后,使用命令 BPF_OBJ_GET_INFO_BY_FD,可以检索最初使用 BPF_BTF_LOAD 加载到内核中的 btf blob。

有了 btf blob、bpf_map_infobpf_prog_info,内省工具就可以获得完整的 btf 知识,并能够漂亮地打印映射键/值,转储函数签名和行信息以及字节/jit 代码。

4. ELF 文件格式接口

4.1 .BTF section

.BTF section 包含类型和字符串数据。此 section 的格式与 2. BTF 类型和字符串编码 中描述的格式相同。

4.2 .BTF.ext section

.BTF.ext section 编码 func_info、line_info 和 CO-RE 重定位,在加载到内核之前需要加载器操作。

.BTF.ext section 的规范在 tools/lib/bpf/btf.htools/lib/bpf/btf.c 中定义。

.BTF.ext section 的当前标头

struct btf_ext_header {
    __u16   magic;
    __u8    version;
    __u8    flags;
    __u32   hdr_len;

    /* All offsets are in bytes relative to the end of this header */
    __u32   func_info_off;
    __u32   func_info_len;
    __u32   line_info_off;
    __u32   line_info_len;

    /* optional part of .BTF.ext header */
    __u32   core_relo_off;
    __u32   core_relo_len;
};

它与 .BTF section 非常相似。它不包含类型/字符串 section,而是包含 func_info、line_info 和 core_relo 子 section。有关 func_info 和 line_info 记录格式的详细信息,请参见 3.3 BPF_PROG_LOAD

func_info 的组织如下。

func_info_rec_size              /* __u32 value */
btf_ext_info_sec for section #1 /* func_info for section #1 */
btf_ext_info_sec for section #2 /* func_info for section #2 */
...

func_info_rec_size 指定生成 .BTF.ext 时 bpf_func_info 结构的大小。下面定义的 btf_ext_info_sec 是每个特定 ELF section 的 func_info 集合。

struct btf_ext_info_sec {
   __u32   sec_name_off; /* offset to section name */
   __u32   num_info;
   /* Followed by num_info * record_size number of bytes */
   __u8    data[0];
};

在这里,num_info 必须大于 0。

line_info 的组织如下。

line_info_rec_size              /* __u32 value */
btf_ext_info_sec for section #1 /* line_info for section #1 */
btf_ext_info_sec for section #2 /* line_info for section #2 */
...

line_info_rec_size 指定生成 .BTF.ext 时 bpf_line_info 结构的大小。

内核 API 和 ELF API 之间对 bpf_func_info->insn_offbpf_line_info->insn_off 的解释不同。对于内核 API,insn_off 是以 struct bpf_insn 为单位的指令偏移量。对于 ELF API,insn_off 是从 section 开头(btf_ext_info_sec->sec_name_off)开始的字节偏移量。

core_relo 的组织如下。

core_relo_rec_size              /* __u32 value */
btf_ext_info_sec for section #1 /* core_relo for section #1 */
btf_ext_info_sec for section #2 /* core_relo for section #2 */

core_relo_rec_size 指定生成 .BTF.ext 时 bpf_core_relo 结构的大小。单个 btf_ext_info_sec 中的所有 bpf_core_relo 结构描述应用于由 btf_ext_info_sec->sec_name_off 命名的节的重定位。

有关 CO-RE 重定位的更多信息,请参阅 Documentation/bpf/llvm_reloc.rst

4.3 .BTF_ids 节

.BTF_ids 节编码内核中使用的 BTF ID 值。

此节是在内核编译期间,借助 include/linux/btf_ids.h 头文件中定义的宏创建的。内核代码可以使用它们来创建 BTF ID 值的列表和集合(排序列表)。

BTF_ID_LISTBTF_ID 宏定义了 BTF ID 值的无序列表,语法如下:

BTF_ID_LIST(list)
BTF_ID(type1, name1)
BTF_ID(type2, name2)

在 .BTF_ids 节中产生以下布局:

__BTF_ID__type1__name1__1:
.zero 4
__BTF_ID__type2__name2__2:
.zero 4

定义 u32 list[]; 变量来访问列表。

BTF_ID_UNUSED 宏定义 4 个零字节。当我们想在 BTF_ID_LIST 中定义未使用的条目时使用,例如:

BTF_ID_LIST(bpf_skb_output_btf_ids)
BTF_ID(struct, sk_buff)
BTF_ID_UNUSED
BTF_ID(struct, task_struct)

BTF_SET_START/END 宏对定义了 BTF ID 值的排序列表及其计数,语法如下:

BTF_SET_START(set)
BTF_ID(type1, name1)
BTF_ID(type2, name2)
BTF_SET_END(set)

在 .BTF_ids 节中产生以下布局:

__BTF_ID__set__set:
.zero 4
__BTF_ID__type1__name1__3:
.zero 4
__BTF_ID__type2__name2__4:
.zero 4

定义 struct btf_id_set set; 变量来访问列表。

typeX 名称可以是以下之一:

struct, union, typedef, func

在解析 BTF ID 值时用作过滤器。

所有 BTF ID 列表和集合都在 .BTF_ids 节中编译,并在内核构建的链接阶段由 resolve_btfids 工具解析。

4.4 .BTF.base 节

拆分 BTF - 其中 .BTF 节仅包含关联的 base .BTF 节中没有的类型 - 是一种极其高效的方式来编码内核模块的类型信息,因为它们通常由一些特定于模块的类型和一大组共享的内核类型组成。前者在拆分 BTF 中编码,而后者在 base BTF 中编码,从而产生更紧凑的表示。拆分 BTF 中引用 base BTF 中类型的类型使用其 base BTF ID 引用它,并且拆分 BTF ID 从 last_base_BTF_ID + 1 开始。

然而,这种方法的缺点是,这使得拆分 BTF 有点脆弱 - 当 base BTF 更改时,base BTF ID 引用不再有效,并且拆分 BTF 本身变得无用。.BTF.base 节的作用是使拆分 BTF 在 base BTF 可能更改的情况下(例如,对于并非每次都构建内核的内核模块),更具弹性。.BTF.base 包含命名的 base 类型:INT、FLOAT、STRUCT、UNION、ENUM[64] 和 FWD。INT 和 FLOAT 在 .BTF.base 节中完全描述,而像结构体和联合体这样的复合类型则没有完全定义 - .BTF.base 类型仅用作拆分 BTF 引用的类型的描述,因此结构体/联合体在 .BTF.base 节中具有 0 个成员。ENUM[64] 也以 0 个成员记录。任何其他类型都添加到拆分 BTF 中。然后,此提炼过程为我们留下了一个具有这些基本类型的最少描述的 .BTF.base 节,以及一个引用这些基本类型的 .BTF 拆分节。稍后,我们可以使用存储在 .BTF.base 节和新的 .BTF base 中的信息来重定位拆分 BTF;.BTF.base 节中的类型信息允许我们更新拆分 BTF 引用,以指向相应的新 base BTF ID。

当内核模块具有 .BTF.base 节时,BTF 重定位发生在内核模块加载时,libbpf 还提供了一个 btf__relocate() API 来完成此操作。

例如,考虑以下 base BTF:

[1] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED
[2] STRUCT 'foo' size=8 vlen=2
        'f1' type_id=1 bits_offset=0
        'f2' type_id=1 bits_offset=32

...以及关联的拆分 BTF:

[3] PTR '(anon)' type_id=2

即,拆分 BTF 描述一个指向 struct foo { int f1; int f2; } 的指针;

.BTF.base 将由以下组成:

[1] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED
[2] STRUCT 'foo' size=8 vlen=0

如果我们稍后使用以下新的 base BTF 重定位拆分 BTF:

[1] INT 'long unsigned int' size=8 bits_offset=0 nr_bits=64 encoding=(none)
[2] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED
[3] STRUCT 'foo' size=8 vlen=2
        'f1' type_id=2 bits_offset=0
        'f2' type_id=2 bits_offset=32

...我们可以使用我们的 .BTF.base 描述来了解拆分 BTF 引用的是 struct foo,并且重定位会产生新的拆分 BTF:

[4] PTR '(anon)' type_id=3

请注意,我们必须更新拆分 BTF 的 BTF ID 和起始 BTF ID。

因此,我们看到 .BTF.base 如何发挥促进后续重定位的作用,从而产生更具弹性的拆分 BTF。

.BTF.base 节将为树外内核模块构建自动生成 - 即,设置了 KBUILD_EXTMOD 的情况(例如“make M=path/2/mod”的情况)。.BTF.base 生成需要 pahole 支持“distilled_base”BTF 功能;这在 pahole v1.28 及更高版本中可用。

5. 使用 BTF

5.1 bpftool 映射漂亮打印

使用 BTF,可以基于字段而不是简单的原始字节来打印映射键/值。这对于大型结构或数据结构具有位字段的情况尤其有价值。例如,对于以下映射:

enum A { A1, A2, A3, A4, A5 };
typedef enum A ___A;
struct tmp_t {
     char a1:4;
     int  a2:4;
     int  :4;
     __u32 a3:4;
     int b;
     ___A b1:4;
     enum A b2:4;
};
struct {
     __uint(type, BPF_MAP_TYPE_ARRAY);
     __type(key, int);
     __type(value, struct tmp_t);
     __uint(max_entries, 1);
} tmpmap SEC(".maps");

bpftool 能够像下面这样进行漂亮打印:

[{
      "key": 0,
      "value": {
          "a1": 0x2,
          "a2": 0x4,
          "a3": 0x6,
          "b": 7,
          "b1": 0x8,
          "b2": 0xa
      }
  }
]

5.2 bpftool prog dump

以下示例显示 func_info 和 line_info 如何帮助 prog dump 提供更好的内核符号名称、函数原型和行信息。

$ bpftool prog dump jited pinned /sys/fs/bpf/test_btf_haskv
[...]
int test_long_fname_2(struct dummy_tracepoint_args * arg):
bpf_prog_44a040bf25481309_test_long_fname_2:
; static int test_long_fname_2(struct dummy_tracepoint_args *arg)
   0:   push   %rbp
   1:   mov    %rsp,%rbp
   4:   sub    $0x30,%rsp
   b:   sub    $0x28,%rbp
   f:   mov    %rbx,0x0(%rbp)
  13:   mov    %r13,0x8(%rbp)
  17:   mov    %r14,0x10(%rbp)
  1b:   mov    %r15,0x18(%rbp)
  1f:   xor    %eax,%eax
  21:   mov    %rax,0x20(%rbp)
  25:   xor    %esi,%esi
; int key = 0;
  27:   mov    %esi,-0x4(%rbp)
; if (!arg->sock)
  2a:   mov    0x8(%rdi),%rdi
; if (!arg->sock)
  2e:   cmp    $0x0,%rdi
  32:   je     0x0000000000000070
  34:   mov    %rbp,%rsi
; counts = bpf_map_lookup_elem(&btf_map, &key);
[...]

5.3 验证器日志

以下示例显示 line_info 如何帮助调试验证失败。

   /* The code at tools/testing/selftests/bpf/test_xdp_noinline.c
    * is modified as below.
    */
   data = (void *)(long)xdp->data;
   data_end = (void *)(long)xdp->data_end;
   /*
   if (data + 4 > data_end)
           return XDP_DROP;
   */
   *(u32 *)data = dst->dst;

$ bpftool prog load ./test_xdp_noinline.o /sys/fs/bpf/test_xdp_noinline type xdp
    ; data = (void *)(long)xdp->data;
    224: (79) r2 = *(u64 *)(r10 -112)
    225: (61) r2 = *(u32 *)(r2 +0)
    ; *(u32 *)data = dst->dst;
    226: (63) *(u32 *)(r2 +0) = r1
    invalid access to packet, off=0 size=4, R2(id=0,off=0,r=0)
    R2 offset is outside of the packet

6. BTF 生成

您需要最新的 pahole:

或 llvm(8.0 或更高版本)。pahole 充当 dwarf2btf 转换器。它还不支持 .BTF.ext 和 btf BTF_KIND_FUNC 类型。例如:

-bash-4.4$ cat t.c
struct t {
  int a:2;
  int b:3;
  int c:2;
} g;
-bash-4.4$ gcc -c -O2 -g t.c
-bash-4.4$ pahole -JV t.o
File t.o:
[1] STRUCT t kind_flag=1 size=4 vlen=3
        a type_id=2 bitfield_size=2 bits_offset=0
        b type_id=2 bitfield_size=3 bits_offset=2
        c type_id=2 bitfield_size=2 bits_offset=5
[2] INT int size=4 bit_offset=0 nr_bits=32 encoding=SIGNED

llvm 能够直接为 bpf 目标生成 .BTF 和 .BTF.ext,带有 -g 选项。汇编代码 (-S) 能够以汇编格式显示 BTF 编码。

-bash-4.4$ cat t2.c
typedef int __int32;
struct t2 {
  int a2;
  int (*f2)(char q1, __int32 q2, ...);
  int (*f3)();
} g2;
int main() { return 0; }
int test() { return 0; }
-bash-4.4$ clang -c -g -O2 --target=bpf t2.c
-bash-4.4$ readelf -S t2.o
  ......
  [ 8] .BTF              PROGBITS         0000000000000000  00000247
       000000000000016e  0000000000000000           0     0     1
  [ 9] .BTF.ext          PROGBITS         0000000000000000  000003b5
       0000000000000060  0000000000000000           0     0     1
  [10] .rel.BTF.ext      REL              0000000000000000  000007e0
       0000000000000040  0000000000000010          16     9     8
  ......
-bash-4.4$ clang -S -g -O2 --target=bpf t2.c
-bash-4.4$ cat t2.s
  ......
        .section        .BTF,"",@progbits
        .short  60319                   # 0xeb9f
        .byte   1
        .byte   0
        .long   24
        .long   0
        .long   220
        .long   220
        .long   122
        .long   0                       # BTF_KIND_FUNC_PROTO(id = 1)
        .long   218103808               # 0xd000000
        .long   2
        .long   83                      # BTF_KIND_INT(id = 2)
        .long   16777216                # 0x1000000
        .long   4
        .long   16777248                # 0x1000020
  ......
        .byte   0                       # string offset=0
        .ascii  ".text"                 # string offset=1
        .byte   0
        .ascii  "/home/yhs/tmp-pahole/t2.c" # string offset=7
        .byte   0
        .ascii  "int main() { return 0; }" # string offset=33
        .byte   0
        .ascii  "int test() { return 0; }" # string offset=58
        .byte   0
        .ascii  "int"                   # string offset=83
  ......
        .section        .BTF.ext,"",@progbits
        .short  60319                   # 0xeb9f
        .byte   1
        .byte   0
        .long   24
        .long   0
        .long   28
        .long   28
        .long   44
        .long   8                       # FuncInfo
        .long   1                       # FuncInfo section string offset=1
        .long   2
        .long   .Lfunc_begin0
        .long   3
        .long   .Lfunc_begin1
        .long   5
        .long   16                      # LineInfo
        .long   1                       # LineInfo section string offset=1
        .long   2
        .long   .Ltmp0
        .long   7
        .long   33
        .long   7182                    # Line 7 Col 14
        .long   .Ltmp3
        .long   7
        .long   58
        .long   8206                    # Line 8 Col 14

7. 测试

内核 BPF 自测 tools/testing/selftests/bpf/prog_tests/btf.c 提供了一组广泛的 BTF 相关测试。