惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Y
Y Combinator Blog
有赞技术团队
有赞技术团队
J
Java Code Geeks
H
Hackread – Cybersecurity News, Data Breaches, AI and More
美团技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Hugging Face - Blog
Hugging Face - Blog
人人都是产品经理
人人都是产品经理
酷 壳 – CoolShell
酷 壳 – CoolShell
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
C
Check Point Blog
博客园 - 【当耐特】
The GitHub Blog
The GitHub Blog
Recent Announcements
Recent Announcements
The Cloudflare Blog
Microsoft Azure Blog
Microsoft Azure Blog
腾讯CDC
Vercel News
Vercel News
IT之家
IT之家
MyScale Blog
MyScale Blog
博客园_首页
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
罗磊的独立博客

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

国密算法与国密 TLS 系列索引 【系统架构设计】架构质量属性:不只是"高可用高性能" 【系统架构设计百科】告警策略:如何避免"狼来了" 【系统架构设计】CQRS:读写分离的架构哲学 【系统架构设计】空间架构:极端扩展场景的解法 【系统架构设计】微服务架构深度审视:优势、代价与适用边界 【系统架构设计】扩展性原理:水平、垂直与对角扩展 【系统架构设计】无状态设计:扩展的第一步也是最难的一步 【系统架构设计】缓存架构:从本地到分布式的多级缓存体系 【系统架构设计】管道与过滤器:Unix 哲学的架构表达 【系统架构设计】复杂性管理:架构的核心战场 【系统架构设计】消息队列架构:异步解耦的设计与陷阱 【系统架构设计】CDN 架构:全球加速的设计原理 【系统架构设计】连接池设计:被忽视的性能杀手 【系统架构设计】弹性设计模式:熔断器、舱壁与超时 【系统架构设计】高可用设计模式:冗余、故障转移与仲裁 【系统架构设计】容量规划:从拍脑袋到数据驱动 【系统架构设计】数据库扩展:分库分表的工程实践与替代方案 【系统架构设计】SLO 工程:可靠性的量化管理 【系统架构设计】性能建模:用数学思维分析系统瓶颈 【系统架构设计】混沌工程:主动验证系统的韧性 【系统架构设计】零拷贝与内存映射:数据搬运的极致优化 【系统架构设计】线程模型:从 thread-per-request 到协程 【系统架构设计】容灾架构:多活与灾备设计 【系统架构设计】数据库性能模式:索引、查询与连接管理 【系统架构设计】数据建模:从关系范式到文档模型的真实权衡 【系统架构设计】吞吐量优化:批处理、流水线与并发模型 【系统架构设计】流处理架构:从批处理到实时的范式迁移 【系统架构设计】搜索引擎架构:倒排索引之上的系统设计 【系统架构设计】时序数据架构:监控与 IoT 的存储设计
【eBPF 内核实现深度拆解】BTF 格式规范与内核类型系统
Liao Tonglang · 2026-06-12 · via 土法炼钢兴趣小组的算法知识备份

如果你曾经在内核模块里用过 container_of(),就知道 C 代码依赖编译期的类型信息——offsetof(struct task_struct, pid) 在编译时就已经固定。BPF 程序在内核中运行时没有 libc、没有头文件、没有 <stddef.h>,但它需要读 struct task_struct 的某个字段、需要知道 struct socksk_stateunsigned char 还是 int、需要理解一个函数签名来判断 helper 调用的类型匹配。

这些信息都来自 BTF(BPF Type Format)——一种为 BPF 定制的二进制类型编码格式。它小到可以放在内核镜像里(vmlinux BTF 经 btf_dedup() 去重后通常在 1–2 MB 量级;下文涉及 vmlinux 体积的数字均来自典型 x86_64 构建且开启 CONFIG_DEBUG_INFO_BTF 时的经验值,具体大小因内核配置和架构而异),紧凑到可以快速解析(单遍扫描),表达能力足够覆盖 C 语言的类型系统(结构体、联合体、枚举、函数原型、typedef、类型修饰符)。

