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

【eBPF 内核实现深度拆解】BPF 程序调试与测试:verifier log、bpftool、test runner 与内核自测

文章导航

分类入口
kernelebpf
标签入口
#ebpf#debugging#verifier-log#bpftool#selftests#test_progs#bpf_printk#production#linux-kernel

目录

你的 BPF 程序通过了 clang 编译、链接了正确的 headers、看起来逻辑正确——但 BPF_PROG_LOAD 返回 -EACCES。没有 stack trace,没有 core dump,dmesg 里一行 BPF: Invalid argument。连哪行代码出问题都不知道。

这不是内核的 bug,这是 BPF 程序调试的独特挑战:你不能用 gdb attach 到内核态 BPF 程序、不能 printf()、不能 assert()、甚至不能相信”加了个 if 条件应该能优化掉”——verifier 的静态分析和编译器的 dead code elimination 是两个完全不同的东西。

本文覆盖完整的 BPF 程序调试工具箱:verifier log 的三个级别与逆向解读方法论、bpftool 从字节码反汇编到 map 运行时检查的全套命令、bpf_printk() 的临时追踪模式、BPF selftests 的结构与编写新测试、以及生产环境排障的策略矩阵。源码基于 kernel 6.6 的 tools/testing/selftests/bpf/tools/bpf/bpftool/

一、调试挑战与心智模型

1.1 BPF 程序的独特调试约束

约束 原因 替代方案
没有 debugger BPF 程序在内核上下文运行,无 ptrace 支持 verifier log + bpftool dump
没有 printf BPF 环境无 libc bpf_printk() / ring buffer / perf buffer
没有 assert BPF 不允许 abort bpf_printk() 打印预期值,用户态对比
不能 attach 到 running process BPF 不是进程 bpftool prog show 查看运行时状态
verifier 拒绝负载时不告诉你源码行 verifier 不解析 C 源码 line_info + 从底部逆读日志
访问非法内存 = load fail, not crash verifier 静态拒绝 在开发环境中将 verifier 设为低严格度,让日志包含所有拒绝路径

1.2 调试心智模型

将 BPF 程序的调试视为三阶段瀑布:

源码正确性 → verifier 验证 → 运行时行为
   (人)         (verifier)      (观测)

大多数调试时间花在阶段 2——原因不是 verifier 出错,而是程序员的直觉(“我觉得没问题”)与 verifier 的形式化规则(“这个路径上 R0 的状态包含 PTR_TO_STACK 但后续使用中要求 SCALAR_VALUE”)之间存在巨大的认知差距。理解了第 03 篇的抽象解释算法后,这个差距会大幅缩小。

二、Verifier Log:最核心的调试工具

2.1 Log Level 控制

verifier log 由 bpf_prog_load_optslog_levellog_buf 控制:

char log_buf[1024 * 1024];  // 1 MB 缓冲区(生产级程序建议 16 MB)

struct bpf_prog_load_opts opts = {
    .log_level = 1 | 2,    // 1=基本步骤, 2=每步的寄存器状态
    .log_buf = log_buf,
    .log_size = sizeof(log_buf),
};

int fd = bpf_prog_load(..., &opts);
if (fd < 0) {
    fprintf(stderr, "%s\n", log_buf);
}

Log level 的位掩码语义:

bit 含义
0 1 基本验证步骤——显示每个函数的验证边界、do_check 的主循环帧摘要、拒绝原因
1 2 每条指令后 dump 各寄存器的状态(类型、值域、堆栈偏移)——极其详细
2 4 显示 BPF 字节码(十六进制 dump)和源码行(如果有 line_info)——可选,建议始终开启
3 8 打印所有已访问状态的详细信息(路径爆炸判断的日志)

组合示例: - log_level = 1:最小日志,看”在哪条指令被拒绝,什么原因” - log_level = 1 | 2(即 3):最常用——看拒绝指令 + 该指令前的寄存器状态 - log_level = 1 | 2 | 4(即 7):完整详细信息(生产调试时的标准选择)

2.2 逆向解读 Verifier Log 的方法

拿到一个 verifier log,不要从头读——从最后一行开始,逆向追踪:

