PAYATHON 2026

升级至 kernel 6.8.0-48-generic 后 libbpf 无法找到 BTF

排障赵师傅

结论

有 vmlinux.h,并不能说明 libbpf 在运行时一定可以加载内核 BTF。这两个文件的用途不同:

  • vmlinux.h 是编译期使用的头文件,由 bpftool 根据 BTF 生成。
  • /sys/kernel/btf/vmlinux 是运行时 BTF 数据。libbpf 加载 eBPF object、执行 CO-RE relocation 时会读取它。

从目前的现象看,libbpf 已经找到了 /sys/kernel/btf/vmlinux,但未能成功解析或使用,随后才到 /boot、/lib/modules 等位置搜索候选 vmlinux。因此,strace 中的这些 ENOENT 通常只是回退搜索记录,未必是问题的根因。

首先应确认用户程序实际链接的 libbpf 版本。Ubuntu 22.04 自带的用户态 libbpf 可能早于 6.8 HWE kernel。新内核导出的某些 BTF 类型或编码,旧版 libbpf 可能无法识别,进而把 /sys/kernel/btf/vmlinux 判定为无效。也要考虑文件读取失败、内容损坏,或程序运行在容器、chroot 等不同 mount namespace 中的情况。

为什么重新生成 vmlinux.h 没有解决问题

这条命令只负责读取 BTF 并生成 C 类型定义:

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

编译 execve.bpf.c 时,Clang 会使用 vmlinux.h 中的结构体声明。程序运行后,libbpf 不会再读取这个头文件,而是加载当前运行内核的原始 BTF,用它解析 CO-RE relocation,并将 eBPF object 中引用的类型和字段映射到当前内核。

即使 vmlinux.h 能正常生成,运行时仍可能遇到以下问题:

  1. 生成头文件的 bpftool 可以解析这份 BTF,但应用链接了另一个更旧的 libbpf。
  2. 编译和运行时使用的 libbpf 版本不同。
  3. /sys/kernel/btf/vmlinux 虽然可见,但读取结果不完整,或运行环境不同。
  4. eBPF object 是旧文件,切换内核或工具链后没有彻底重建。
  5. 程序设置了自定义 BTF 路径,或构建参数引入了另一份 libbpf。

重新安装 kernel headers 或升级 pahole 通常无法解决这些问题。headers 不参与 libbpf 的运行时解析;pahole 主要用于在内核及其他 ELF 文件中生成 BTF,不负责应用加载阶段的解析。

先验证内核导出的 BTF

先确认当前运行的内核和 BTF 文件处于同一个环境:

uname -a
uname -r
stat /sys/kernel/btf/vmlinux

再检查内核配置:

grep -E 'CONFIG_DEBUG_INFO_BTF|CONFIG_BPF_SYSCALL' \
    /boot/config-$(uname -r)

正常情况下,至少应看到:

CONFIG_DEBUG_INFO_BTF=y
CONFIG_BPF_SYSCALL=y

用下面的命令直接验证 BTF 能否完整解析:

sudo bpftool btf dump \
    file /sys/kernel/btf/vmlinux \
    format raw > /dev/null

echo $?

返回值为 0,说明当前 bpftool 可以读取并解析这份 BTF。还可以检查文件大小和文件头:

sudo wc -c /sys/kernel/btf/vmlinux
sudo od -An -tx1 -N16 /sys/kernel/btf/vmlinux

原始 BTF 的开头应包含 BTF magic。在使用小端序的 x86_64 系统中,前两个字节通常是:

9f eb

如果 bpftool btf dump 本身就执行失败,应先排查 kernel package、sysfs mount、文件读取错误或该 kernel build 的 BTF,暂时不必修改 eBPF 源码。

检查程序实际使用的 libbpf

只看 libbpf-dev 的包版本不够,还要确认最终 executable 加载的是哪个共享库:

pkg-config --modversion libbpf
ldd ./execve_user | grep libbpf
readelf -d ./execve_user | grep -E 'NEEDED|RPATH|RUNPATH'

如果使用 Ubuntu package,还可以检查:

dpkg-query -W libbpf-dev libbpf0 libbpf1 2>/dev/null
apt-cache policy libbpf-dev libbpf0 libbpf1