本文从 struct btf_header 的二进制布局开始,逐步拆解 20 种 BTF type kind 的编码方式,讲清 BTF.ext 的 func_info 和 line_info 如何连接指令偏移与类型信息,最后追踪内核构建流程中 pahole 的 DWARF-to-BTF 转换和 btf_dedup() 的去重算法。源码基于 kernel 6.6 的 include/uapi/linux/btf.htools/lib/bpf/btf.c

一、为什么 BTF 取代了 DWARF

DWARF(Debugging With Attributed Record Formats)是 C/C++ 编译器生成的标准调试信息格式。Linux 内核的 CONFIG_DEBUG_INFO=y 会在 vmlinux 中生成 DWARF 调试信息。但 DWARF 在 BPF 场景下有三个硬伤:

维度 DWARF BTF
大小 vmlinux DWARF 可达 ~120 MB(典型 x86_64 + CONFIG_DEBUG_INFO_BTF 构建) vmlinux BTF ~1–2 MB(去重后,同上构建条件)
解析复杂度 需要处理复杂的 DIE 树、跨 CU 引用、属性缩写 单一结构体数组,单遍线性扫描
内核内解析 不可行(内存占用 + 复杂度) 可行(kernel/bpf/btf.c 在加载时解析)
表示能力 C/C++ 所有特性 C 语言子集(足以覆盖内核类型系统)
自包含性 需要调试信息解析库(libdw/libdwarf) 自描述文件头,无需外部库

BPF 需要类型信息在内核中可用——verifier 通过 BTF 验证 fentry 的 callback 签名、bpftool map dump 通过 BTF 将 map value 人性化展示、CO-RE 重定位通过 BTF 跨内核版本计算字段偏移。这些场景都要求一个比 DWARF 小 100 倍、解析简单得多的格式。

BTF 的设计目标不是通用的调试格式,而是 BPF 程序的元数据格式。它只编码 BPF 程序需要的那部分类型信息:基本类型的大小和符号属性、复合类型的成员偏移、函数签名、类型名称映射。

BTF 在 ELF 文件中是一个独立的 section(.BTF),在内存中是一个连续字节块。整个格式由三个顺序部分构成:header、type section、string section。

graph TD
    subgraph BTF_FILE["BPF ELF .o 文件中的 .BTF 节"]
        HDR["struct btf_header<br/>magic: 0xeb9f<br/>version: 1<br/>hdr_len: 24<br/>type_off: 24<br/>type_len<br/>str_off<br/>str_len"]
        TYPES["Type Section<br/>struct btf_type 数组<br/>每个 12 字节<br/>可能后跟 variable-length data"]
        STRINGS["String Section<br/>\0 分隔的字符串表<br/>type entries 以 offset 引用"]
    end
    HDR --> TYPES --> STRINGS

struct btf_header 定义在 include/uapi/linux/btf.h,固定 24 字节(按 32-bit 对齐),出现在 BTF 数据块的最前部:

/* include/uapi/linux/btf.h (kernel 6.6) */
struct btf_header {
    __u16   magic;      /* 魔数:0xeb9f */
    __u8    version;    /* 版本:当前为 1 */
    __u8    flags;      /* 标志位,当前为 0 */
    __u32   hdr_len;    /* sizeof(struct btf_header) + 可变长度数据(若有) */
    __u32   type_off;   /* 类型节的起始偏移(从 BTF 数据起始算起) */
    __u32   type_len;   /* 类型节的字节长度 */
    __u32   str_off;    /* 字符串节的起始偏移 */
    __u32   str_len;    /* 字符串节的字节长度 */
};

