你的 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) (观测)
- 阶段 1:程序逻辑是否正确——通过阅读、reviews、静态分析;
- 阶段 2:verifier 为什么拒绝——通过
log_level +
bpftool prog dump xlated定位拒绝指令; - 阶段 3:运行时行为是否符合预期——通过
bpftool map dump、bpf_printk()、ring buffer、bpftool prog show观测。
大多数调试时间花在阶段 2——原因不是 verifier 出错,而是程序员的直觉(“我觉得没问题”)与 verifier 的形式化规则(“这个路径上 R0 的状态包含 PTR_TO_STACK 但后续使用中要求 SCALAR_VALUE”)之间存在巨大的认知差距。理解了第 03 篇的抽象解释算法后,这个差距会大幅缩小。
二、Verifier Log:最核心的调试工具
2.1 Log Level 控制
verifier log 由 bpf_prog_load_opts 的
log_level 和 log_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
逆向步骤:
- 找到最后一行:
"R10 invalid variable-length read"—— 错误发生在 R10(帧指针)相关的变长读取; - 往上看最后一条拒绝指令前的一条指令:指令
50(
*(u8 *)(r10 -9))——试图读取fp - 9处的一个字节; - 再往上看寄存器状态:查看指令 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 unroll
或 for (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 42x86-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 0x00Dump 的局限性: -
BPF_MAP_TYPE_RINGBUF、BPF_MAP_TYPE_PERF_EVENT_ARRAY、BPF_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'3.6 Link 运行状态
# 列出所有 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 43bpf_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 -l5.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:期望结果(REJECT 或
ACCEPT) - .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
生成),依赖只有 libelf、libz 和
libcap。它可以在目标机器上检查所有内核 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.jsonJSON 输出(-p =
--pretty)适合脚本批处理,在生产环境中可以用脚本来判断”是否这个程序还在运行、map
大小是否在增长”。
6.3 BPF 程序统计
如果启用了
CONFIG_BPF_STATS,bpftool 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: 226.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 的性能差异来源。
参考
- Linux 内核源码
kernel/bpf/verifier.c(kernel 6.6):do_check()、bpf_verifier_log_write()verifier 日志实现 - Linux 内核源码
tools/testing/selftests/bpf/(kernel 6.6):BPF selftests 框架 - Linux 内核源码
tools/testing/selftests/bpf/test_verifier.c:verifier 行为测试用例 - Linux 内核源码
tools/bpf/bpftool/:bpftool 所有子命令实现 - Linux 内核文档
Documentation/bpf/verifier.rst:verifier 设计文档 - Linux 内核文档
Documentation/bpf/btf.rst:BTF 在工具链中的使用
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【eBPF 内核实现深度拆解】BPF 指令集解码:寄存器机器、调用约定与指令编码
从 eBPF 虚拟机的 11 个 64-bit 寄存器和 struct bpf_insn 出发,逐条拆解 ALU64/ALU32、跳转、加载存储、call 四类指令的字段语义与编码格式,建立后续 verifier 和 JIT 讨论的精确基础。
【eBPF 内核实现深度拆解】验证器框架:从 BPF_PROG_LOAD 到 do_check()
跟踪 BPF_PROG_LOAD 系统调用的内核执行路径,逐层拆解 bpf_prog_load()→bpf_check()→do_check_main() 的调用链,建立 verifier 执行全景——这是理解 verifier 安全保证的入口。
【eBPF 内核实现深度拆解】与验证器共舞:常见拒绝模式与编程约束
把 02-03 的理论落地为可操作的编程指南——unbounded memory access、variable-length read、pointer arithmetic on scalar、循环上界推断失败、helper 参数类型不匹配等 18 种常见 verifier 错误的根因分析、最小复现与正确写法。
【eBPF 内核实现深度拆解】JIT 编译器后端:x86-64 与 ARM64 的 BPF→Native 翻译管线
从 bpf_jit_compile() 入口出发,拆解 BPF 字节码到 x86-64/ARM64 本地指令的翻译过程——寄存器映射策略、ALU 指令的 one-to-one/many-to-one 翻译、尾调用与 call 的本地实现、JIT 镜像的 kallsyms 集成,以及 JIT 与 interpreter 的性能边界。