本文地址:https://www.ebpf.top/post/inline-btf-tracing

写 BPF 追踪程序时,最让人泄气的一刻是:明明在源码里看到了那个函数,fentry 却挂不上,kprobe 也报找不到符号。Oracle 的 Alan Maguire(pahole 共同维护者)在博客 Adding inlined functions to the BPF Type Format 里,先把「函数为什么追不到」拆成几类,再给出一个让被内联函数也能被描述、进而被追踪的 BTF 扩展方案。

先给结论:在作者统计的一份 Fedora 43 内核里,可直接用 fentry/fexit 的函数约 50,852 个(50.0%),完全被内联、没有独立副本的约 40,026 个(39.4%),对应约 40 万处内联点(inline site)。内联点大约是可追踪函数的 10 倍。这部分过去基本无解,也是新方案瞄准的对象。方案涉及 libbpf、pahole 和 kbuild 三处的补丁系列,目前仍在推进,消费侧(如 kprobe multi)也在跟进,还不是发行版的默认能力。

本号《BTF 实践指南》讲过 BTF 如何描述类型和函数,这篇补上「被编译器内联掉的函数」这一块。

1. 问题:一个函数追不到,通常是四种原因之一

挂载失败时,先判断属于哪一类,再决定换挂点还是换方法。作者给出的四类如下:

追不到函数的四类原因及判断方法

图中的分类用于初步排查:ftrace 可用列表、BTF 签名和 kprobe 黑名单是不同的检查项,不能仅凭其中一项断定所有追踪方式都不可用。

第一类:函数受 notrace、noinstr 等插桩限制。 这类限制用于避免追踪递归或保护不能插桩的关键路径。函数在 /proc/kallsyms 里有、却不在 /sys/kernel/tracing/available_filter_functions 里,只能说明它不在 ftrace 的可用列表中,不能据此断定一定标记了 notrace 或 noinstr。available_filter_functions_addrs 还给出 ftrace 补丁点地址、函数名和可选的模块名,地址可能与 kallsyms 不同。能否改用 kprobe,要另外核对 kprobe 黑名单及目标路径的限制;不能把 notrace 一概理解为所有追踪方式都不可用。受禁止插桩约束的目标应换上下游挂点。

第二类:函数存在,但 BTF 里没有它的签名。 fentry/fexit 依赖 BTF 中的 BTF_KIND_FUNC/FUNC_PROTO,verifier 要靠签名才能安全地读参数。编译器优化有时会去掉参数,或把参数放进 ABI 约定之外的寄存器,这时 pahole 生成 BTF 会跳过该函数。可以用 bpftool btf dump file /sys/kernel/btf/vmlinux 检查内核本体的签名;模块函数还应检查 /sys/kernel/btf/<模块名>。未找到签名只能说明所查 BTF 中缺少它,不能单凭这一点认定是优化导致;GCC 编译的内核里,这类函数常带 .isra 或 .constprop 后缀。对策是改用 kprobe,但要注意参数可能不在你预期的寄存器里。

第三类:函数被完全内联。 编译器把函数体直接展开进调用者,省掉调用开销。内核大量使用 inline 和 __always_inline,尤其是头文件里的小函数。这类函数在 /proc/kallsyms 里根本没有符号,过去除了改代码(比如去掉 static,但开启 LTO 时也会失效)几乎没有办法。

第四类:具备 fentry/fexit 的基本条件。 函数出现在 available_filter_functions_addrs 中,且对应内核或模块 BTF 中有可用的 BTF_KIND_FUNC/FUNC_PROTO 签名时,可优先尝试 fentry/fexit;最终仍以当前内核的挂载检查结果为准。

作者在 Fedora 43 内核上的分布统计(合计 101,604 个函数):

类别 函数数 占比
可用 fentry/fexit 50,852 50.0%
完全内联 40,026 39.4%
notrace / noinstr 9,071 8.9%
优化导致无 BTF 1,655 1.6%

Fedora 43 内核函数可追踪性分布

可以看到,追不到的函数里,内联占了大头,远多于另外两类之和。

2. 原理:内联点的信息原本只在 DWARF 里

好消息是 kprobe 能挂在几乎任意指令上,理论上可以在内联点下探针。难点在于:哪条指令属于哪个被内联的函数、参数此刻在哪个寄存器或栈位置,这些信息只存在于 DWARF 调试信息里。DWARF 动辄几百 MB,通常要单独装 debuginfo 包,生产机上往往没有。

BTF 则不同,它体积小、直接编进内核镜像,在 /sys/kernel/btf 下随时可读。所以思路很自然:如果能把约 40 万个内联点紧凑地写进 BTF,生产机就能在没有 DWARF 的情况下追踪内联函数。

DWARF 与 BTF:内联信息能否上生产机