关键语义:

  • magic = 0xeb9f:这个魔数的选择来自 Alexei Starovoitov 提交时的 arbitrary 值,区分于其他数据格式。
  • hdr_len >= 24:当 BTF 新增 header 字段时为大于 24 的值,老解析器按 hdr_len 跳过自己不认识的部分保持前向兼容。
  • type_off = hdr_len:当前实现中类型节紧接 header 之后。
  • str_off = type_off + type_len:字符串节紧接类型节之后。

2.2 btf_type

每个类型以 struct btf_type 12 字节的固定头部开始,后面可能跟随变长数据(成员列表、枚举值列表、参数列表等):

/* include/uapi/linux/btf.h (kernel 6.6) */
struct btf_type {
    __u32 name_off;     /* 类型名称在字符串表中的偏移(0 表示匿名类型) */
    __u32 info;         /* 编码:kind + vlen + kind_flag */
    __u32 size;         /* 类型大小(结构体/联合体/枚举等),或 "type" 字段 */
};

info 字段的位布局需要解码:

/* tools/lib/bpf/btf.h */
#define BTF_INFO_KIND(info)     (((info) >> 24) & 0x1f)   /* bits 24-28: kind (5 bits) */
#define BTF_INFO_VLEN(info)     ((info) & 0xffff)          /* bits 0-15: vlen (16 bits) */
#define BTF_INFO_KFLAG(info)    ((info) >> 31)             /* bit 31: kind_flag */
  • kind(5 bits):类型种类(BTF_KIND_INT, BTF_KIND_STRUCT, 等 20 种),定义在 include/uapi/linux/btf.henum btf_kind 中;
  • vlen(16 bits):变长成员数量——如结构体的字段数、函数的参数数;
  • kind_flag(1 bit,bit 31):扩展标志,用在 BTF_KIND_STRUCT/BTF_KIND_UNION 中,指示成员偏移量编码方式(0 为 32-bit 偏移,1 使用 BTF_MEMBER_BITFIELD_SIZE 记录高 8 位作为位域大小);
  • kflag(= kind_flag):存在是因为 BTF 的 vlen 只需要 16 位而 type kind 只有 5 位,剩下 11 位中的最高位用来承载 extra flag。

size 字段的含义随 kind 变化:

kind size 含义
BTF_KIND_INT 整数类型的总字节大小
BTF_KIND_STRUCT / BTF_KIND_UNION 结构体/联合体的总字节大小
BTF_KIND_ENUM / BTF_KIND_ENUM64 枚举的底层类型大小
BTF_KIND_PTR / BTF_KIND_FWD / BTF_KIND_TYPEDEF 0(不适用)
BTF_KIND_ARRAY 0,数组的 element type / nelems 存在 vlen data 中
BTF_KIND_FUNC <linkage>(符号链接属性)
BTF_KIND_FUNC_PROTO 无关紧要
BTF_KIND_VAR 变量大小
BTF_KIND_DATASEC 节的字节大小

2.3 字符串表

字符串表是一个以 \0 结尾的连续字节区域,存放在 BTF 数据块的尾部。name_off 是类型名称在字符串表中的起始偏移——与 ELF 的字符串表不同,BTF 字符串没有全局去重的要求(虽然 btf_dedup 会合并相同字符串),解析时按 name_off 直接读取,读到下一个 \0 为止。

例如,name_off = 42 意味着类型名称的起始地址为 btf_data + str_off + 42

三、20 种 BTF Type Kind 编码

3.1 基本类型:BTF_KIND_INT

BTF_KIND_INT 编码 C 语言的整数类型(char, short, int, long, unsigned long, bool 等)。size 字段给出字节大小(1/2/4/8),vlen data 包含一个 32-bit 的 encoding 字段:

/* include/uapi/linux/btf.h - BTF_INT_* 编码位 */
#define BTF_INT_ENCODING(VAL)   (((VAL) & 0x0f000000) >> 24)
#define BTF_INT_OFFSET(VAL)     (((VAL) & 0x00ff0000) >> 16)
#define BTF_INT_BITS(VAL)       ((VAL)  & 0x000000ff)