# 示例:verifier 拒绝访问 uninitialized stack
...
48: (61) r1 = *(u32 *)(r10 -8)
49: (15) if r1 == 0x0 goto pc+4
50: (71) r1 = *(u8 *)(r10 -9)    ← 这条指令被拒绝
R10=fp0 fp-8=mmmm????
R10 invalid variable-length read: memcpy size exceeds stack boundary
processed 92 insns (limit 1000000) max_states_per_insn 2 total_states 12 peak_states 12 mark_read 5

逆向步骤:

  1. 找到最后一行"R10 invalid variable-length read" —— 错误发生在 R10(帧指针)相关的变长读取;
  2. 往上看最后一条拒绝指令前的一条指令:指令 50(*(u8 *)(r10 -9))——试图读取 fp - 9 处的一个字节;
  3. 再往上看寄存器状态:查看指令 49 后 dump 的寄存器状态——fp-8=mmmm???? 表示 fp-8 处有 4 字节已写入(m),其后是未初始化的(?)。读 fp-9 触发了未初始化访问拒绝。

这个方法的理论基础是 verifier 的 do_check() 执行深度优先遍历,当它遇到不可继续的路径时立即输出拒绝原因——因此错误信息附近的上下文包含了导致拒绝的精确寄存器状态。

2.3 常见 Log 模式与根因矩阵

