TL;DR:本文会用相对比较啰嗦的篇幅来讲述完整的 BTF 数据格式,如果只是想浮光掠影地看一看 BTF 是什么或有什么用,只需要看前面几节内容即可。如果是想自己写写 BTF Parser,那么后面几节内容或许会有些帮助。本文重点参考了内核文档cilium/ebpf/internal/btf 的实现,具体探索了一下 .BTF 数据格式.BTF.ext.BTF 数据编码非常类似,后续有时间再聊聊)。

什么是 BTF

BTF(BPF Type Format)原本是用来描述 BPF prog 和 map 相关调试信息的元数据格式,后面 BTF 又进一步拓展成可描述 function info 和 line info,大大增强了 BTF 的能力。

那么 BTF 到底有什么用呢 ?举一个简单的例子。如果我们有这样一个结构体变量:

1
2
3
4
5
struct Foo {
    int a;
    float b;
    char c;
} foo;

当我们在代码中使用这个结构体并进行编译后,foo 最终呈现的其实只是一段非常原始的字节流,而使用 foo 内部成员的地方也相应地转换为对字节流 offset 的引用。换句话来说,Foo 结构体最开始用以描述其每个字段的类型、字段命名、类型名等信息在编译的过程中都被优化掉了,因为机器并不需要这部分信息也能很好的工作。但是,这部分信息对于人类的日常调试中则非常有用,而 BTF 正是用以描述这部分元数据的编码格式。通过 BTF,我们可以还原出原始的数据类型定义,比如我们可以还原出内核头文件(vmlinux.h),从而可以辅助我们更好地编译和使用依赖于内核头文件的代码。我们在调试程序的时候,调试器可利用 BTF 为我们提供更加丰富的调试信息(比如函数定义与具体的源代码行)。

BTF 为 Struct 和 Union 类型提供了对应成员的 offset 信息,并结合 Clang 的扩展(主要是 __builtin_preserve_access_index)和 BPF 加载器(比如 libbpf),BPF Prog 就可以准确访问某个 Struct 或者 Union 类型的成员,而不用担心重定位问题(即 CO-RE 特性)。这背后的逻辑其实就是:

  1. Clang 会根据扩展函数将对应结构体访问转化成可重定位信息(这部分 LLVM 逻辑可以参考:BPFAbstractMemberAccess.cpp#L761);

  2. Libbpf 在加载对应的 BPF Prog 时,会根据实际 Host 上内核 BTF 信息来修正 1 中的可重定位信息,从而在加载时期来完成重定位;

BTF 对标的是 DWARF。这是 ELF 文件中调试信息元数据格式的事实标准,同样也是一门相对比较古老的数据格式。较之 DWARF,BTF 采用了更加轻量和紧凑的编码方式,从而可得到比 DWARF 小非常多的元数据。由于 BTF 生成的元数据非常小,因此可随目标文件一起分发和加载,比如我们开启 CONFIG_DEBUG_INFO_BTF=y 之后 Linux 内核(>= 5.2)就可以携带 BTF 数据,从而我们就无需再额外下载内核头文件。

BTF 标准包含两个方面:

  1. BTF 内核 API

    Linux 内核为 BTF 提供一组基于 bpf(2) 系统调用的 API,从而可以让用户将 BTF 加载入内核。这些 API 是用户空间与内核空间的约定。

  2. BTF ELF 文件格式

    BTF 通常将作为 ELF 文件中的一个 Section,此时我们需要约定好 ELF 文件格式与 BPF 加载器(比如 libbpf)之间的数据格式。

BTF 整体的编码格式

完整的 BTF 数据可分为 BTF 部分BTF 扩展部分两部分数据。前者将会放在 ELF 文件的 .BTF Section 中,后者则会放在 BTF.ext Section 中(之后都将简称为 .BTF 数据和 .BTF.ext 数据)。

这两部分数据的格式基本上大同小异,其中:

  • .BTF 数据:存放 BTF 类型和字符串数据;
  • .BTF.ext 数据: 存放 func_infoline_info 数据,这部分数据需在加载到内核之前由 BPF 加载器处理;

.BTF 数据整体的编码格式可如下图所示:

.BTF 数据的开头是一个 24 bytes 大小的固定头部,头部之后则分别是类型编码(即上图的 type_data)和字符串编码string_data)。

BTF Header 如果用 Go 语言描述的话,可为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/* ebpf/internal/btf/btf.go */