/* 编码标志 */
#define BTF_INT_SIGNED  (1 << 0)  /* 有符号 */
#define BTF_INT_CHAR    (1 << 1)  /* char 或类似 */
#define BTF_INT_BOOL    (1 << 2)  /* bool */

例子——unsigned int(32-bit 无符号整数): - name_off 指向字符串 "unsigned int" - infokind=BTF_KIND_INT, vlen=1(1 个 encoding 值) - size=4 - vlen data 中的 encoding 值:bits(32) | offset(0) | (BTF_INT_SIGNED 未置位)

3.2 指针/引用:BTF_KIND_PTR

最简单的一种——size 为 0(真正的类型信息在被指向的类型中),vlen 为 0,vlen data 为空。指向目标类型的 type_id 存放在 btf_type->size 后面的隐含 4 字节中:

struct btf_type    // 12 字节
__u32 type;        // 隐含的 4 字节:被指向类型的 type_id

解析器需要根据 btf_type 的长度判断是否有这个尾部字段——BTF_KIND_PTRvlen=0 且 total size = 16(12 + 4)。

3.3 数组:BTF_KIND_ARRAY

编码 C 语言的数组(int arr[10])。size 字段为 0。vlen data 包含一个 struct btf_array

/* include/uapi/linux/btf.h (kernel 6.6) */
struct btf_array {
    __u32   type;           /* 元素类型 ID */
    __u32   index_type;     /* 索引类型 ID(通常是 'unsigned int' 的 BTF type_id) */
    __u32   nelems;         /* 元素个数 */
};

对于 int[5]type 指向 int 类型的 BTF type_id,index_type 指向 __u32 对应的 BTF_KIND_INT entry,nelems=5

3.4 结构体:BTF_KIND_STRUCT

编码 C 语言的结构体。size 字段给出总字节大小。vlen 表示成员数量,vlen data 包含 vlenstruct btf_member

/* include/uapi/linux/btf.h (kernel 6.6) */
struct btf_member {
    __u32   name_off;       /* 成员名称在字符串表中的偏移 */
    __u32   type;           /* 成员类型 ID */
    __u32   offset;         /* 从结构体起始到该成员的位偏移 */
};

offset 的单位是(bit),不是字节。对于正常的非位域成员,offset 是字节偏移量乘以 8。对于位域成员,offset 的低 24 位表示位偏移,高 8 位表示位域大小(当 kind_flag 设置时):

offset 编码(kind_flag=1 时):
  bits [31:24]: 位域大小(0 表示非位域)
  bits [23:0]:  位偏移(从结构体起始)

3.5 联合体:BTF_KIND_UNION

BTF_KIND_STRUCT 几乎相同,唯一的区别是所有成员的 offset 都是 0(因为联合体成员共享起始地址),size 是最大成员的大小。

3.6 枚举:BTF_KIND_ENUM 与 BTF_KIND_ENUM64

编码 C 语言的枚举。vlen data 包含 vlenstruct btf_enumstruct btf_enum64

/* include/uapi/linux/btf.h */
struct btf_enum {
    __u32   name_off;       /* 枚举值名称 */
    __s32   val;            /* 枚举值(32-bit 有符号) */
};

struct btf_enum64 {
    __u32   name_off;
    __u32   val_lo32;       /* 低 32-bit */
    __u32   val_hi32;       /* 高 32-bit */
};

BTF_KIND_ENUM64(6.0+)解决了传统 BTF_KIND_ENUM 无法表示 64-bit 枚举值的问题——在内核中带有 U64_MAX 等值的枚举需要 64 位宽度。

3.7 前向声明:BTF_KIND_FWD

编码 C 语言的前向声明(struct task_struct;)。name_off 指向类型名称,size=0(前向声明没有大小信息),kind_flag 指示是 struct(0)还是 union(1)。

3.8 typedef:BTF_KIND_TYPEDEF