Log 模式 根因 解法
R1 type=scalar expected=ptr_to_ctx 程序入口 R1 被覆盖 在保存 ctx 指针到栈前不要写入 R1
R0 invalid mem access 'scalar' 从 bpf_map_lookup_elem 返回后未检查 NULL if (!val) return 0; 在解引用前
invalid variable-length read 尝试读取变长大小的字节,但栈上大小未初始化 确保所有循环/变长读的地界在 verifier 可判定的范围内
math between map_value pointer and register with unbounded min value 指针算术中添加了未限制的值 在指针算术前对变量做 if (val > 0 && val < MAX) 边界检查
back-edge from insn X to Y 循环或 goto 回跳 确保循环有编译器可分析的迭代次数上限(#pragma unrollfor (i=0;i<MAX;i++) with verifier-known MAX)
invalid indirect read from stack 尝试通过变量偏移量访问栈,偏移量未被 verifier 约束 对栈指针做 if (offset >= 0 && offset < 512) 检查
call to 'bpf_map_lookup_elem' is not GPL compatible license 节不是 “GPL” 在 BPF C 中添加 char LICENSE[] SEC("license") = "GPL";
func 'foo' not found in BTF fentry/fexit 目标函数名拼写错误或函数不是 exported 检查 /proc/kallsyms 确认函数存在且是全局可访问的

2.4 选择性寄存器 Log(Selective Register Logging)

6.5+ 引入了选择性寄存器日志——只 dump 你关心的寄存器的状态:

// log_level = 1 | 2,加上寄存器掩码
opts.log_level = (2 << 8) | (1 << 0);  
// bits [11:8] = 2 → 只 dump R0 和 R1(bit 0=R0, bit 1=R1)

这在你已经知道错误涉及某个特定寄存器时将日志从几 MB 缩减到几 KB。掩码按 (1 << reg_nr) 设置每一位——(1 << 0) = R0, (1 << 1) = R1, 依此类推。

2.5 Libbpf 中的 Verifier Log 集成

使用 skeleton 时可以这样启用 verifier log:

struct bpf_object_open_opts open_opts = {
    .kernel_log_level = 1 | 2 | 4,
    .kernel_log_buf = log_buf,
    .kernel_log_size = sizeof(log_buf),
};

skel = myprog_bpf__open_opts(&open_opts);
// ...
err = myprog_bpf__load(skel);
if (err) {
    fprintf(stderr, "BPF load failed. Verifier log:\n%s\n", log_buf);
}
// 成功时也可以打印 log——verifier 日志包含成功路径的通行信息

一个重要的调试技巧——即使加载成功,也查看 verifier log。它包含每个函数的路径总数、最大状态数、内存使用等统计数据。如果状态数突然上升(比如从 5 个增加到 500 个),即使程序通过了,也意味着 verifier 正在为你的代码做更耗时的状态遍历——可能指示潜在的复杂度问题。

三、bpftool:从反汇编到运行时检查

3.1 程序列表与显示

# 列出所有已加载的 BPF 程序
bpftool prog list

# 查看特定程序的详细信息
bpftool prog show id 42

# 输出示例(经删减):
# 42: xdp  name xdp_filter  tag 7cd8e7e8f9e8d7c6
#     loaded_at 2026-06-12T10:30:00+0000  uid 0
#     xlated 248B  jited 451B  memlock 4096B  map_ids 12,13
#     btf_id 22
#     pids my_loader(1234)

其中 tag 是一个 8 字节 SHA 哈希的十六进制表示——相同字节码的 BPF 程序有相同的 tag,用于跨系统识别同一程序。

3.2 反汇编:xlated 模式

bpftool prog dump xlated id <id> 是调试的核心命令。它将 BPF 字节码反汇编为可读的指令文本:

bpftool prog dump xlated id 42

输出示例(经删减):

   0: (bf) r6 = r1                     ; r6 = ctx
   1: (b7) r1 = 0
   2: (6b) *(u16 *)(r10 -4) = r1       ; *(u16*)(fp-4) = 0
   3: (b7) r1 = 0
   4: (73) *(u8 *)(r10 -2) = r1        ; *(u8*)(fp-2) = 0
   5: (18) r1 = map[id:12]             ; load map pointer
   7: (bf) r2 = r10
   8: (07) r2 += -4                    ; r2 = fp-4 → pointer to key
   9: (85) call bpf_map_lookup_elem#1
  10: (15) if r0 == 0x0 goto pc+2
  11: (b7) r1 = 1                      ; r1 = 1
  12: (73) *(u8 *)(r0 +0) = r1         ; *val = 1
  13: (b7) r0 = 0
  14: (95) exit

关键选项组合:

# 显示操作码(十六进制编码)——帮助验证 LLVM 生成的编码是否正确
bpftool prog dump xlated id 42 opcodes

# 显示源码行(inline with BPF 指令)——从 .BTF.ext 的 line_info 读取
bpftool prog dump xlated id 42 linum

# 输出:
# ; u32 key = 1;
#    2: (b7) r1 = 1
#    3: (63) *(u32 *)(r10 -4) = r1
# ; val = bpf_map_lookup_elem(&my_map, &key);
#    4: (18) r1 = map[id:12]
#    6: (bf) r2 = r10
#    7: (07) r2 += -4
#    8: (85) call bpf_map_lookup_elem#1
# ; if (!val) return 0;
#    9: (15) if r0 == 0x0 goto pc+2
# ; *val = 1;
#   10: (b7) r1 = 1
#   11: (63) *(u32 *)(r0 +0) = r1

源码行对照功能在 BPF 程序编译时必须使用 -g 参数——否则 .BTF.ext 中不会有 line_info,反汇编时看不到源码行。

3.3 反汇编:jited 模式

JIT 编译后的本机码反汇编:

bpftool prog dump jited id 42

x86-64 输出示例(经删减):

   0:   nopl   0x0(%rax,%rax,1)
   5:   push   %rbp
   6:   mov    %rsp,%rbp
   9:   sub    $0x40,%rsp
  10:   xor    %eax,%eax
  12:   mov    %rdi,%rbx            ; r6 = ctx
  15:   xor    %edi,%edi            ; r1 = 0
  17:   mov    %di,-0x4(%rbp)       ; *(u16*)(fp-4) = 0
  1b:   xor    %edi,%edi
  1d:   mov    %di,-0x2(%rbp)       ; *(u8*)(fp-2) = 0
  ...

对比 xlated 和 jited 输出可以检查 JIT 的翻译质量。例如,查看是否出现了不必要的 spill/fill(因寄存器分配不足导致的栈溢出恢复),或者是否有指令序列可以合并但 JIT 没有合并。更多 JIT 内部细节见第 05 篇。

3.4 Map 运行时检查

# Map 元数据
bpftool map show id 12
# Output:
# 12: hash  name conn_map  flags 0x0
#     key 4B  value 8B  max_entries 10240  memlock 90112B

# Dump 所有 key-value 对(只适用于支持 get_next_key 的 map)
bpftool map dump id 12

# 按 key 精确查找
bpftool map lookup id 12 key 0x01 0x00 0x00 0x00

# 按 key 更新
bpftool map update id 12 key 0x01 0x00 0x00 0x00 value 0xff 0x00 0x00 0x00 0x00 0x00 0x00 0x00

# 删除 key
bpftool map delete id 12 key 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00

Dump 的局限性: - BPF_MAP_TYPE_RINGBUFBPF_MAP_TYPE_PERF_EVENT_ARRAYBPF_MAP_TYPE_PROG_ARRAY 等不支持 get_next_key 迭代,不能 dump; - hash map 的 dump 不保证顺序,且在大容量 map 中可能很慢; - 如果 key/value 是结构体,dump 输出是纯 hex,没有结构体格式化。使用 bpftool map dump id 12 -p(pretty-print with BTF)来获取按字段标记的格式化输出(需要 BTF)。

3.5 BTF 查阅

# 查看 BTF ID
bpftool btf show id 1

# 查看在 BTF 中查找类型
bpftool btf dump id 1 | grep -A 30 'STRUCT.*task_struct'

# 从程序获取其关联的 BTF
bpftool prog show id 42 --json | jq '.btf_id'
bpftool btf dump id <btf_id> format c | head -100

# 查看 line_info 记录——确认源码对应关系
bpftool btf dump id <btf_id> | grep 'FUNC.*my_function'
# 列出所有 bpf_links
bpftool link list

# 查看特定 link
bpftool link show id 42
# Output(经删减):
# 42: tracing  prog 43
#     bpf_cookie 0
#     pids test_runner(1234)

# 列出程序、map、link 之间的对应关系
bpftool prog show id 43

bpf_link 的状态是判断”程序是否真的 attach 到预期的钩子上”的关键——程序加载成功+attach 调用返回 0 不等于程序正在运行。如果是 cgroup 钩子,需要确认 cgroup 路径正确、层级权限匹配;如果是 tracepoint,需要确认 tracepoint 名称拼写和子系统正确。

四、bpf_printk():最直接的运行时追踪

4.1 使用

bpf_printk() 是 BPF 的 printf——将格式化字符串写入内核 trace buffer:

#include <bpf/bpf_helpers.h>

SEC("kprobe/tcp_connect")
int kprobe_tcp_connect(struct pt_regs *ctx)
{
    bpf_printk("tcp_connect called from pid=%d\n", bpf_get_current_pid_tgid() >> 32);
    return 0;
}

输出通过 trace_pipe 读取(需要 mount debugfs):

mount -t debugfs none /sys/kernel/debug
cat /sys/kernel/debug/tracing/trace_pipe

限制: - 最多 3 个格式化参数(除了格式字符串) - 格式字符串最大 512 字节 - 全局共享的 trace buffer——多个程序同时调用 bpf_printk() 时输出混合 - 有显著的性能开销(每次调用都要格式化并写入全局 buffer) - 生产环境中不应使用——trace_pipe 是为调试设计的,不是事件通道

4.2 替代方案:Ring Buffer / Perf Buffer

对于生产环境的结构化事件输出,使用 ring buffer(BPF_MAP_TYPE_RINGBUF)或 perf buffer。libbpf 的 ring_buffer__new()(第 10 篇详述)提供了高性能的 mmap 消费者模式。

// BPF 端
struct event {
    u32 pid;
    char comm[16];
    u64 timestamp;
};

struct {
    __uint(type, BPF_MAP_TYPE_RINGBUF);
    __uint(max_entries, 256 * 1024);
} events SEC(".maps");

SEC("kprobe/tcp_connect")
int kprobe_tcp_connect(struct pt_regs *ctx)
{
    struct event *e = bpf_ringbuf_reserve(&events, sizeof(*e), 0);
    if (!e) return 0;

    e->pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&e->comm, sizeof(e->comm));
    e->timestamp = bpf_ktime_get_ns();

    bpf_ringbuf_submit(e, 0);
    return 0;
}

