土法炼钢兴趣小组的算法知识备份

【eBPF 内核实现深度拆解】BPF 编译工具链:clang 后端、目标文件布局与调试信息

文章导航

分类入口
kernelebpf
标签入口
#ebpf#llvm#clang#bpf-backend#dwarf#btf#bpftool#elf#toolchain#vmlinux-h#inline-asm

目录

输入是 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 对应指令时。例如:

2.4 BPF 特定的 LLVM 优化 pass

BPF 后端实现了几个针对 BPF 约束的专用优化 pass:

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 优化级别:

对大多数 BPF 程序,-O2 是正确选择。-Os 在指令数紧张的复杂程序中有价值(如一个包含 50+ 个 map 和多个子程序的大型 Agent)。-O0 不应用于生产代码——生成的代码包含大量冗余 store/loadalloca,verifier 更难以推理。

3.3 -g 与调试信息

-g 让 clang 生成 DWARF 调试信息。对于 -target bpf,DWARF 节的典型大小(复杂 BPF 程序):

这些 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 缩减到 50K

3.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 符号表(可选)
 └─────────────────────────────────┘

四个核心的非程序节:

  1. license:许可字符串。没有此节(或许可与 GPL 不兼容)的程序,verifier 会拒绝其调用 GPL-only helper(如 bpf_probe_read_kernel()bpf_ktime_get_ns() 等)。值为 GPLDual BSD/GPL

  2. maps(旧语法):包含 struct bpf_map_def 数组。每个 map 定义为一个连续的结构体实例。libbpf 在 bpf_object__elf_collect() 中读取此节来创建 struct bpf_map 列表。从 5.13 开始,推荐使用 .maps 节的新 BTF 驱动的 map 定义语法(__uint(type, ...))。

  3. .BTF:BTF 类型信息(type entries + strings)。来源是 pahole 从 DWARF 转换的 BTF,或者 clang 直接生成 BTF(6.x+ clang 支持 BTF 作为 target extension)。

  4. .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 flags

4.4 Relocation sections

BPF .o 文件可能包含重定位节(.rel 开头)。两种 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 编码设计。转换流程:

  1. 读取 ELF 文件的所有 DWARF .debug_info CU(编译单元);
  2. 构建全局 DIE 树(所有 CU 的类型和子程序合并到一个命名空间);
  3. 遍历 BPF 程序的 .text 和其他程序节,收集实际使用的函数和结构体字段;
  4. 对被引用的类型执行深度优先遍历——从程序使用的结构体开始,递归编码所有成员类型,直到达到 leaf type(INT, PTR, FLOAT 等);
  5. 对每个类型按 struct btf_type 格式编码,构建类型数组和字符串表;
  6. 写入 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 包含所有内核类型定义,特点是:

  1. 所有结构体/联合体带有 __attribute__((preserve_access_index)):使得所有字段访问自动获得 CO-RE 保护;
  2. 只包含类型定义,不包含任何代码或宏:没有 #define、没有 static inline 函数、没有 DECLARE_PER_CPU 宏——这些在 BPF 编译环境中不可用;
  3. 类型按依赖关系排列:基础类型在前,复合类型在后,保证引用完整性;
  4. 大小约为 500K-2MB(取决于内核版本和配置)。

使用 vmlinux.h 的注意事项:

七、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 严格得多:

  1. 只能用 r 约束:表示寄存器。BPF 的 memory constraint (m) 支持有限——因为 BPF 指令集中的内存访问要求 reg + imm 格式。
  2. 只能操作寄存器操作数:BPF 指令集不允许内存到内存操作。
  3. 无浮点/向量寄存器:也没有对应的约束。
  4. Clobbers 限制:声明 clobber 寄存器后,编译器会在 asm 前后保存/恢复这些寄存器(取决于寄存器是否是 callee-saved)。
  5. volatile 关键字是必须的:非 volatile asm 可以被 LLVM 优化掉(dead code elimination)。声明 volatile 保证生成该指令。

7.3 用途

In-core BPF 程序中 inline asm 的主要用途:

八、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 的代码生成——四个工具构成一个从源码到加载的完整链条。下一篇文章进入调试和测试工具箱——如何在实际开发中拆解这个工具链各环节的故障。


参考

  1. LLVM 源码 llvm/lib/Target/BPF/:BPF 后端的 ISel、AsmPrinter、MCTargetDesc
  2. LLVM 源码 llvm/lib/Target/BPF/BPFISelLowering.cpp:BPF 指令合法化规则
  3. Linux 内核源码 tools/bpf/bpftool/(kernel 6.6):bpftool 工具链集成
  4. Linux 内核源码 scripts/link-vmlinux.sh:vmlinux BTF 生成的构建系统脚本
  5. IETF BPF ISA 草案(draft-ietf-bpf-isa):BPF 指令编码规范
  6. pahole(dwarves 包):DWARF 到 BTF 转换工具

上一篇CO-RE 重定位引擎(第 12 篇)

下一篇BPF 程序调试与测试(第 14 篇)

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-12 · kernel / ebpf

【eBPF 内核实现深度拆解】BTF 格式规范与内核类型系统

从 BTF 的二进制编码格式(btf_header + type entries + string table)出发,讲清 BTF 如何编码基本类型、结构体、联合体、函数原型与 typedef——BTF.ext 节的 func_info/line_info 记录,以及内核 pahole 的 BTF 生成与去重算法 btf_dedup。


By .