在 eBPF 技术的爆发式发展中,BTF(BPF Type Format) 毫无疑问是推动“一次编译,到处运行”(CO-RE, Compile Once – Run Everywhere)的关键基石。它不仅解决了困扰 eBPF 社区多年的内核结构体偏移与版本兼容问题,还在不断演进中将触角伸向了内核追踪的更深角落。
本文将系统梳理 BTF 的演进历史、未来发展方向(包含 2026 年 Linux 存储、文件系统、内存管理与 BPF 峰会上的最新提案),以及如何在实际的内核开发与 BPF 程序中应用 BTF。
一、 BTF 的前世今生与演进历史
1. 诞生背景:DWARF 的“胖”与难用
在 BTF 诞生前,Linux 内核与用户态程序主要依靠 DWARF 格式存储调试信息(Debugging Information)。
-
DWARF 的痛点:DWARF 格式极其庞大、结构复杂。一个包含完整 DWARF 信息的内核镜像或
vmlinux经常高达数百 MB 甚至上 GB。 -
eBPF 的困境:eBPF 运行在内核空间,加载器(如
libbpf)需要校验内核数据结构。如果在内核运行期解析庞大的 DWARF,无论是内存开销还是 CPU 性能损耗都是无法接受的。
2. BTF 的演进历程
为了提供一种极度轻量、易于内核解析的元数据格式,BTF 应运而生:
-
BTF v1(约 2018 年,Linux 4.18+):作为 DWARF 的轻量级替代品引入。它通过高度去重(Deduplication)算法,将整个内核所有数据类型的调试信息压缩到了仅 2~5 MB。
-
CO-RE 的基石(2019 年,Linux 5.2+):引入了 BTF Relocation(BTF 重定位) 机制。从此,eBPF 程序不再依赖编译宿主机的内核头文件,实现了一次编译即可运行在任意开启 BTF 的内核版本上。
-
Func & Var 支持(Linux 5.3+):BTF 开始支持内核函数原型和全局变量的描述,为
fentry/fexit等高效追踪探针铺平了道路。 -
Kernel Module BTF(Linux 5.11+):支持内核模块(
.ko)的 BTF 动态加载与分割(Split BTF),模块只存储相对于主内核增量的类型信息。 -
BTF Gen & decl_tag / type_tag(Linux 5.16+):进一步丰富了类型系统的表达能力(如标识指针的地址空间或安全属性)。
二、 BTF 的未来发展趋势:追踪内联函数
尽管 BTF 已经非常强大,但内核中仍有一个巨大的盲区:内联函数(Inlined Functions)。
在 2026 年 Linux 存储、文件系统、内存管理和 BPF 峰会上,Alan Maguire 针对这一瓶颈提出了最新的 BTF 扩展方案。以下为相关讨论的详细编译与分析:
追踪内联函数:2026 Summit 技术提案
BPF 程序使用 BPF 类型格式(BTF)调试信息来确定如何与内核中的函数进行交互。具体而言,追踪内核函数需要找到它在内核 BTF 节(section)中的地址——但这不适用于已被内联(inlined)的函数,因为内联函数没有单一且具体的地址。Alan Maguire 希望将内联函数的信息添加到 BTF 中以使其能够被追踪,并在 2026 年 Linux 存储、文件系统、内存管理和 BPF 峰会上主持了关于该主题的讨论。
Maguire 表示,内核中有超过 100,000 个内联函数,散布在五倍之多的位置。更糟的是,其中一些是部分内联的:在某些地方被正常调用,而在另一些地方则被内联。这可能导致如今出现一种情况:看起来函数已被成功追踪,但实际上某些调用并没有被捕获。
好消息是,追踪内联函数的其余基础设施已经就位(用于启用可以附加到任意位置的 kprobes)。Maguire 声明,现在的关键只是将函数在何处被内联的数据转换为可用的格式。“其实整个体系已经相当完整了。”
那么,将这些信息存储在 BTF 中需要什么?DWARF 调试格式已经有一种表示内联信息的方法,但 DWARF 难以处理,且缺乏表示常见情况的简便方法。Maguire 表示,适用于 BTF 的解决方案应当紧凑并允许去重,以保持较低的内存开销。理想情况下,内联信息可以存储在内核二进制文件的单独节中,甚至可以作为独立的内核模块分发,以便仅在需要时才进行加载。
具体而言,Maguire 建议向 BTF 添加三项新信息:
第一项是关于每个调用点内联了哪个函数以及如果不内联将如何调用的特定于内联点的信息,称为**“位置节”(location section)**。该数据无法轻易去重,因为它特定于给定的调用点,因此应尽可能由指向可去重数据的指针组成。
第二和第三项信息则是被指向的数据:“位置原型”(location prototype)和“位置参数”(location parameter)。位置原型通过指向位置参数的指针列表,指定了内联函数的参数在调用点是如何表示的;而每个位置参数则存储如何访问单个函数参数。
在理论上,编译器可以为每个内联调用点(共有 538,090 个)将给定的函数参数存储在不同的位置;但在实践中,编译器转换参数的方式有限,且许多函数具有兼容的签名,导致编译器做出相同的选择。在当前的内核中,对位置原型进行去重后仅产生 57,141 个独立条目,总共只引用了 17,535 个位置参数条目。
这意味着位置节占用了新增数据的大部分。总体而言,Maguire 提议向 BTF 添加的改动将增加约 11MB 的数据,即每个内联调用点约 21 字节。当提取到单独的内核模块并进行压缩后,总数据量可降至 3.5MB。
随后,Maguire 以一个具体函数为例,展示了如何存储其信息。假设有如下函数:
Cint foo(int a, void *b, bool c);如果编译器选择内联
foo(),且优化掉了未使用的参数a、将b提升为通过寄存器传递,并确定c为常量,则 BTF 表示将是一个单独的位置节条目,其中存储有foo()的 BTF 类型 ID、调用点相对于内存中内核基地址的偏移量,以及指向位置原型条目的指针。该位置原型条目会成为一个带有长度标记的数组,指向各个位置参数条目:
第一个条目为空(null),表示
a无法恢复。第二个条目指向一个位置参数条目,其标志指示该值包含在寄存器中,并指定了
b所在的具体寄存器编号。最后一个位置参数条目具有不同的标志,指定其为一个常量,随后是该常量的值。
Alexei Starovoitov 询问位置节条目将按什么顺序存储,因为内核在设置追踪时必须对其进行搜索以找到正确的条目。起初,Maguire 认为按调用点的地址排序最好,但在研究了数据的使用方式后,他决定改为按函数名称排序。这样一来,寻找特定函数信息的追踪程序就可以通过二分查找快速找到它。
Maguire 还讨论了向 BTF 添加内联信息时面临的两个前提问题:
位置节条目的数量要求将包含结构体的长度字段扩大到 24 位。
更严重的是,用于处理 BTF 的一些现有工具无法很好地应对其不认识的新标签。每当使用新类型信息扩展 BTF 时都会遇到这种问题,这也是 DWARF 最终变得如此脆弱的原因之一。为了解决这个问题,Maguire 将如何解析 BTF 的信息直接添加到了 BTF 本身之中(即自描述 BTF)。现在,任何能够读取该元信息的工具都可以正确跳过其无法理解的新标签。
Maguire 还必须更新
pahole(poke-a-hole)工具,以使其能够正确处理新 BTF 标签的排序。这最终变得有些复杂,但在正常的内核构建中应该可以正常工作。Andrii Nakryiko 询问了如何处理树外(out-of-tree)模块。Maguire 解释道,这些模块需要具有韧性的模块 BTF,以便在加载到运行中的内核时可以被明确地重定位。他的设计允许这样做,但会在一定程度上增加构建的复杂性。关于优雅地支持树外模块所需的修改,现场进行了一些讨论,但最终未达成一致意见。
三、 在 Linux 内核中如何使用 BTF 格式
理解了 BTF 的过去和未来后,我们在实际开发中该如何构建和消费 BTF 呢?
1. 内核编译期:如何生成 BTF?
内核构建系统利用 pahole 工具将 DWARF 自动转换为 BTF:
-
在内核配置中开启 BTF:
CONFIG_DEBUG_INFO_BTF=y -
编译内核时,构建工具链在生成
vmlinux后,会自动调用pahole -J vmlinux,提取其中的类型信息,进行去重压缩,并写入内核二进制文件的.BTF节中。
2. 内核运行期:如何获取 BTF 信息?
系统启动后,内核会将自身的类型元数据暴露在 sysfs 中:
-
主内核类型信息:
/sys/kernel/btf/vmlinux -
内核模块类型信息(如
ext4):/sys/kernel/btf/ext4
开发者可以使用 bpftool 工具对这些 BTF 信息进行 Dump 或查询:
# 1. 将内核 BTF 导出为单头文件 vmlinux.h(CO-RE 开发的核心)
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h
# 2. 查询内核中某个特定的结构体定义
bpftool btf dump file /sys/kernel/btf/vmlinux type task_struct
3. 开发实战:利用 BTF 实现 CO-RE 编程
在编写 eBPF 程序时,结合 BTF 可以实现跨不同内核版本的结构体无缝访问。
示例代码(eBPF C 语言侧):
#include "vmlinux.h" // 包含了从 BTF 导出的所有内核类型定义
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
SEC("kprobe/do_sys_openat2")
int BPF_KPROBE(do_sys_openat2, int dfd, const char *filename) {
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
pid_t pid;
// 方式 1:使用 BPF_CORE_READ,依赖 BTF 重定位,忽略目标内核结构体成员偏移的变化
pid = BPF_CORE_READ(task, pid);
// 方式 2:利用 Clang 编译器的 __builtin_preserve_access_index 直接访问
// pid = task->pid;
bpf_printk("Task PID from BTF CO-RE: %d\n", pid);
return 0;
}
char _license[] SEC("license") = "GPL";
用户态 Libbpf 的工作流程:
-
编译期:Clang 在将 BPF 代码编译为 ELF 对象文件时,会在记录有类型重定位信息的
.BTF.ext节中留下标记(如“访问了task_struct的pid字段”)。 -
加载期:
libbpf读取用户程序的.BTF.ext,并同时读取宿主机内核的/sys/kernel/btf/vmlinux。 -
动态修正:
libbpf对比两者的 BTF 定义,计算出目标内核中pid字段的实际字节偏移量(Offset),并直接修改 BPF 机器码中的指令,从而做到无缝兼容。
从简单的调试元数据,到支撑 CO-RE 的核心组件,再到未来对 10 万+ 内联函数的追踪探索,BTF 正在成为 Linux 内核可观测性与系统编程不可或缺的底层支柱。

:从演进历史到内联函数追踪的未来&spm=1001.2101.3001.5002&articleId=163996399&d=1&t=3&u=d9f8721a12ee4e91812789e579bb2a45)
449

被折叠的 条评论
为什么被折叠?