编码 C 语言的 typedef(typedef int pid_t)。size=0,隐含的尾部 __u32 type 指向被定义的类型。

3.9 类型修饰符:CONST / VOLATILE / RESTRICT / TYPE_TAG

这四种修饰符都是对另一个类型的包裹:

  • BTF_KIND_CONSTconst intCONST(INT)
  • BTF_KIND_VOLATILEvolatile intVOLATILE(INT)
  • BTF_KIND_RESTRICTint *restrict 中的 restrict
  • BTF_KIND_TYPE_TAG__user__rcu 等 attribute 标签

编码方式与 BTF_KIND_PTR 相同——隐含的尾部 __u32 type 指向被修饰的类型,name_off 指向类型标签的名称(如 "__user")。

3.10 函数相关:BTF_KIND_FUNC 与 BTF_KIND_FUNC_PROTO

BTF_KIND_FUNC 编码内核中的函数符号(如 tcp_connect)。name_off 指向函数名,隐含的尾部 __u32 type 指向对应的 BTF_KIND_FUNC_PROTO

BTF_KIND_FUNC_PROTO 编码函数签名。size 表示参数数量。vlen data 包含 vlenstruct btf_param,每个参数为一个 (name_off, type_id) 对:

/* include/uapi/linux/btf.h */
struct btf_param {
    __u32   name_off;       /* 参数名称(可以是 0 表示未命名) */
    __u32   type;           /* 参数类型 ID */
};

隐含的尾部 __u32 type 指向返回类型。

int tcp_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len) 在 BTF 中编码为:

FUNC_PROTO(返回类型=INT, 参数数=3)
  param[0]: name="sk", type=PTR→STRUCT("sock")
  param[1]: name="uaddr", type=PTR→STRUCT("sockaddr")
  param[2]: name="addr_len", type=INT
FUNC(name="tcp_connect", type=FUNC_PROTO)

3.11 全局变量与节:BTF_KIND_VAR 与 BTF_KIND_DATASEC

BTF_KIND_VAR 编码全局变量(如 BPF 程序的全局 data/rodata 变量)。name_off 指向变量名,隐含尾部 __u32 type 指向变量类型,额外的尾部 __u32 linkage(1 = static,2 = global,3 = extern)。

BTF_KIND_DATASEC 编码 ELF Section 中的变量布局。size 是节的字节大小,vlen data 包含 vlenstruct btf_var_secinfo

/* include/uapi/linux/btf.h */
struct btf_var_secinfo {
    __u32   type;       /* BTF_KIND_VAR 的 type_id */
    __u32   offset;     /* 变量在节中的字节偏移 */
    __u32   size;       /* 变量占用的字节大小 */
};

skeleton 中的 skel->rodataskel->data 等指针就是通过 DATASEC 解析得到的——它告诉用户态代码每个全局变量在 BPF 程序的数据区中的精确偏移和大小。

3.12 浮点与声明标签

  • BTF_KIND_FLOAT(5.1+):编码 floatsize=4)和 doublesize=8)。虽然 BPF 指令集不支持浮点操作,但 BTF 需要能够描述内核数据结构中的 float/double 字段。
  • BTF_KIND_DECL_TAG(5.16+):声明级别的注解(如 __attribute__((btf_decl_tag("...")))),用于给 map、程序、变量附加 key-value 元数据。内核通过 bpf_core_decl_tag 宏在编译时转换为 BTF_KIND_DECL_TAG 条目。

3.13 完整的 type kind 表