对于纯调试目的(在开发环境快速验证程序逻辑),bpf_printk() 仍然是最快路径——一行代码、无需设置 ring buffer consumer、实时反馈。

4.3 bpf_snprintf():格式化到缓冲区

5.10+ 引入了 bpf_snprintf(),允许 BPF 程序将格式化字符串写入 map value 或栈缓冲区,然后通过 ring buffer 输出:

// 将格式化字符串写入 map value,然后通过 ring buffer 输出
char msg[128];
bpf_snprintf(msg, sizeof(msg), "pid=%d duration=%llu ns", pid, duration_ns);
// 然后通过 ring buffer 提交 msg

这比 bpf_printk() 更灵活——因为它允许你把格式化结果填充到任意内存区域,然后通过自己的事件通道输出。

五、BPF Selftests:内核自测系统

5.1 结构与组织

BPF selftests 是内核源码树中的完整测试框架,位于 tools/testing/selftests/bpf/。它是 BPF 子系统的回归测试和负向测试的主阵地——内核 BPF 补丁合并前必须在 selftests 中通过所有现有测试或需要新测试。

目录结构:

tools/testing/selftests/bpf/
├── test_progs.c          # 测试运行器主程序(编译为 ./test_progs)
├── test_maps.c           # map 专项测试(编译为 ./test_maps)
├── test_verifier.c       # verifier 边界条件测试(编译为 ./test_verifier)
├── prog_tests/           # 用户态测试程序(每个 .c 文件是一个测试模块)
   ├── xdp_attach.c      # XDP attach 测试
   ├── bpf_cookie.c      # BPF cookie 测试
   ├── core_reloc.c      # CO-RE 重定位测试
   ├── ringbuf.c         # ring buffer 测试
   └── ...