不同 Ubuntu 更新源使用的包名可能不同。部分查询返回“未安装”,不等于系统中没有 libbpf。

还要查看 Makefile 是否混用了不同目录下的头文件和库,例如:

/usr/include/bpf
/usr/local/include/bpf
/usr/lib/x86_64-linux-gnu
/usr/local/lib

常见情况是编译时包含了 /usr/local/include/bpf 中的新 header,链接或运行时却加载 /usr/lib/... 中的旧 libbpf,也可能反过来。

下面的命令可以显示动态加载过程:

sudo env LD_DEBUG=libs ./execve_user 2>&1 | grep -i libbpf

如果程序静态链接 libbpf,ldd 不会显示 libbpf.so。这时需要检查 Makefile 是否直接链接了 libbpf.a,并确认该静态库的来源。

为 libbpf 安装可靠的调试回调

LIBBPF_LOG_LEVEL=debug 不是所有 libbpf 版本都支持的统一开关。下面这种写法会先把变量设置给 sudo,而 sudo 可能清理环境:

LIBBPF_LOG_LEVEL=debug sudo ./execve_user

需要传递环境变量时,应使用:

sudo env LIBBPF_LOG_LEVEL=debug ./execve_user

更可靠的办法是在程序中安装 libbpf print callback:

#include <stdarg.h>
#include <stdio.h>
#include <bpf/libbpf.h>

static int libbpf_log(enum libbpf_print_level level,
                      const char *format,
                      va_list args)
{
    return vfprintf(stderr, format, args);
}

int main(void)
{
    libbpf_set_print(libbpf_log);

    /* 打开和加载 eBPF object */
    return 0;
}

回调应在调用 bpf_object__open_file()、skeleton open 或其他 libbpf API 之前注册。日志会显示 libbpf 正在解析哪份 BTF,以及解析在哪一步失败。

错误中的 -3 通常对应负的 errno 值,但仅凭这个数字不能断定文件不存在。它也可能由 libbpf 内部的类型查找产生,仍需结合完整 debug log 判断。

单独测试当前 libbpf 能否解析 kernel BTF

可以写一个小程序,先排除 eBPF object、ring buffer 和 attach 过程:

#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>

#include <bpf/btf.h>
#include <bpf/libbpf.h>

static int libbpf_log(enum libbpf_print_level level,
                      const char *format,
                      va_list args)
{
    return vfprintf(stderr, format, args);
}

int main(void)
{
    struct btf *btf;
    long err;

    libbpf_set_print(libbpf_log);

    btf = btf__parse("/sys/kernel/btf/vmlinux", NULL);
    err = libbpf_get_error(btf);
    if (err) {
        fprintf(stderr, "btf__parse failed: %ld (%s)\n",
                err, strerror((int)-err));
        return 1;
    }

    printf("kernel BTF parsed successfully\n");
    btf__free(btf);
    return 0;
}

使用项目实际采用的编译参数构建:

cc -Wall -O2 btf_probe.c \
    $(pkg-config --cflags --libs libbpf) \
    -o btf_probe

sudo ./btf_probe

根据结果可以进一步缩小排查范围:

  • bpftool btf dump 成功而 btf_probe 失败时,重点检查两者使用的 libbpf 版本和来源。
  • 两者都失败时,重点检查内核提供的 BTF、kernel package 和运行环境。
  • 两者都成功而原项目失败时,检查 execve.bpf.o、CO-RE relocation、Makefile,以及程序是否指定了其他 BTF 路径。

升级并统一 libbpf

如果应用确实链接了旧版 libbpf,应换用能够解析当前 kernel BTF 的较新版本,并确保 header、静态库或共享库来自同一套安装。

可选方案包括:

  1. 使用发行版提供的更新包或 backport。
  2. 升级到提供较新 libbpf 的 Ubuntu release。
  3. 从 libbpf 的正式 release 构建,并让项目明确使用这个版本。
  4. 将合适版本的 libbpf vendoring 到项目中,避免与系统库混用。

从源码构建并安装后,应用未必会自动加载新库。安装完成后仍要执行并检查:

sudo ldconfig
ldd ./execve_user | grep libbpf
pkg-config --modversion libbpf

如果系统中同时存在 /usr 和 /usr/local 两套 libbpf,应清理 Makefile 中指向不明确的 -I、-L 参数,或明确指定构建路径。不要长期依靠全局 LD_LIBRARY_PATH 掩盖库版本冲突。

仅凭现有错误信息,无法确定一个“最低可用 libbpf 版本”。应根据完整 debug log 中报告的不支持 BTF kind、解析错误或 relocation 错误,选择包含相应支持或修复的 release。

清理并重新构建 eBPF object

切换 kernel、Clang 或 libbpf 后,应先删除旧的中间文件:

make clean
rm -f execve.bpf.o execve.skel.h

然后重新生成头文件并构建:

bpftool btf dump \
    file /sys/kernel/btf/vmlinux \
    format c > vmlinux.h

make
sudo ./execve_user

如果项目会生成 skeleton,还要重新生成 execve.skel.h,不要继续使用旧文件。

可以检查 object 是否包含 BTF 和 BTF.ext:

readelf -S execve.bpf.o | grep -E 'BTF|BTF.ext'

通常应能看到 .BTF 和 .BTF.ext section。这些数据属于 eBPF object 本身,和 /sys/kernel/btf/vmlinux 中的 kernel BTF 不是同一份数据。

理解 /boot/vmlinux-* 的访问记录

libbpf 通常会先加载 sysfs 暴露的 raw BTF。这个步骤失败后,它可能继续搜索带有 .BTF section 的 kernel ELF image。不同版本的 libbpf 可能使用不同的搜索路径,例如:

/boot/vmlinux-$(uname -r)
/lib/modules/$(uname -r)/vmlinux-$(uname -r)

Ubuntu 默认安装中没有这些未压缩的 vmlinux 文件很常见。例如:

access("/boot/vmlinux-6.8.0-48-generic", R_OK) = -1 ENOENT

这条记录只表示一个回退候选不存在,不能据此判断必须手动创建该文件。把它软链接到 vmlinuz 也无法解决问题,因为 /boot/vmlinuz-* 通常是压缩的 boot image,并不是含有可直接读取 BTF section 的未压缩 ELF vmlinux。

在确认 libbpf 无法使用 /sys/kernel/btf/vmlinux 之前,不建议优先下载或复制 vmlinux。

排除运行环境问题

双启动和 grub2win 通常不会改变 libbpf 的 BTF 解析逻辑,但仍要确认实际启动的 kernel 是否符合预期:

uname -r
cat /proc/version
ls -l /boot/vmlinuz-*

如果程序运行在容器、chroot、systemd service 或其他 mount namespace 中,还应进入相应环境执行:

uname -r
stat /sys/kernel/btf/vmlinux
mount | grep ' /sys '

宿主机上存在该文件,不表示容器中看到的是同一个 /sys。如果 vmlinux.h 在宿主机上生成,而程序在另一个环境中运行,也会出现表面上互相矛盾的结果。

推荐的定位顺序

可以按以下顺序排查:

  1. 用 bpftool btf dump ... format raw 验证 kernel BTF。
  2. 用 ldd、pkg-config 和 LD_DEBUG=libs 确认程序实际使用的 libbpf。
  3. 安装 libbpf print callback,获取完整解析日志。
  4. 用独立的 btf__parse() 测试程序复现问题。
  5. 如果只有应用使用的 libbpf 失败,升级并统一 libbpf。
  6. 执行 make clean,重新生成 vmlinux.h、BPF object 和 skeleton。
  7. 如果相同版本的 libbpf 与 bpftool 都无法解析,再排查 kernel package 或该内核版本的 BTF 回归问题。

结合现有信息,最可能的情况是系统升级到 6.8 HWE kernel 后,应用仍在使用 Ubuntu 22.04 环境中的旧版或混装 libbpf。/boot/vmlinux-* 不存在,只是 libbpf 首次解析失败后触发的回退搜索。最终应以 btf__parse() 的完整 debug log 和 executable 实际加载的 libbpf 路径为准。

备注:内容仅供参考。