kind 枚举 数值 引入版本 vlen data 结构体 用途
BTF_KIND_INT 1 本源 u32 encoding 整数、char、bool
BTF_KIND_PTR 2 本源 u32 type 指针
BTF_KIND_ARRAY 3 本源 struct btf_array 数组
BTF_KIND_STRUCT 4 本源 struct btf_member[] 结构体
BTF_KIND_UNION 5 本源 struct btf_member[] 联合体
BTF_KIND_ENUM 6 本源 struct btf_enum[] 枚举(32-bit)
BTF_KIND_FWD 7 本源 前向声明
BTF_KIND_TYPEDEF 8 本源 u32 type typedef
BTF_KIND_VOLATILE 9 本源 u32 type volatile 修饰
BTF_KIND_CONST 10 本源 u32 type const 修饰
BTF_KIND_RESTRICT 11 本源 u32 type restrict 修饰
BTF_KIND_FUNC 12 5.0 u32 type 函数符号
BTF_KIND_FUNC_PROTO 13 5.0 struct btf_param[] 函数签名
BTF_KIND_VAR 14 5.1 u32 type + u32 linkage 全局变量
BTF_KIND_DATASEC 15 5.1 struct btf_var_secinfo[] 节内变量布局
BTF_KIND_FLOAT 16 5.1 浮点类型
BTF_KIND_DECL_TAG 17 5.16 u32 type + s32 idx 声明注解
BTF_KIND_TYPE_TAG 18 5.17 u32 type 类型标签
BTF_KIND_ENUM64 19 6.0 struct btf_enum64[] 枚举(64-bit)

四、BTF.ext 节:连接指令与类型

.BTF.ext 是 ELF 文件中独立于 .BTF 的节,包含三类扩展信息:

/* include/uapi/linux/btf.h */
struct btf_ext_header {
    __u16   magic;          /* 0xeb9f */
    __u8    version;
    __u8    flags;
    __u32   hdr_len;
    __u32   func_info_off;  /* func_info 子节的偏移 */
    __u32   func_info_len;
    __u32   line_info_off;  /* line_info 子节的偏移 */
    __u32   line_info_len;
    __u32   core_relo_off;  /* CO-RE 重定位子节的偏移(第 12 篇详述) */
    __u32   core_relo_len;
};

4.1 func_info

func_info 将 BPF 程序中的函数指令偏移映射到 BTF 中的函数类型 ID。内核 verifier 在加载时使用 func_info 来确定 BPF_PSEUDO_CALL 目标函数的签名——这使得 verifier 能够像检查 helper 调用一样检查程序内调用:

/* 对每个 BPF 子程序 */
struct bpf_func_info {
    __u32   insn_off;       /* 函数起始指令的偏移(从程序第一条指令算起) */
    __u32   type_id;        /* 此函数在 BTF 中的 BTF_KIND_FUNC type_id */
};

4.2 line_info

line_info 将 BPF 程序的每条指令映射回源码的 "file:line:column\“。它的作用是让 verifier 日志输出人类可读的源码位置,而不是只打印指令编号:

/* 对每条 BPF 指令 */
struct bpf_line_info {
    __u32   insn_off;       /* 指令偏移 */
    __u32   file_name_off;  /* 文件名在 BTF 字符串表中的偏移 */
    __u32   line_off;       /* 行号在 BTF 字符串表中的偏移(存为字符串) */
    __u32   line_col;       /* 低 10 位: 列号, 高 22 位: 行号 */
};

没有 line_info 时,verifier 日志长这样:

0: (b7) r1 = 0
1: (73) *(u8 *)(r10 -1) = r1
2: (bf) r2 = r10
...

line_info 后:

; u8 key = 0;
0: (b7) r1 = 0
; map_val = bpf_map_lookup_elem(&my_map, &key);
1: (73) *(u8 *)(r10 -1) = r1
2: (bf) r2 = r10

这在调试大型 BPF 程序时是不可或缺的。

4.3 CO-RE 重定位条目

core_relo 子节存储面向 CO-RE 的重定位条目(struct bpf_core_relo),包含指令偏移、访问路径、重定位种类等信息。libbpf 在加载时使用这些条目修补 BPF 指令。详细机制见第 12 篇。

五、内核 BTF 的生成管线

5.1 构建流程

内核 BTF 的生成是构建流程的一部分,需要在 CONFIG_DEBUG_INFO_BTF=y 下进行:

kernel compile (gcc/clang + -g)
   vmlinux(含 DWARF .debug_info 节, ~120 MB)
     pahole --btf_encode_detached
       vmlinux.btf(BTF 编码, ~5 MB 原始)
         libbpf btf_dedup()
           vmlinux.btf(去重后, ~1–2 MB,典型 x86_64 + CONFIG_DEBUG_INFO_BTF)
             嵌入 vmlinux image
               挂载到 /sys/kernel/btf/vmlinux

关键步骤:

  1. 内核编译gcc -gclang -g 生成 DWARF 调试信息,嵌入 vmlinux ELF;
  2. pahole 转换:构建系统(scripts/link-vmlinux.sh)调用 pahole --btf_encode_detached,从 vmlinux 的 DWARF 中提取类型信息,编码为 BTF 格式;
  3. btf_dedup 去重pahole 内部链接 libbpf,调用 btf_dedup() 将原始 BTF(包含因头文件重复包含产生的大量重复类型)压缩到最终大小;
  4. 嵌入 vmlinux:去重后的 BTF 通过 objcopy --add-section .BTF=vmlinux.btf 嵌入 vmlinux ELF;
  5. 运行时访问:内核启动后将 vmlinux BTF 暴露到 /sys/kernel/btf/vmlinux

5.2 模块 BTF

每个内核模块也会生成自己的 BTF(如果 CONFIG_DEBUG_INFO_BTF_MODULES=y),存储在模块的 ko 文件中,加载时通过 /sys/kernel/btf/<模块名> 暴露。模块 BTF 的 type_id 会进行偏移调整——模块的第一个类型紧接 vmlinux BTF 的最后一个类型之后,形成统一的类型命名空间。

六、btf_dedup:BTF 去重算法

6.1 为什么需要去重

内核源码中的同一个类型可能出现在多个地方:

// include/linux/types.h
typedef unsigned int __u32;        // type_id=100

// include/linux/counter.h (includes types.h)
typedef unsigned int __u32;        // type_id=1500

// arch/x86/include/asm/page.h (includes types.h)
typedef unsigned int __u32;        // type_id=3000

在 DWARF 中,这三个 __u32 分别对应不同的编译单元(CU),pahole 在转换时会把每个 DWARF DIE 生成一个独立的 BTF type entry。如果不去重,三个完全相同的 __u32 typedef 会浪费类型空间和字符串表空间。

6.2 算法原理

btf_dedup()tools/lib/bpf/btf.c)基于结构等价性(structural equivalence)进行去重:

  1. 哈希阶段:为每个类型计算一个哈希值——对 INT 类型,哈希输入为 (name_off, size, encoding);对 STRUCT 类型,哈希输入为 (name_off, size, member_count, 每个成员的 (name_off, type_id, offset))。递归依赖的 type_id 在哈希时使用当前的临时映射。

  2. 等价性映射阶段:对每个哈希桶中的类型进行等价性比较。两个 STRUCT 类型被认为等价当且仅当:

    • 名称相同(或两者都为匿名)
    • 大小相同
    • 成员数量相同
    • 每个对应成员的名称、类型(经过映射后)、偏移量相同
  3. 重映射阶段:创建从旧 type_id 到新 type_id 的映射表,所有引用旧 type_id 的地方替换为新 type_id。

  4. 压缩阶段:移除重复的 type entry,重新编号,生成紧凑的 BTF。

6.3 去重的边界

去重不会合并名称不同但结构相同的类型。struct {int x; int y;} 如果分别以 pointvector 命名,去重后仍是两个独立类型——名称是结构等价性判断的输入之一。这是刻意的设计选择:合并不同名称的类型会导致调试工具(bpftool btf dump format c)输出混乱(类型名丢失)。

去重的一个副作用是消除 "forward declaration" 冲突——如果一个 FWD 类型指向一个后续定义的 STRUCT,去重会将 FWD 直接映射到 STRUCT 的 type_id,消除间接引用。