├── progs/                # BPF 程序文件(测试用的 .bpf.c 文件)
   ├── test_xdp.bpf.c
   ├── test_ringbuf.bpf.c
   └── ...
├── bpf_testmod/          # 用于集成测试的内核模块
├── Makefile
└── config                # 内核 .config 要求

5.2 编译与运行

# 构建所有 selftests
cd tools/testing/selftests/bpf
make -j$(nproc)

# 运行所有测试
./test_progs

# 运行特定测试
./test_progs -t xdp_attach
./test_progs -t core_reloc

# 运行匹配模式的测试
./test_progs -t xdp        # 运行所有包含 "xdp" 的测试

# 带 verifier log 运行(调试用)
./test_progs -t xdp_attach -v

# 列出所有可用测试
./test_progs -l

5.3 测试模式与编写

一个典型的 BPF selftest 由两部分组成:一个 BPF 程序(progs/test_foo.bpf.c)和一个用户态测试程序(prog_tests/foo.c)。

BPF 程序progs/test_foo.bpf.c):

// tools/testing/selftests/bpf/progs/test_foo.bpf.c
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>

struct {
    __uint(type, BPF_MAP_TYPE_HASH);
    __type(key, u32);
    __type(value, u64);
    __uint(max_entries, 1);
} result_map SEC(".maps");

SEC("xdp")
int test_xdp_prog(struct xdp_md *ctx)
{
    u32 key = 0;
    u64 val = 1;
    bpf_map_update_elem(&result_map, &key, &val, BPF_ANY);
    return XDP_PASS;
}

char LICENSE[] SEC("license") = "GPL";

用户态测试prog_tests/foo.c):

// tools/testing/selftests/bpf/prog_tests/foo.c
#include <test_progs.h>

void test_foo(void)
{
    struct test_foo_bpf *skel;
    int err;

    // 打开和加载 BPF skeleton
    skel = test_foo_bpf__open_and_load();
    if (!ASSERT_OK_PTR(skel, "test_foo_bpf__open_and_load"))
        return;

    // Attach
    // (XDP attach 需要实际的网络接口,这里简化示例)
    // ERR = test_foo_bpf__attach(skel);

    // 验证结果
    u32 key = 0;
    u64 val;

    err = bpf_map_lookup_elem(bpf_map__fd(skel->maps.result_map), &key, &val);
    if (!ASSERT_OK(err, "map_lookup"))
        goto cleanup;

    ASSERT_EQ(val, 1, "result_value");

cleanup:
    test_foo_bpf__destroy(skel);
}

常用测试宏(定义在 test_progs.h):

语义
ASSERT_OK(err, name) 检查 err == 0,失败则输出 name 和 err 值
ASSERT_OK_PTR(ptr, name) 检查 ptr != NULL,失败则输出 name
ASSERT_EQ(actual, expected, name) 检查 actual == expected
ASSERT_NEQ(actual, expected, name) 检查 actual != expected
ASSERT_GT(a, b, name) 检查 a > b
ASSERT_STREQ(s1, s2, name) 字符串相等
ASSERT_HAS_SUBSTR(s, sub, name) s 包含子串 sub
CHECK(cond, name, fmt, ...) 旧式检查,cond 为 true 时输出错误

5.4 test_verifier:Verifier 行为测试