type btfHeader struct {
    Magic   uint16
    Version uint8
    Flags   uint8
    HdrLen  uint32

    TypeOff   uint32
    TypeLen   uint32
    StringOff uint32
    StringLen uint32
}
  • Magic:总是 0xeb9f,且不同的大小端机器上会有不同的编码格式,这可以测试 BTF 所在系统是否为大小端系统;
  • Version:目前总是 1;
  • Flags:目前总是 0;
  • HdrLen:BTF Header 的长度,可认为 HdrLenbtfHeader 大小一致,即总是为 24;
  • TypeOff:BTF 类型编码部分相对于 BTF Header 之后的偏移量。如果 TypeOff 为 0,而 HdrLen 为 24,则 BTF 类型编码相对于文件开始的偏移量为 0+24,即为 24;
  • TypeLen:BTF 类型编码部分的数据长度;
  • StringOff:BTF 字符串编码部分相对于 BTF Header 之后的偏移量,与 TypeOff 类似;
  • StringLen:BTF 字符串编码部分的数据长度;

由上可见,BTF Header 是一个非常容易理解的格式,我们重点需要理解什么是类型数据(type_data字符串数据(string_data

所谓类型数据,就是将我们常见的数据类型,比如 整型、指针、数组、结构体、函数等的定义转换成 BTF 二进制格式,比如目前 BTF 支持以下这几种数据类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* ebpf/internal/btf/btf_types.go */

// btfKind describes a Type.
type btfKind uint8

// Equivalents of the BTF_KIND_* constants.
const (
    kindUnknown btfKind = iota // 0 预留给 void 类型
    kindInt // 1
    kindPointer
    kindArray
    kindStruct
    kindUnion
    kindEnum
    kindForward
    kindTypedef
    kindVolatile
    kindConst
    kindRestrict
    // Added ~4.20
    kindFunc
    kindFuncProto
    // Added ~5.1
    kindVar
    kindDatasec
    // Added ~5.13
    kindFloat
)

按照内核 C 语言的枚举可以定义为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#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  */
#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     */

BTF 字符串数据则是一组以 \x00 开头和 \x00 结尾的字符串数组(每个字符串均以 \x00 结尾)。这部分数据收集了我们代码中类型使用到的字符串(可以理解是一个 string table),比如结构体字段名、变量名等。BTF 类型数据将以偏移量的形式引用这部分数据

我们可以用如下代码来实现对 BTF 数据的解析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* ebpf/internal/btf/btf.go */

// parseBTF 将返回 BTF 类型数据和 BTF 字符串数据.
func parseBTF(btf io.Reader, bo binary.ByteOrder) ([]rawType, stringTable, error) {
    // 将 .BTF section 的数据读出来.
    rawBTF, err := io.ReadAll(btf)
    
    // ...
    
    // 创建一个 bytes.Reader 方便解析.
    rd := bytes.NewReader(rawBTF)
    
    // 读取和解析 BTF Header.
    var header btfHeader
    if err := binary.Read(rd, bo, &header); err != nil {
        // ...
    }
    
    if header.Magic != 0xeb9f {
        // ...
    }
    
    if header.Version != 1 {
        // ...
    }
    
    if header.Flags != 0 {
        // ...
    }
    
    // ...
    
    // 读取和读取 BTF 字符串数据.
    if _, err := rd.Seek(int64(header.HdrLen+header.StringOff), io.SeekStart); err != nil {
        // ...
    }
    // 读取对应长度的字符串数据,构建 string table.
    rawStrings, err := readStringTable(io.LimitReader(rd, int64(header.StringLen)))
    if err != nil {
        // ...
    }
    
    // 读取和读取 BTF 类型数据.
    if _, err := rd.Seek(int64(header.HdrLen+header.TypeOff), io.SeekStart); err != nil {
        // ...
    }
    // 读取对应长度的 BTF 类型数据,构建 BTF 类型.
    rawTypes, err := readTypes(io.LimitReader(rd, int64(header.TypeLen)), bo)
    if err != nil {
        // ...
    }
    
    return rawTypes, rawStrings, nil
}

BTF 数据的生成和测试

我们使用 LLVM(>=8.0)工具可生成 BTF 数据格式。

比如我们有如下测试代码(结构体变量必须定义出变量,否则将不会触发生成相应的 BTF 数据):

1
2
3
4
5
6
7
struct Foo {
    int a1;
    short a2;
    char a3;
} foo;

void bar(void) { }

我们可以直接用 clang 进行编译(必须指定为 BPF target):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ cat foo.c
struct Foo {
    int a1;
    short a2;
    char a3;
} foo;

void bar(void) { }

# -g: 指示编译器生成 debug 信息,即 BTF 数据
$ clang -c -g -O2 -target bpf foo.c

# 检查生成的 foo.o
$ file foo.o
foo.o: ELF 64-bit LSB relocatable, eBPF, version 1 (SYSV), with debug_info, not stripped

我们可以用 readelf 工具来查看 foo.o 的 ELF Section 信息:

1
2
3
4
5
$ readelf -S foo.o  | grep BTF
  [ 8] .BTF              PROGBITS         0000000000000000  000001c4
  [ 9] .rel.BTF          REL              0000000000000000  00000570
  [10] .BTF.ext          PROGBITS         0000000000000000  000002ca
  [11] .rel.BTF.ext      REL              0000000000000000  00000580

可以发现编译结果生成了 .BTF.BTF.ext 数据(ELF Section 的 PROGBITS 类型表示该 Section 包含的是代码、数据或者调试信息,我们暂时先忽略 REL Section)。

我们可以用 llvm-objdump 来 dump 具体的二进制数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ llvm-objdump -sj .BTF foo.o

foo.o:	file format elf64-bpf

Contents of section .BTF:
 0000 9feb0100 18000000 00000000 a0000000  ................
 0010 a0000000 4e000000 00000000 0000000d  ....N...........
 0020 00000000 01000000 0100000c 01000000  ................
 0030 29000000 03000004 08000000 2d000000  )...........-...
 0040 04000000 00000000 30000000 05000000  ........0.......
 0050 20000000 33000000 06000000 30000000   ...3.......0...
 0060 36000000 00000001 04000000 20000001  6........... ...
 0070 3a000000 00000001 02000000 10000001  :...............
 0080 40000000 00000001 01000000 08000001  @...............
 0090 45000000 0000000e 03000000 01000000  E...............
 00a0 49000000 0100000f 00000000 07000000  I...............
 00b0 00000000 08000000 00626172 002e7465  .........bar..te
 00c0 7874002f 746d702f 666f6f2e 6300766f  xt./tmp/foo.c.vo
 00d0 69642062 61722876 6f696429 207b207d  id bar(void) { }
 00e0 00466f6f 00613100 61320061 3300696e  .Foo.a1.a2.a3.in
 00f0 74007368 6f727400 63686172 00666f6f  t.short.char.foo
 0100 002e6273 7300                        ..bss.

我们可以用 bpftool 来查看 BTF 数据:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ bpftool btf dump file foo.o
[1] FUNC_PROTO '(anon)' ret_type_id=0 vlen=0
[2] FUNC 'bar' type_id=1 linkage=global
[3] STRUCT 'Foo' size=8 vlen=3
	'a1' type_id=4 bits_offset=0
	'a2' type_id=5 bits_offset=32
	'a3' type_id=6 bits_offset=48
[4] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED
[5] INT 'short' size=2 bits_offset=0 nr_bits=16 encoding=SIGNED
[6] INT 'char' size=1 bits_offset=0 nr_bits=8 encoding=SIGNED
[7] VAR 'foo' type_id=3, linkage=global-alloc
[8] DATASEC '.bss' size=0 vlen=1
	type_id=7 offset=0 size=8

或者用 clang 的 -S 选项生成反汇编代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ clang -S -g -O2 -target bpf foo.c

$ cat foo.s
...
.Linfo_string11:
	.asciz	"bar"                           # string offset=125
	.section	.BTF,"",@progbits
	.short	60319                           # 0xeb9f
	.byte	1
	.byte	0
	.long	24
	.long	0
	.long	160
	.long	160
	.long	78
	.long	0                               # BTF_KIND_FUNC_PROTO(id = 1)
	.long	218103808                       # 0xd000000
	.long	0
	.long	1                               # BTF_KIND_FUNC(id = 2)
	.long	201326593                       # 0xc000001
	.long	1
	.long	41                              # BTF_KIND_STRUCT(id = 3)
	...
	.byte	0
	.ascii	"int"                           # string offset=54
	.byte	0
	.ascii	"short"                         # string offset=58
	.byte	0
	.ascii	"char"                          # string offset=64
	.byte	0
	.ascii	"foo"                           # string offset=69
	.byte	0
	.ascii	".bss"                          # string offset=73
	.byte	0
...

我们可从汇编代码观察到对应 BTF 数据的反汇编代码。

BTF 字符串数据

由于 BTF 字符串数据格式最为简单,我们先来介绍这部分数据格式。BTF 字符串数据可理解为以下数据:

1
00 <string1> 00 <string2> 00 <string3> ... 00

这部分数据必须要以 \x00 开头和结尾,而这中间则是字符串(包括字符串结尾的 \x00)数组

我们可以观察 readStringTable() 的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* ebpf/internal/btf/strings.go */

// stringTable 是 byte 数组.
type stringTable []byte

func readStringTable(r io.Reader) (stringTable, error) {
    // 将 BTF 字符串数据全部读取出来.
    contents, err := io.ReadAll(r)
    if err != nil {
        // ...
    }
    
    // 必须要有数据
    if len(contents) < 1 {
        // ...
    }
    
    // 开头必须是 '\x00'
    if contents[0] != '\x00' {
        // ...
    }
    
    // 结尾必须是 '\x00'
    if contents[len(contents)-1] != '\x00' {
        // ...
    }
    
    return stringTable(contents), nil
}

BTF 类型数据将以 offset 的形式引用 stringTable,比如我们有下数据:

1
['\x00', 'i', 'n', 't', '\x00', 'f', 'o', 'o', '\x00', 'h', 'e', 'l', 'l', 'o', '\x00']

则几个典型的 offset 为:

  • offset = 1:得到 int
  • offset = 5:得到 foo
  • offset = 9:得到 hello

如下 Lookup() 函数,即输入一个 offset,将返回对应的字符串:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* ebpf/internal/btf/strings.go */

func (st stringTable) Lookup(offset uint32) (string, error) {
    // ...
    
    pos := int(offset)
    
    // 错误情况:超出 stringTable 的界限.
    if pos >= len(st) {
        // ...
    }
    
    // 错误情况:offset 的前一个不是 '\x00'.
    if pos > 0 && st[pos-1] != '\x00' {
        // ...
    }
    
    // 以 pos 为开始索引.
    str := st[pos:]
    // end 将是 pos 为开始的第一个 '\x00' 的索引位置,即当前字符串的结束字符.
    end := bytes.IndexByte(str, '\x00')
    // 错误情况:没有以 '\x00' 结尾的字符串
    if end == -1 {
        // ...
    }
    
    // 返回对应字符(去掉了结尾的 '\x00').
    return string(str[:end]), nil
}

BTF 类型数据

BTF 类型数据头部

类比于 BTF 字符串数据是一组字符串数组,而 BTF 类型数据则是一组 BTF 类型的数组。每一个 BTF 类型数据我们可以理解为如下格式:

如果用 Go 来表达可为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* ebpf/internal/btf/btf_types.go */

// BTF 类型数据的头部.
type btfType struct {
    NameOff uint32
    Info uint32
    SizeType uint32
}

// 一个最基本的 BTF 类型数据格式,包含一个头部和可变数据.
type rawType struct {
    btfType
    data interface{}
}

如上图所示:

  • name_off:执行 BTF 字符串数据的 offset,如上文所说,BTF 字符串数据以索引的形式被 BTF 类型数据引用;

  • info:描述当前类型的类型元数据,以 bits 模式编码,如下所示:

    如上所述,info 其实包含了 vlen / kind / kind_flag 3 个可用字段,其中 kind 表示的是当前数据类型

  • size_type:在 C 语言中,这个字段其实是一个枚举类型,根据不同的数据类型来决定表示的是 size 还是 type,比如:

    • 如果数据类型是 INT / ENUM / STRUCT / UNION,则该字段是 size,用以表达类型的长度;
    • 如果数据类型是 PTR / TYPEDEF / VOLATILE / CONST / RESTRICT / FUNC / FUNC_PROTO,则该字段是 type,即下文中将提及的 type_id

按照 BTF 类型的数组顺序,从 1 开始,依次为每个 BTF 类型数据赋予一个 type_id,用以标识当前段的 BTF 类型。

综上,我们可以用如下逻辑来完成对 BTF 类型数据的解析:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* ebpf/internal/btf/btf_types.go */

// readTypes 从 BTF 类型数据中解析出基本类型数组.
func readTypes(r io.Reader, bo binary.ByteOrder) ([]rawType, error) {
    var (
    	header btfType
        types  []rawType
    )
    
    // type_id 从 1 开始,依次为每一个 rawType 赋予 type_id.
    // 一直遍历读取,直到遇到 io.EOF 结束.
    for id := TypeID(1); ; id++ {
        // 读取 btf type 头部.
        if err := binary.Read(r, bo, &header); err == io.EOF {
            return types, nil
        } else if err != nil {
            // ...
        }
        
        // 因为不同类型的数据对象有不同的数据定义,此处用 interface{}.
        var data interface{}
        
        // 从头部的 info 字段读取当前数据类型,根据不同的数据定义执行不同的读取逻辑,即定义不同的数据对象.
        switch header.Kind() {
        case kindInt:
            data = new(uint32)
        case kindPointer: // 无需 data
        case kindArray:
            data = new(btfArray)
        // ...
        }
        
        // 即 data 可为空的类型.
        if data == nil {
            types = append(types, rawType{header, nil})
            continue
        }
        
        // 读取头部之后的 data.
        if err := binary.Read(r, bo, data); err != nil {
            // ...
        }
        
        types = append(types, rawType{header, data})
    }
}

BTF_KIND_INT 类型

此时 BTF 类型头部字段的取值为:

  • name_off:任何有效的字符串数据索引;
  • info.kind_flag:此时为 0;
  • info.kind:此时为 BTF_KIND_INT
  • info.vlen:此时为 0;
  • size:具体整型数据大小(bytes),比如 int 类型为 4,short 类型为 2,char 类型为 1,等等;

BTF_KIND_INT 类型之后的 data 是一个 4 个 bytes 的数据(u32),其中的编码格式如下所示:

  • bits:表示该整型所对应的准确的二进制位数。btf_type.size * 8 必须大于或等于 bits(一般是等于 bits);
  • offset:一般总是为 0;
  • encoding:该字段提供了如下的额外信息:
    • 1(即 1 << 0):有符号整型;
    • 2(即 1 << 1):char 整型;
    • 4(即 1 << 2):bool 整型;

BTF_KIND_PTR 类型

此时 BTF 类型头部字段的取值为:

  • name_off:此时为 0;
  • info.kind_flag:此时为 0;
  • info.kind:此时为 BTF_KIND_PTR
  • info.vlen:此时为 0;
  • type:指针类型的 type_id。指针的类型也会被当成一个 BTF 类型,所以也具有一个 type_id

该类型之后没有数据

BTF_KIND_ARRAY 类型

此时 BTF 类型头部字段的取值为:

  • name_off:此时为 0;
  • info.kind_flag:此时为 0;
  • info.kind:此时为 BTF_KIND_ARRAY
  • info.vlen:此时为 0;
  • size/type:此时为 0,没有使用;

BTF_KIND_ARRAY 类型之后的 data 是一个 btfArray 结构体,btfArray 类型用 Go 可表示为:

1
2
3
4
5
6
7
/* ebpf/internal/btf/btf_types.go */

type btfArray struct {
    Type      TypeID
    IndexType TypeID
    Nelems    uint32
}
  • Type:元素类型的 type_id
  • IndexType:数组索引类型的 type_id
  • nelems:数组个数(可以为 0);

BTF_KIND_STRUCT / BTF_KIND_UNION 类型

这两个类型将使用一样的数据结构。

此时 BTF 类型头部字段的取值为:

  • name_off:0 或者是一个有效 C 语言标识的有效字符串数据 offset;
  • info.kind_flag:0 或者 1;
  • info.kindBTF_KIND_STRUCT / BTF_KIND_UNION
  • info.vlen:struct 或者 union 结构的成员个数;
  • size:struct 或者 union 结构的大小(bytes);

该类型后续将跟着类型为 btfMember 数组,个数为 info.vlen。即每一个 btfMember 表示 struct 或者 union 的一个成员。

btfMember 类型用 Go 可表示为:

1
2
3
4
5
6
7
/* ebpf/internal/btf/btf_types.go */

type btfMember struct {
    NameOff uint32
    Type    TypeID
    Offset  uint32
}
  • NameOff:指向 BTF 字符串数据的索引,用以标识成员名;
  • Type:成员类型的 type_id
  • Offset:如果 kind_flag 为 0,则该字段对应成员的 offset(以 bit 为单位);如果 kind_flag 为 1,则该字段包含位域大小和偏移:低 24 位为 offset,高 8 位为位域大小(bitfield size);

比如我们有这么一个结构体变量(没有使用位域):

1
2
3
4
5
struct Foo {
    int a;
    char b;
    short c;
} foo;

则我们将得到如下 BTF 信息(由 bpftool 输出,内容有所省略):

1
2
3
4
5
6
7
8
[1] STRUCT 'Foo' size=8 vlen=3
	'a' type_id=2 bits_offset=0
	'b' type_id=3 bits_offset=32
	'c' type_id=4 bits_offset=48
[2] INT 'int' size=4 bits_offset=0 nr_bits=32 encoding=SIGNED
[3] INT 'char' size=1 bits_offset=0 nr_bits=8 encoding=SIGNED
[4] INT 'short' size=2 bits_offset=0 nr_bits=16 encoding=SIGNED
...

每个 BTF 类型前面的编号即为 type_id

其中 type_id 为 1 则对应 Foo 结构体,其中有 3 个 btfMember,其中:

  • a:对应的 type_id 为 2,即 int 类型,其中 offset 为 0(即第一个字段);
  • b:对应的 type_id 为 3,即 char 类型,其中 offset 为 32(因为 a 字段为 32 bits 大小,则紧接着 a 字段的 b 的 offset 就为 32);
  • c:对应的 type_id 为 4,即 short 类型,其中 offset 为 48(同理,因为前面的 ab 字段分别为 32 bits 和 16 bits,那么 c 字段的 offset 则为 32 + 16 = 48);

BTF_KIND_ENUM 类型

此时 BTF 类型头部字段的取值为:

  • name_off:0 或者是一个有效 C 语言标识的有效字符串数据 offset;
  • info.kind_flag:此时为 0;
  • info.kindBTF_KIND_ENUM
  • info.vlen:枚举值的个数;
  • size:此时为 4;

该类型后续将跟着类型为 btfEnum 数组,个数为 info.vlenbtfEnum 类型用 Go 可表示为:

1
2
3
4
5
6
/* ebpf/internal/btf/btf_types.go */

type btfEnum struct {
    NameOff uint32
    Val     int32
}
  • NameOff:指向 BTF 字符串数据的索引,用以标识枚举名;
  • Val:具体枚举值;

BTF_KIND_FWD 类型

此时 BTF 类型头部字段的取值为:

  • name_off:任何有效的字符串数据索引;
  • info.kind_flag:如果是 struct 为 0,是 union 则为 1;
  • info.kindBTF_KIND_FWD
  • info.vlen:此时为 0;
  • type:此时为 0;

该类型之后没有数据

BTF_KIND_TYPEDEF 类型

此时 BTF 类型头部字段的取值为:

  • name_off:任何有效的字符串数据索引;
  • info.kind_flag:此时为 0;
  • info.kindBTF_KIND_TYPEDEF
  • info.vlen:此时为 0;
  • typetypedef 所定义的原始类型的 type_id

该类型之后没有数据

BTF_KIND_VOLATILE 类型

此时 BTF 类型头部字段的取值为:

  • name_off:此时为 0;
  • info.kind_flag:此时为 0;
  • info.kindBTF_KIND_VOLATILE
  • info.vlen:此时为 0;
  • type:带有 volatile 限定符的类型的 type_id

该类型之后没有数据

BTF_KIND_CONST 类型

此时 BTF 类型头部字段的取值为:

  • name_off:此时为 0;
  • info.kind_flag:此时为 0;
  • info.kindBTF_KIND_CONST
  • info.vlen:此时为 0;
  • type:带有 const 限定符的类型的 type_id

该类型之后没有数据

BTF_KIND_RESTRICT 类型

此时 BTF 类型头部字段的取值为:

  • name_off:此时为 0;
  • info.kind_flag:此时为 0;
  • info.kindBTF_KIND_RESTRICT
  • info.vlen:此时为 0;
  • type:带有 restrict 限定符的类型的 type_id

该类型之后没有数据

BTF_KIND_FUNC 类型

此时 BTF 类型头部字段的取值为:

  • name_off:任何有效的字符串数据索引;
  • info.kind_flag:此时为 0;
  • info.kindBTF_KIND_FUNC
  • info.vlen:此时为 0;
  • type:相应 BTF_KIND_FUNC_PROTO 类型对应的 type_id

该类型之后没有数据

BTF_KIND_FUNC_PROTO 类型

此时 BTF 类型头部字段的取值为:

  • name_off:此时为 0;
  • info.kind_flag:此时为 0;
  • info.kindBTF_KIND_FUNC_PROTO
  • info.vlen:参数的数量;
  • type:返回类型的 type_id

BTF_KIND_FUNC_PROTO 是表示一个函数的签名信息

该类型后续将跟着类型为 btfParam 数组,个数为 info.vlenbtfParam 类型用 Go 可表示为:

1
2
3
4
type btfParam struct {
    NameOff uint32
    Type    TypeID
}
  • NameOff:指向 BTF 字符串数据的索引,用以标识参数名;
  • Type:对应参数类型的 type_id

如果函数有可变参数,则最后一个参数的这两个字段均为 0

BTF_KIND_VAR 类型

此时 BTF 类型头部字段的取值为:

  • name_off:任何有效的字符串数据索引;
  • info.kind_flag:此时为 0;
  • info.kindBTF_KIND_FUNC_VAR
  • info.vlen:此时为 0;
  • type:对应变量的类型;

该类型后续将跟着类型为 btfVariable 类型的数据。btfVariable 类型用 Go 可表示为:

1
2
3
type btfVariable struct {
    Linkage uint32
}
  • Linkage:目前只有静态变量设置为 0,而全局分配的变量则设置为 1;

BTF_KIND_DATASEC 类型

此时 BTF 类型头部字段的取值为:

  • name_off:对应数据段 ELF Section 名的字符串数据索引,比如 .data.bss.rodata 等。如果有多个这样的数据段,则对应有多个 BTF_KIND_DATASEC 类型;
  • info.kind_flag:此时为 0;
  • info.kindBTF_KIND_DATASEC
  • info.vlen:变量的数量;
  • size:该段的总的大小(以 bytes 为单位)。该字段编译的时候为 0,由 BPF 加载器(比如 libbpf)来修订为实际大小;

BTF_KIND_DATASEC 表示是全局变量的数据

该类型后续将跟着类型为 btfVarSecinfo 数组,个数为 info.vlenbtfVarSecinfo 类型用 Go 可表示为:

1
2
3
4
5
type btfVarSecinfo struct {
    Type   TypeID
    Offset uint32
    Size   uint32
}
  • Type:对应数据类型的 type_id
  • Offset:段内偏移量(基本是 0);
  • Size:对应数据的大小(以 bytes 为单位);

BTF 与 Linux 内核

BTF 与 Linux 内核间的 API 同样是用了 bpf(2) 这个系统调用(不同的参数)。根据具体的作用不同,我们可以分为以下几种:

  • BPF_BTF_LOAD

    将 BTF 数据加载入内核,加载成功将返回 btf_id。用户层后续将通过 btf_id 来获取 BTF 数据;

  • BPF_MAP_CREATE

    可利用 btf_id 和 key/value 的 type id 来创建 BPF Map:

    1
    2
    3
    
    __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 */
    
  • BPF_PROG_LOAD

    我们加载 BPF Prog,可将 .BTF.ext 的信息也一同加载。

  • BPF_{PROG,MAP}_GET_NEXT_ID

    在内核中,Prog / Map / BTF 都有其唯一的 ID,这些 ID 在其对象对应的生命周期内保持不变。我们可以利用该调用遍历出所有的 ID(最开始可以选择从 0 开始)。

  • BPF_{PROG,MAP}_GET_FD_BY_ID

    我们无法通过 ID 获取到 Prog 或者 Map 更多的元数据信息,此时必须通过 fd。该调用就是利用 ID 换一个 fd。

  • BPF_OBJ_GET_INFO_BY_FD

    一旦获取到了 fd,我们就可以利用这个获取 Prog 或者 Map 更多的元数据,而这些元数据也带有相应的 BTF 信息。通过这些 BTF 信息,我们可以得到更丰富的类型数据。

  • BPF_BTF_GET_FD_BY_ID

    一旦我们获取到 BTF ID,我们可以利用该调用获取 BTF fd。然后再通过 BPF_OBJ_GET_INFO_BY_FD 我们可以得到用 BPF_BTF_LOAD 加载的最原始的 BTF Blob 数据,从而可获得所有的 BTF 上下文信息。

参考文档

  1. Enhancing the Linux kernel with BTF type information
  2. BTF deduplication and Linux kernel BTF
  3. BPF Type Format (BTF)
  4. BPF BTF 详解