七、常用操作:通过 BTF 访问内核类型

7.1 从 /sys/kernel/btf/vmlinux 获取 BTF

# 查看 vmlinux BTF 的大小
ls -lh /sys/kernel/btf/vmlinux

# 导出为原始二进制
cp /sys/kernel/btf/vmlinux /tmp/vmlinux.btf

# 查看 BTF 中的所有类型
bpftool btf dump file /sys/kernel/btf/vmlinux | head -100

# 生成 vmlinux.h
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

7.2 查找特定类型

# 在 BTF 中查找 struct task_struct
bpftool btf dump file /sys/kernel/btf/vmlinux | grep -A 50 'STRUCT "task_struct"'

# 查找特定函数签名
bpftool btf dump file /sys/kernel/btf/vmlinux | grep -A 20 'FUNC_PROTO.*tcp_connect'

# 查看所有枚举
bpftool btf dump file /sys/kernel/btf/vmlinux | grep '^\[.*\] ENUM '

7.3 检查模块 BTF

# 列出所有可用的 BTF
ls /sys/kernel/btf/

# 查看特定模块的类型
bpftool btf dump file /sys/kernel/btf/nf_tables | head -50

八、小结

BTF 是 eBPF 基础设施的类型语言。从 struct btf_header 的固定 24 字节头,到 20 种 type kind 的变长编码,再到 BTF.ext 的 func_info/line_info 指令级映射——这条格式管线支撑了 verifier 的类型检查、bpftool 的人性化输出、CO-RE 的跨版本字段定位。内核构建流程中 pahole 的 DWARF-to-BTF 转换将 120 MB 的调试信息压缩到 1-2 MB,btf_dedup() 的结构等价性去重让这个压缩成为可能。下一篇文章进入 CO-RE 重定位引擎——BTF 的下游消费者,讲清 libbpf 如何使用 BTF 在加载时修补 BPF 指令来实现在不同内核版本上的可移植性。


参考

  1. Linux 内核源码 include/uapi/linux/btf.h(kernel 6.6):struct btf_headerstruct btf_type、BTF_KIND_* 枚举及其成员结构体
  2. Linux 内核源码 tools/lib/bpf/btf.cbtf_dedup() 去重算法实现
  3. Linux 内核源码 tools/lib/bpf/btf.h:BTF 解析宏(BTF_INFO_KIND()BTF_INFO_VLEN()
  4. Linux 内核文档 Documentation/bpf/btf.rst:BTF 格式规范
  5. Linux 内核文档 Documentation/bpf/bpf_devel_QA.rst:BTF 生成的编译要求
  6. pahole 源码(dwarves 项目):DWARF 到 BTF 的编码器实现

上一篇libbpf 加载器工程(第 10 篇)

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

同主题继续阅读

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

2026-06-12 · kernel / ebpf

【eBPF 内核实现深度拆解】从验证器到 JIT,从 BTF 到调度器

eBPF 内核虚拟机内部实现系统讲解:BPF 指令集与寄存器机器、验证器的抽象解释与状态裁剪、JIT 编译器后端、Map 各类型的并发与内存模型、helper 函数注册与类型检查、BTF 格式规范与 CO-RE 重定位引擎、libbpf 加载器工程、fentry/fexit 蹦床机制、sched_ext 调度器内核接口。面向想读懂 eBPF 内核源码、写生产级 BPF 程序的系统工程师。

2026-06-12 · kernel / ebpf

【eBPF 内核实现深度拆解】实战:构建微型 eBPF 可观测 Agent

把 01--17 的知识串成一条实践线——从 libbpf skeleton 写第一个 BPF 程序、加载到内核、用 ring buffer 回传事件、用 CO-RE 实现跨内核版本兼容、map pinning 实现热升级、配上半自动化的 verifier 错误排障流程——构建一个麻雀虽小五脏俱全的 eBPF 可观测 Agent。