test_verifier.c 是一个特殊的测试——它不加载实际程序,而是生成合成 BPF 指令序列,验证 verifier 对特定边界条件的行为:

/* tools/testing/selftests/bpf/test_verifier.c 中的测试条目 */
{
    "store uninit stack",
    .insns = {
        BPF_ST_MEM(BPF_DW, BPF_REG_10, -8, 0),  // 未初始化 stack 就读取
        BPF_LDX_MEM(BPF_DW, BPF_REG_0, BPF_REG_10, -16),
        BPF_EXIT_INSN(),
    },
    .result = REJECT,
    .errstr = "invalid read from stack",
},

每个测试指定: - .insns:BPF 指令序列(使用宏 BPF_ST_MEM 等构造) - .result:期望结果(REJECTACCEPT) - .errstr:如果期望 REJECT,verifier log 中应该包含的错误子串 - .prog_type:测试的程序类型(如 BPF_PROG_TYPE_XDP) - .fixup_map_*:map fd 的 fixup 标记

test_verifier 包含数千个边界条件测试——这是确认 verifier 升级后行为兼容性的核心。

5.5 test_progs 与 test_maps 的区别

test_progs test_maps
测试范围 程序加载、attach、功能正确性 Map 操作:create/update/lookup/delete/iterate
测试模式 用户态程序 + BPF 程序(skeleton 模式) 纯用户态程序(直接调用 bpf() syscall)
测试粒度 功能级(加载→attach→验证) API 级(参数验证、边界条件、权限)

更推荐新测试加入 test_progs + prog_tests/ + progs/ 模式——因为它模拟了真实的使用方式(libbpf + skeleton),与 bpftool gen skeleton 生成的头文件保持同步。

六、生产环境下的 BPF 排障

6.1 排障流程

Program fails to load?
├── Yes → Check verifier log (enable log_level=1|2|4)
│         ├── R1 type=scalar → ctx pointer was clobbered → save ctx first
│         ├── invalid mem access → add NULL check
│         ├── variable-length read → bound check the size parameter
│         └── unknown → compare with bpftool dump xlated of working version
└── No → Program loads but doesn't fire?
    ├── Yes → Check attach type and parameters
    │         ├── bpftool link list (is the bpf_link created?)
    │         ├── bpftool prog show (is the program in expected type?)
    │         └── For tracing: is the target function being called?
    │             (cat /proc/kallsyms | grep target_func)
    └── No → Program fires but produces wrong data?
        └── Yes → Insert bpf_printk() (dev) or ring buffer event at key points
                  │ → bpftool map dump (are maps populated?)
                  │ → bpftool prog dump xlated linum (is compiled code correct?)
                  └── → Recheck verifier optimizations: does verifier optimize away
                        branches that SHOULD execute (verifier precision loss)?

6.2 bpftool 在生产中的最小依赖

bpftool 是一个静态链接的二进制文件(可由 make -C tools/bpf/bpftool static 生成),依赖只有 libelflibzlibcap。它可以在目标机器上检查所有内核 BPF 状态而不需要其他工具:

# 一键获取系统上所有 BPF 状态
bpftool prog list -p > /tmp/bpf_progs.json
bpftool map list -p > /tmp/bpf_maps.json
bpftool link list -p > /tmp/bpf_links.json

JSON 输出(-p = --pretty)适合脚本批处理,在生产环境中可以用脚本来判断”是否这个程序还在运行、map 大小是否在增长”。

6.3 BPF 程序统计

如果启用了 CONFIG_BPF_STATSbpftool prog show 会显示每个程序的运行计数和运行时间:

bpftool prog show id 42
# 输出包含:
#     run_time_ns 12456789012  run_cnt 9876543

这些统计数据对于判断”程序是否真的在运行”和”运行时是否有突增”具有决定性的证据价值——不用推测,直接看计数器。

6.4 通过 /proc 文件系统查看进程的 BPF 状态

/proc/<pid>/fdinfo/<bpf_fd> 提供特定进程持有的 BPF fd 的详细信息:

ls -l /proc/$(pidof my_loader)/fd | grep bpf-prog
cat /proc/$(pidof my_loader)/fdinfo/42

