输入是 myprog.bpf.c——一份带有
SEC() 注解、bpf_map_def 声明和 BPF
helper 调用的 C 代码。输出是一个 ELF .o
文件,可以被 libbpf 解析、加载到内核、通过 JIT
编译为本机码。中间的转换管线是什么?哪些编译器选项真正影响输出?-target bpf
和 -target x86_64 的 LLVM
后端有什么根本区别?-g 生成的 DWARF 如何变成
.BTF 节?
本文拆解 BPF 编译工具链的四个环节:LLVM BPF 后端把 IR
翻译成 BPF 指令的机制、BPF ELF 目标文件的 section
布局承诺、DWARF-to-BTF 的转换管道、以及 bpftool
在工具链集成中的角色。源码基于 LLVM BPF
后端(llvm/lib/Target/BPF/)和 kernel 6.6 的
bpftool(tools/bpf/bpftool/)。
一、编译管道全景
flowchart LR
subgraph COMPILE["编译阶段"]
C["BPF C 源码<br/>myprog.bpf.c"] --> FE["Clang 前端<br/>C → LLVM IR"]
FE --> OPT["LLVM IR 优化<br/>-O2, -Os, -Oz"]
OPT --> ISEL["BPF 后端 ISel<br/>IR → BPF MCInst"]
ISEL --> ASM["BPF AsmPrinter<br/>MCInst → 机器码"]
ASM --> ELF["BPF ELF .o"]
end
subgraph POST["后处理阶段"]
ELF --> DWARF["DWARF 节(.debug_info, .debug_abbrev 等)"]
DWARF --> PAHOLE["pahole<br/>DWARF → BTF"]
PAHOLE --> BTF["BTF 节(.BTF, .BTF.ext)"]
end
subgraph TOOLS["工具阶段"]
BTF --> BPFTOOL["bpftool gen"]
BPFTOOL --> SKEL["skeleton .skel.h"]
BPFTOOL --> VMLINUX["vmlinux.h"]
end
一个典型的编译命令行:
# 最小编译
clang -target bpf -O2 -g -c myprog.bpf.c -o myprog.bpf.o
# 带 ISA 版本选择 + strip DWARF
clang -target bpf -O2 -g -mcpu=v3 -c myprog.bpf.c -o myprog.bpf.o
llvm-strip --strip-debug myprog.bpf.o # 剥离 DWARF,保留 BTF二、LLVM BPF 后端
2.1 后端的位置
BPF 后端是 LLVM 的一个正式目标,代码在
llvm/lib/Target/BPF/ 下。核心组件:
| 组件 | 路径 | 功能 |
|---|---|---|
BPFTargetMachine |
BPFTargetMachine.cpp |
目标机器描述:endian、ISA 版本、relocation model |
BPFISelLowering |
BPFISelLowering.cpp |
IR 指令到 BPF 指令的合法化与选择 |
BPFISelDAGToDAG |
BPFISelDAGToDAG.cpp |
指令选择(DAG 模式) |
BPFInstrInfo |
BPFInstrInfo.td |
BPF 指令集定义(TableGen DSL) |
BPFAsmPrinter |
BPFAsmPrinter.cpp |
输出 BPF 对象代码到 ELF |
BPFRegisterInfo |
BPFRegisterInfo.td |
11 个寄存器定义 |
2.2 Target triple
BPF 的目标三元组有三种变体:
clang -target bpf # 默认:little-endian (bpfel)
clang -target bpfel # 显式 little-endian
clang -target bpfeb # big-endian(极少使用,仅用于特定网络硬件)-target bpf 告诉 LLVM 使用 BPF
后端而非宿主机后端(x86、ARM
等)。这影响的不只是指令编码——所有 IR
降级规则、调用约定、类型大小、端序行为全部不同的。BPF 后端是
LLVM 中最精简的后端之一,因为 BPF
指令集规模小且约束清晰。
2.3 指令选择(Instruction Selection)
BPF 后端的 ISel 流程:
LLVM IR(通用,与宿主机无关)
→ DAG Combiners(优化 DAG)
→ BPFDAGToDAGISel::Select()(TR → BPF MCInst)
→ 验证(不能选到未定义的 BPF 指令)BPF 后端的 ISel 比 x86 简单很多——因为 BPF
指令集没有复杂的寻址模式(只有
BPF_REG + imm16),没有条件执行,没有可变长度指令。大部分
IR 指令直接到单条 BPF 指令的映射:
| LLVM IR 指令 | BPF 指令 |
|---|---|
add i64 %a, %b |
BPF_ALU64_REG(BPF_ADD, R_a, R_b) |
add i64 %a, 42 |
BPF_ALU64_IMM(BPF_ADD, R_a, 42)(立即数在范围
[-32768, 32767] 内) |
load i64, i64* %p |
BPF_LDX(BPF_DW, R_a, R_p, 0) |
store i64 %a, i64* %p |
BPF_STX(BPF_DW, R_p, R_a, 0) |
icmp eq i64 %a, %b |
BPF_JMP_REG(BPF_JEQ, R_a, R_b, +1) +
MOV |
br label %target |
BPF_JMP(BPF_JA, 0, 0, offset) |
call @helper |
BPF_CALL(helper_id) |
ret i64 %a |
BPF_EXIT + R0 已设置 |
复杂的情况出现在 LLVM IR 指令没有直接 BPF 对应指令时。例如:
- 64-bit 乘法:BPF ISA v1
没有乘法指令;v2 起提供 32-bit
BPF_MUL(BPF_ALU/BPF_ALU64类)。64-bit 乘法在 v4 之前通常展开为__muldi3库调用。 - 64-bit 除法/取模:v1–v3
无除法指令,展开为
__divdi3/__udivdi3库调用(链接时替换为内联序列或报错)。v4 增加了BPF_SDIV/BPF_SMOD(有符号除法/取模)。 - 大立即数(超过 16-bit):分解为
BPF_LD_IMM64(双宽指令)先加载常量到寄存器。 - 非对齐内存访问:BPF
指令集要求对齐访问,非对齐访问需要展开为多字节复制序列(
bpf_probe_read_kernel隐含处理非对齐)。
2.4 BPF 特定的 LLVM 优化 pass
BPF 后端实现了几个针对 BPF 约束的专用优化 pass:
BPFAdjustOpt(BPFAdjustOpt.cpp):调整 IR 以适应 BPF 约束,如将alloca转换为栈槽分配(BPF 有 512 字节栈帧限制)。BPFCheckAndAdjustIR:检查 IR 是否违反 BPF 限制(如函数参数数量超过 5、使用不支持的类型),拒绝或尝试修复。BPFAbstractMemberAccess:CO-RE 专用的 pass——识别__builtin_preserve_access_index()标记的访问,提取为 CO-RE 重定位记录。BPFMIPeephole:机器级窥孔优化——识别指令组合(如MOV + ADD = ADD)、消除冗余r0 = r0等。
2.5 BPF 内置函数(intrinsics)
LLVM BPF 后端提供了一套内置函数:
// BPF 内置函数(LLVM 内部调用,不直接暴露给用户)
llvm.bpf.passthrough(i32, i64) // 穿透标记,帮助 ISel 逻辑
llvm.bpf.compare(i64, i64, i32) // 比较并返回条件码
llvm.bpf.load.byte/dword(i8*, i32) // 类型化 load
llvm.bpf.preserve_field_info(i64, i64, i32) // CO-RE 字段信息查询llvm.bpf.preserve_field_info 是 CO-RE
的核心内置函数——它在编译时被
BPFAbstractMemberAccess pass 转换为实际的
bpf_core_relo 条目和占位符偏移量。用户态的
BPF_CORE_READ
宏最终翻译为对这个内置函数的调用。
attribute((preserve_access_index))
作用在类型级别——clang 将其视为”对此类型的所有成员访问都进行
CO-RE 保护”。它等价于在每次成员访问时隐式调用
__builtin_preserve_access_index()。
三、编译器 flags 及其影响
3.1 -mcpu:BPF ISA
版本
BPF ISA 已经历了四个版本(v1 到 v4),每个版本增加了新指令:
| ISA 版本 | 新增指令/能力 | 最小内核版本 | -mcpu 参数 |
|---|---|---|---|
| v1 | 基础指令集(ALU, JMP, LD, ST, CALL) | Linux 3.18 | -mcpu=v1(默认) |
| v2 | BPF_BSWAP(字节交换) |
Linux 3.18 | -mcpu=v2 |
| v3 | BPF_ATOMIC 系列(BPF_XADD,
BPF_CMPXCHG 等) |
Linux 5.6 | -mcpu=v3 |
| v4 | BPF_SDIV, BPF_SMOD,
BPF_MOVSX(有符号扩展 mov) |
Linux 6.7 | -mcpu=v4 |
版本选择影响代码生成质量。写 v3 的代码在
v1 的内核上运行时,verifier
会在加载阶段拒绝含有 BPF_ATOMIC
指令的程序。v4
的主要收益是有符号除法和有符号扩展——这些操作在旧版本中必须展开为代价高昂的内联序列。
3.2 -O2 vs
-Os vs -Oz
BPF 后端支持所有 LLVM 优化级别:
-O2(推荐):标准优化——常数折叠、死代码消除、内联小函数、循环旋转。生成代码在大小和速度之间平衡。-Os:优先减小代码大小。对 BPF 程序特别有意义——因为 verifier 的指令上限(默认 100 万条处理指令,6.x 大幅增加),但更重要的是 JIT 翻译后的代码在 CPU 的 instruction cache 中占用更少空间。-Oz:极致大小优化。可能出现 “code size shenanigans”(如用复杂指令序列替代简单指令来减少字节数),对 BPF 场景通常过度(BPF 指令固定 8 字节,没有变长压缩空间)。
对大多数 BPF 程序,-O2
是正确选择。-Os
在指令数紧张的复杂程序中有价值(如一个包含 50+ 个 map
和多个子程序的大型 Agent)。-O0
不应用于生产代码——生成的代码包含大量冗余
store/load 和 alloca,verifier
更难以推理。
3.3 -g 与调试信息
-g 让 clang 生成 DWARF 调试信息。对于
-target bpf,DWARF 节的典型大小(复杂 BPF
程序):
.debug_info:DWARF 信息主体(类型 DIE、子程序 DIE)——可能 50-200 KB.debug_abbrev:缩写表——数 KB.debug_str:调试字符串——10-50 KB.debug_line:源码行映射——10-30 KB.debug_frame:调用帧信息——少量 KB
这些 DWARF 节在最终 .o
文件中继续存在但不会被加载到内核(没有生产环境需要 BPF
程序的 DWARF)。pahole 读取 DWARF 节生成 BTF 后,可以用
llvm-strip --strip-debug 剥离 DWARF
减小文件:
clang -target bpf -O2 -g -c prog.bpf.c -o prog.bpf.o
# pahole 在 LLVM objcopy 步骤中转换为 BTF(见第四节)
llvm-strip --strip-debug prog.bpf.o # 文件从 200K 缩减到 50K3.4
-grecord-gcc-switches
此 flag 让 clang 在 ELF 中嵌入编译命令行的完整字符串(如
-target bpf -O2 -g -mcpu=v3)。这个信息存储在
.GCC.command.line 节中,对排查”为什么这个 .o
文件的行为与预期不同”有帮助——因为它记录了编译时
-mcpu 参数和确切优化级别。libbpf
不读这个节,它是人工调试用的。
四、BPF ELF 目标文件布局
一个 BPF .o
文件不是普通的中间目标文件——它是一份自描述的、包含程序代码和类型信息的具体化程序包。下面的
ELF section 有 BPF 特有的含义:
4.1 ELF section 布局
BPF ELF .o 文件布局:
┌─────────────────────────────────┐
│ ELF Header │
│ e_machine = EM_BPF (247) │ ← 指示这是一个 BPF 目标文件
├─────────────────────────────────┤
│ Section Headers │
├─────────────────────────────────┤
│ .text │ ← BPF 主程序代码(函数体)
│ kprobe/tcp_connect │ ← SEC("kprobe/tcp_connect") 生成的节
│ xdp │ ← SEC("xdp") 生成的节
│ tracepoint/syscalls/... │ ← SEC("tracepoint/...") 生成的节
│ ... 其他自定义程序节 ... │
├─────────────────────────────────┤
│ maps │ ← BPF map 定义(struct bpf_map_def)
│ .maps │ ← BPF map 元数据(5.13+ 推荐语法)
├─────────────────────────────────┤
│ license │ ← GPL 兼容性声明(如 "GPL", "Dual BSD/GPL")
├─────────────────────────────────┤
│ .BTF │ ← BTF 类型信息
│ .BTF.ext │ ← BTF 扩展(func_info + line_info + core_relo)
├─────────────────────────────────┤
│ .debug_info, .debug_line, ... │ ← DWARF 调试信息(可选,load 时不使用)
│ .symtab, .strtab │ ← ELF 符号表(可选)
└─────────────────────────────────┘
四个核心的非程序节:
license:许可字符串。没有此节(或许可与GPL不兼容)的程序,verifier 会拒绝其调用 GPL-only helper(如bpf_probe_read_kernel()、bpf_ktime_get_ns()等)。值为GPL或Dual BSD/GPL。maps(旧语法):包含struct bpf_map_def数组。每个 map 定义为一个连续的结构体实例。libbpf 在bpf_object__elf_collect()中读取此节来创建struct bpf_map列表。从 5.13 开始,推荐使用.maps节的新 BTF 驱动的 map 定义语法(__uint(type, ...))。.BTF:BTF 类型信息(type entries + strings)。来源是 pahole 从 DWARF 转换的 BTF,或者 clang 直接生成 BTF(6.x+ clang 支持BTF作为 target extension)。.BTF.ext:BTF 扩展信息——func_info(每个函数的 BTF func type)、line_info(每条指令的源码位置)和 core_relo_info(CO-RE 重定位条目)。libbpf 加载.BTF.ext来解析程序内调用的类型、提供 verifier 日志的源码位置、以及应用 CO-RE 修补。
4.2 SEC() 与 ELF section 的对应关系
第 10 篇详细讨论了 SEC() 注解到程序类型的映射。在 ELF
层面,SEC("xdp") 展开为:
__attribute__((section("xdp"), used))
int xdp_filter(struct xdp_md *ctx) { ... }clang 将此函数编译为独立的 ELF section 名为
xdp,节的
sh_type = SHT_PROGBITS,内容是 BPF
指令字节。libbpf 读取 ELF 节表时找到所有
SHT_PROGBITS 且名称匹配 BPF 程序节模式的
section,将其创建为 struct bpf_program。
4.3 ELF 文件标识
readelf 确认文件类型:
$ readelf -h myprog.bpf.o
# 示例经删减:
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
OS/ABI: UNIX - Linux
Machine: Linux BPF ← EM_BPF = 247
Flags: 0x0 ← 未设置 XDP metadata flags4.4 Relocation sections
BPF .o
文件可能包含重定位节(.rel 开头)。两种 CO-RE
重定位:
- 非 CO-RE 重定位(编译期确定):如
.relxdp,对xdpsection 中的BPF_LD_MAP_FD伪指令进行重定位。记录为R_BPF_64_64类型,符号索引指向 map 在mapssection 中的位置。libbpf 处理这些重定位——它用 map fd 替换 map 符号引用。 - CO-RE 重定位(加载期确定):不在 ELF
重定位节中,而编码在
.BTF.ext的core_relo子节中。原因是 CO-RE 重定位需要更多的上下文(访问路径、重定位种类)而不仅是简单的符号引用。
五、DWARF 到 BTF 的转换
5.1 转换链路
BPF 程序的 BTF 生成有两条路径:
路径 A:通过 pahole(传统,应用最广)
clang -target bpf -g -c prog.bpf.c
→ 生成 prog.bpf.o(含 DWARF)
→ pahole --btf_encode_detached prog.bpf.o
→ 读取 DWARF 节
→ 提取 BPF 程序引用的类型
→ 生成 BTF 和 BTF.ext(存入临时文件)
→ llvm-objcopy --add-section .BTF=tmp.btf prog.bpf.o
→ 将 BTF 嵌入 .BTF 节路径 B:通过 clang 直接生成 BTF(新型,6.x+ clang)
clang -target bpf -g -c prog.bpf.c
→ clang 直接在内存中生成 BTF(跳过完整的 DWARF → BTF 转换)路径 B 更快(避免了 pahole 的 DWARF 解析开销)且更精确(clang 知道哪些类型被 BPF 程序实际引用,可以生成更紧凑的 BTF)。但在较老 clang(< 15)中不成熟,路径 A 仍是多发行版的默认选择。
5.2 pahole 的内部处理
pahole 是一个独立的工具(dwarves 包),专为
DWARF 分析和 BTF 编码设计。转换流程:
- 读取 ELF 文件的所有 DWARF
.debug_infoCU(编译单元); - 构建全局 DIE 树(所有 CU 的类型和子程序合并到一个命名空间);
- 遍历 BPF 程序的
.text和其他程序节,收集实际使用的函数和结构体字段; - 对被引用的类型执行深度优先遍历——从程序使用的结构体开始,递归编码所有成员类型,直到达到
leaf type(
INT,PTR,FLOAT等); - 对每个类型按
struct btf_type格式编码,构建类型数组和字符串表; - 写入 BTF header + type section + string section。
pahole 会跳过没有被 BPF 程序引用的类型——这是 BTF 比 DWARF 小得多的原因之一。
六、vmlinux.h 的生成
vmlinux.h 是使用 CO-RE 的 BPF
程序的标准头文件,替代
<linux/sched.h>、<net/sock.h>
等内核头文件:
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h生成的 vmlinux.h
包含所有内核类型定义,特点是:
- 所有结构体/联合体带有
__attribute__((preserve_access_index)):使得所有字段访问自动获得 CO-RE 保护; - 只包含类型定义,不包含任何代码或宏:没有
#define、没有static inline函数、没有DECLARE_PER_CPU宏——这些在 BPF 编译环境中不可用; - 类型按依赖关系排列:基础类型在前,复合类型在后,保证引用完整性;
- 大小约为 500K-2MB(取决于内核版本和配置)。
使用 vmlinux.h 的注意事项:
- 不能与内核 UAPI 头文件混用——
vmlinux.h已经包含了内核类型,再 include<linux/types.h>会造成类型重定义; - 需要单独 include BPF
helpers:
#include <bpf/bpf_helpers.h>; - 需要声明自己的
SEC()、LICENSE等内容。
七、BPF inline assembly
7.1 语法
BPF 支持 LLVM 的内联汇编:
// 基础:将变量值移到寄存器
int val = 42;
asm volatile ("r1 = %0" :: "r"(val));
// 空操作:给 verifier 提示(某些 verifier 实现能理解这条 hint)
asm volatile ("" ::: "r0", "r1", "r2", "r3", "r4", "r5");
// 基于 LLVM BPF 命名的指令
asm volatile ("lock *(u64 *)(%0+0) += %1" : : "r"(ptr), "r"(val) : "memory");7.2 约束
BPF inline asm 的约束比 x86 严格得多:
- 只能用
r约束:表示寄存器。BPF 的 memory constraint (m) 支持有限——因为 BPF 指令集中的内存访问要求reg + imm格式。 - 只能操作寄存器操作数:BPF 指令集不允许内存到内存操作。
- 无浮点/向量寄存器:也没有对应的约束。
- Clobbers 限制:声明 clobber 寄存器后,编译器会在 asm 前后保存/恢复这些寄存器(取决于寄存器是否是 callee-saved)。
volatile关键字是必须的:非 volatile asm 可以被 LLVM 优化掉(dead code elimination)。声明 volatile 保证生成该指令。
7.3 用途
In-core BPF 程序中 inline asm 的主要用途:
bpf_spin_lock和bpf_atomic:这些指令在 C 层面没有直接的操作符,必须通过 inline asm 或 libbpf 提供的辅助宏(__sync_fetch_and_add,BPF_ATOMIC宏)生成。r1 = ctx的显式控制:在某些情况下,verifier 需要精确控制 R1-R5 的状态,而编译器可能重排参数传递顺序。inline asm 可以硬编码特定寄存器状态。- Verifier
黑科技:
asm volatile ("r1 = *(u32 *)(r1 + 0)")这类手工插入的指令用于需要绕过编译器优化来精确控制指令编码的场景。
八、bpftool 的工具链集成
bpftool(kernel 提供,在
tools/bpf/bpftool/ 下,但常以 distro pack
bpftool 包发行)是 BPF
工具链的瑞士军刀。编译工具链相关的子命令:
# 生成 skeleton(第 10 篇详述)
bpftool gen skeleton prog.bpf.o > prog.skel.h
# 生成最小的 CO-RE BTF(用于嵌入式部署)
bpftool gen min_core_btf prog.bpf.o min.btf
# 查看 BTF 内容
bpftool btf dump file prog.bpf.o format c # 输出 C 类型定义
bpftool btf dump file prog.bpf.o format raw # 输出原始 BTF 条目
bpftool btf dump id 1 # 查看已加载程序的 BTF
# 查看 prog 信息
bpftool prog show id 42
bpftool prog dump xlated id 42 # 反汇编 BPF 字节码
bpftool prog dump xlated id 42 linum # 带源码行的反汇编8.1
bpftool gen min_core_btf
min_core_btf
是部署场景中极其实用的功能。它分析 BPF 程序的 CO-RE
重定位,提取程序实际依赖的那部分类型,生成一个最小的 BTF
blob(通常几 KB),可以嵌入到应用程序中替代完整的 vmlinux
BTF(1-2 MB):
bpftool gen min_core_btf myprog.bpf.o myprog_min.btf
ls -lh myprog_min.btf # 通常是 5-50 KB这使得在嵌入式设备和容器(空间严格受限)中部署 CO-RE 兼容的 BPF 程序成为可能。
九、小结
BPF 编译工具链从 clang 的 -target bpf 切换到
LLVM 的 BPF 后端——一个将 LLVM IR 翻译为受严格约束的 64-bit
RISC BPF 指令的 ISel 管道。BPF ELF
目标文件提供了一份自描述、类型自包含的程序包——程序代码在命名
section 中,map 定义在 .maps section
中,类型信息在 .BTF 和 .BTF.ext
中。-g 生成的 DWARF 由 pahole 转换为 BTF 节嵌入
ELF,vmlinux.h 由 bpftool 从内核 BTF dump
生成,加上 skeleton
的代码生成——四个工具构成一个从源码到加载的完整链条。下一篇文章进入调试和测试工具箱——如何在实际开发中拆解这个工具链各环节的故障。
参考
- LLVM 源码
llvm/lib/Target/BPF/:BPF 后端的 ISel、AsmPrinter、MCTargetDesc - LLVM 源码
llvm/lib/Target/BPF/BPFISelLowering.cpp:BPF 指令合法化规则 - Linux 内核源码
tools/bpf/bpftool/(kernel 6.6):bpftool 工具链集成 - Linux 内核源码
scripts/link-vmlinux.sh:vmlinux BTF 生成的构建系统脚本 - IETF BPF ISA
草案(
draft-ietf-bpf-isa):BPF 指令编码规范 - pahole(
dwarves包):DWARF 到 BTF 转换工具
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【eBPF 内核实现深度拆解】libbpf 加载器工程:skeleton、auto-attach、map pinning 与 ring buffer 消费者
深入 libbpf 的加载生命周期:bpf_object__open() 的 ELF 解析、bpf_object__load() 的程序批量加载与 map 创建、map pinning 与跨进程复用、skeleton 自动生成器、SEC() 注解解析、auto-attach 的链路跟踪、ring_buffer__new() 的 mmap 消费者模式。
【eBPF 内核实现深度拆解】CO-RE 重定位引擎:libbpf 的运行时指令修补
从 clang 内置函数 __builtin_preserve_access_index 出发,追踪 BPF_CORE_READ 等宏如何生成 BTF.ext CO-RE 重定位记录,再到 libbpf 加载时 bpf_core_apply_relo() 根据目标内核 BTF 计算正确字段偏移量并修补 BPF 指令——可移植 BPF 的核心引擎。
【eBPF 内核实现深度拆解】BTF 格式规范与内核类型系统
从 BTF 的二进制编码格式(btf_header + type entries + string table)出发,讲清 BTF 如何编码基本类型、结构体、联合体、函数原型与 typedef——BTF.ext 节的 func_info/line_info 记录,以及内核 pahole 的 BTF 生成与去重算法 btf_dedup。
【eBPF 内核实现深度拆解】BPF 程序调试与测试:verifier log、bpftool、test runner 与内核自测
从 verifier log 的级别控制(log_level 1/2/自选寄存器)出发,覆盖 bpftool prog dump xlated/jited 的反汇编、bpftool map dump 运行时检查、bpftool btf 类型查阅、BPF selftests 结构与编写,以及生产环境下的 BPF 排障方法论。