难点也在「40 万」这个量级上,表示方式必须足够省。BTF 保持小体积靠的是去重,例如整个内核只保留一份 struct sk_buff,而不是每个目标文件各一份。新表示也必须能被去重。

3. 做法:三个新 BTF kind,把「是谁」和「参数在哪」拆开存

每个内联点至少要记三件事:内联点地址、被内联的是哪个函数、参数从哪里取。作者为此新增了三个 kind:

  • BTF_KIND_LOC_PARAM:描述一个参数怎么取,可以是寄存器、寄存器加偏移、对寄存器加偏移解引用,或者是一个常量。
  • BTF_KIND_LOC_PROTO:由一组 LOC_PARAM 组成,描述某个内联点全部参数的位置。
  • BTF_KIND_LOCSEC:一个 ELF section(如 .text)里的内联点列表,每项包含被内联函数的 FUNC id、对应的 LOC_PROTO,以及内联点相对 section 起点的 32 位偏移。

三个新 BTF kind 的层次关系

关键设计在于分离:函数身份只放在 LOCSEC 的条目里,这部分天然无法去重;而参数位置描述 LOC_PARAM/LOC_PROTO 在大量内联点之间是重复的,可以被充分去重。同一套表示既能描述在多个内联点被内联、每处参数可用情况不同的函数,也能描述「部分内联」的函数,即一部分调用点被内联,其余仍是普通函数。

为了让这套表示落地,作者先做了三项前置改造:

  1. kind layout 信息:过去旧工具遇到不认识的 BTF kind 会整个解析失败。现在 BTF 可携带一段「kind 布局」描述,旧工具即使不认识新 kind,也能跳过它继续解析。
  2. multi-split BTF:inline 信息体积大,放在单独的 split BTF 里。模块的 BTF 本身已经叠在 vmlinux 之上,模块的 inline 信息还要再叠一层,例如 xfs.inline 叠在 xfs 上,xfs 再叠在 vmlinux 上。
  3. 扩宽 vlen 和 kind 字段:btf_type 的 info 字段原来只用 16 位存 vlen,LOCSEC 最多只能装 65,535 项,装不下 .text 里的 40 万个内联点。改造后 vlen 用 24 位(上限约 1,670 万),kind 从 5 位扩到 7 位(最多 128 种)。

4. 进展:生成侧有了雏形,消费侧还在跟进

工具链各环节的状态,按作者博客的描述整理如下:

环节 状态
libbpf 补丁系列加入三个新 kind
pahole --btf_features=inline 可生成
kbuild 生成 .BTF.inline,Eduard Zingerman 在 resolve_btfids 中拆分
消费侧 Jiri Olsa 在看 kprobe multi 如何使用

从 pahole 生成到 kprobe multi 消费的链路

kbuild 的设计是:开启 CONFIG_DEBUG_INFO_BTF=y 时,vmlinux 和模块会多出一个 .BTF.inline section,并通过 /sys/kernel/btf/ 下的 .inline 文件暴露。vmlinux 的 inline 信息未压缩约 10MB,所以也支持 CONFIG_DEBUG_INFO_BTF=m,把它放进按需加载的 btf_vmlinux_inline.ko,访问时才加载。

需要强调的是,这些都以作者博客的描述为准,属于仍在推进的补丁系列。你手头的内核、libbpf 和 pahole 是否已经包含,需要对照版本自行确认,不要默认升级就能用。

5. 限制与建议:内联追踪就绪前,先把诊断做对

在消费侧真正可用之前,被内联的函数仍然很难追。眼下能做的是:

  1. 分项初步排查:结合 available_filter_functions_addrs、对应内核或模块的 BTF,以及 kallsyms 判断可能的原因;这些检查不能单独证明函数被内联或禁止所有追踪。考虑 kprobe 时还需核对其黑名单和目标路径限制。
  2. 无 BTF 的优化函数改用 kprobe:同时核对参数的实际寄存器,不要假设符合 ABI。
  3. 内联函数换挂点:改挂外层未被内联的调用者,或在测试环境里通过改代码避免内联,作者也承认这种办法很脆弱。
  4. 跟进工具链:关注 pahole、libbpf 的 inline 特性和内核的 .BTF.inline 进展,等 kprobe multi 接上这些 kind 再评估。

从「描述可见的函数」到「描述被编译器抹掉的调用点」,BTF 正在补上它和 DWARF 之间最大的一块差距。这和本号《BTFGen》关注的是同一个方向:让只有 BTF 的生产机,也能拿到原本只有 DWARF 才能提供的信息。

延伸阅读

  1. 《BTF 实践指南[译]》
  2. 《BTFGen: 让 eBPF 程序可移植发布更近一步》
  3. 《数据包到底卡在哪?用 netstacklat 给网络协议栈做 CT》