# 输出:
# pos:    0
# flags:  02000002
# mnt_id: 15
# ino:    12345
# prog_type:    6          ← BPF_PROG_TYPE_TRACING
# prog_jited:   1          ← JIT 编译
# prog_tag:     7cd8e7e8f9e8d7c6
# memlock:      4096
# map_ids:      12,13
# btf_id:       22

6.5 bpftrace 作为动态调试器

对于”我的程序为什么 attach 不上去”或”attach 后的第一个回调是什么”这类问题,用 bpftrace 直接追踪 BPF 子系统内部的函数调用比看日志快得多。以下 probe 名称与参数以目标内核 /proc/kallsyms 为准——不同版本符号名可能不同:

# 追踪 bpf_prog_load 的调用
bpftrace -e 'kprobe:bpf_prog_load { printf("prog load: name=%s type=%d\n", str(arg1), arg2); }'

# 追踪 bpf_link_create 的调用(6.1+)
bpftrace -e 'kprobe:bpf_link_prime { printf("link: prog=%d type=%d\n", arg1, arg2); }'

# 追踪 verifier 拒绝原因
bpftrace -e 'kprobe:do_check { @check_start[tid] = nsecs; }
             kretprobe:do_check /@check_start[tid]/ { @verifier_time = hist(nsecs - @check_start[tid]); delete(@check_start[tid]); }'

# 追踪 helper 调用(哪个 helper 被调用最频繁)
bpftrace -e 'kprobe:__bpf_call_base { @helpers[arg2] = count(); }'

bpftrace 不需要加载任何 BPF 程序到生产路径——它使用内核自带的 kprobe 框架,追踪 BPF 子系统自身的行为。

七、调试工具速查表

调试场景 工具 命令/方法
程序加载失败 verifier log + bpftool log_level=1\|2\|4 + 从底部逆读日志
查看 BPF 字节码 bpftool bpftool prog dump xlated id <id> linum opcodes
查看 JIT 编译结果 bpftool bpftool prog dump jited id <id>
运行时数据检查 bpftool map bpftool map dump id <id>
临时追踪打印 bpf_printk cat /sys/kernel/debug/tracing/trace_pipe
结构化事件输出 ring buffer + libbpf consumer ring_buffer__new() + ring_buffer__poll()
程序是否正确 attach bpftool link bpftool link list + bpftool prog show
程序运行统计数据 bpftool prog stats bpftool prog show id <id> (需要 CONFIG_BPF_STATS)
编写自动化测试 test_progs prog_tests/ + progs/ 模式
verifier 行为回归 test_verifier 合成指令序列 + 预期 REJECT/ACCEPT
BTF 类型查阅 bpftool btf bpftool btf dump id <id> format c
进程 BPF 状态 /proc/fdinfo cat /proc/<pid>/fdinfo/<bpf_fd>
内核 BPF 追踪 bpftrace kprobe 在 BPF 子系统关键函数上

八、小结

BPF 程序调试不能用传统工具,但有一套不比 gdb + printf 差的工具矩阵:verifier log(理解 verifier 的拒绝原因)、bpftool(反汇编和运行时检查)、bpf_printk() + ring buffer(运行时输出)、BPF selftests(回归和边界条件)。核心习惯是从 verifier log 的尾部开始逆读——逆向追踪法比从头读快 10 倍。生产环境中 bpftool 静态二进制 + /proc/fdinfo 提供所有 BPF 状态的只读检查。

下一篇进入 BPF 蹦床机制(Trampoline)——fentry/fexit 如何通过蹦床实现”零开销”内核函数追踪,以及它与传统 kprobe 的性能差异来源。


参考

  1. Linux 内核源码 kernel/bpf/verifier.c(kernel 6.6):do_check()bpf_verifier_log_write() verifier 日志实现
  2. Linux 内核源码 tools/testing/selftests/bpf/(kernel 6.6):BPF selftests 框架
  3. Linux 内核源码 tools/testing/selftests/bpf/test_verifier.c:verifier 行为测试用例
  4. Linux 内核源码 tools/bpf/bpftool/:bpftool 所有子命令实现
  5. Linux 内核文档 Documentation/bpf/verifier.rst:verifier 设计文档
  6. Linux 内核文档 Documentation/bpf/btf.rst:BTF 在工具链中的使用

上一篇BPF 编译工具链(第 13 篇)

下一篇蹦床与 fentry/fexit(第 15 篇)

同主题继续阅读

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


By .