BPF_PROG_LOAD 系统调用深度剖析——从用户态到内核的指令流转之旅
作者:ybb | 日期:2026年10月8日 | 分类:Linux 内核 / eBPF
1. 概述:BPF 加载全景
2. BPF 加载接口的演进史
3. bpf() 系统调用入口
4. libbpf:现代 BPF 加载的标准库
5. 内核入口:SYSCALL_DEFINE3(bpf)
6. BPF_PROG_LOAD 核心流程详解
7. Map 创建与指令修复
8. JIT 编译与优化
9. 程序附加(Attach)机制
10. BTF 与 CO-RE:可移植性的基石
11. BPF Token 与权限模型
12. 调试与故障诊断
13. 完整实战示例
14. 总结
1. 概述:BPF 加载全景
Berkeley Packet Filter(BPF)已经从当年那个仅用于网络包过滤的简易虚拟机,演变为当今 Linux 内核中最强大的可编程基础设施。无论是网络(XDP)、可追踪性(kprobes/tracepoints)、安全(seccomp/Landlock),还是资源调度(sched-ext),BPF 都在以一种前所未有的方式改变着我们与内核交互的模式。
然而,这一切的基础是一个看似简单的操作——将 BPF 程序加载到内核中。这个操作通过 bpf() 系统调用、使用 BPF_PROG_LOAD 命令来完成。从用户态的 libbpf 调用开始,BPF 字节码经历了验证、修复、JIT 编译、文件描述符分配等一系列复杂的内核处理步骤,最终变成一个可以附加到内核钩子的可执行实体。
本文将完整追踪这一流程,深入到内核源码层面,揭示从 bpf(BPF_PROG_LOAD, ...) 发出那一刻起,到返回文件描述符为止所发生的一切。与本系列上一篇讨论 eBPF Verifier 的文章互补,本文关注的是加载链路的全貌。
2. BPF 加载接口的演进史
理解 BPF_PROG_LOAD 需要先回顾 BPF 加载接口的历史变迁。
2.1 经典 BPF(cBPF)时代
在 Linux 2.1.75(1997年)引入 BPF 时,加载 BPF 程序的方式非常简单:对网络套接字使用 setsockopt(SO_ATTACH_FILTER, ...)。此时的 BPF 指令集极为精简——仅有 32 位操作、两个寄存器(A 和 X),以及有限的跳转指令。程序在套接字层面即时解释执行,没有验证器、没有 JIT。
2.2 eBPF 的诞生
2014 年,Alexei Starovoitov 引入了扩展 BPF(eBPF)。关键变化包括:64 位寄存器(10 个通用寄存器 R0-R9 + 栈指针 R10)、更丰富的指令集、以及引入了验证器(Verifier)来保证程序安全性。
起初,eBPF 程序的加载仍然通过 setsockopt 完成,这带来了严重的设计问题:
- 套接字语义与 BPF 程序类型紧耦合,无法复用 BPF 程序到其他场景
- 缺乏独立的 BPF 对象管理
- 难以支持 Map 等共享状态结构
2.3 bpf() 系统调用
Linux 3.18(2014 年 12 月)引入了专用的 bpf() 系统调用,最初仅支持 BPF_MAP_CREATE 和 BPF_PROG_LOAD 两个命令。这标志着 BPF 开始作为独立于套接字的内核抽象存在。
随后,系统调用不断扩展,新增了数十个 bpf_cmd:
// include/uapi/linux/bpf.h 中的部分 BPF 命令
enum bpf_cmd {
BPF_MAP_CREATE, // 创建 BPF Map
BPF_MAP_LOOKUP_ELEM, // 查找 Map 元素
BPF_MAP_UPDATE_ELEM, // 更新 Map 元素
BPF_MAP_DELETE_ELEM, // 删除 Map 元素
BPF_MAP_GET_NEXT_KEY, // 获取下一个键
BPF_PROG_LOAD, // 加载 BPF 程序
BPF_OBJ_PIN, // 固定 BPF 对象到伪文件系统
BPF_OBJ_GET, // 通过路径获取 BPF 对象
BPF_PROG_ATTACH, // 附加 BPF 程序
BPF_PROG_DETACH, // 分离 BPF 程序
BPF_PROG_TEST_RUN, // 测试运行(bpftool prog run)
BPF_PROG_GET_NEXT_ID, // 获取下一个程序 ID
BPF_MAP_GET_NEXT_ID, // 获取下一个 Map ID
BPF_PROG_GET_FD_BY_ID, // 通过 ID 获取 FD
BPF_MAP_GET_FD_BY_ID, // 通过 ID 获取 FD
BPF_OBJ_GET_INFO_BY_FD, // 通过 FD 获取详细信息
BPF_PROG_QUERY, // 查询已附加的程序
BPF_RAW_TRACEPOINT_OPEN, // 打开裸 tracepoint
BPF_BTF_LOAD, // 加载 BTF 信息
BPF_BTF_GET_FD_BY_ID, // 通过 ID 获取 BTF FD
BPF_TASK_FD_QUERY, // 查询任务 FD
BPF_MAP_LOOKUP_AND_DELETE_ELEM, // 原子查找并删除
BPF_MAP_FREEZE, // 冻结 Map
BPF_BTF_GET_NEXT_ID, // 获取下一个 BTF ID
BPF_MAP_LOOKUP_BATCH, // 批量查找
BPF_MAP_LOOKUP_AND_DELETE_BATCH,
BPF_MAP_UPDATE_BATCH, // 批量更新
BPF_MAP_DELETE_BATCH, // 批量删除
BPF_LINK_CREATE, // 创建 BPF Link(替代 PROG_ATTACH)
BPF_LINK_UPDATE, // 更新 BPF Link
BPF_LINK_GET_FD_BY_ID,
BPF_LINK_GET_NEXT_ID,
BPF_ENABLE_STATS, // 启用统计
BPF_ITER_CREATE, // 创建迭代器
BPF_LINK_DETACH,
BPF_PROG_BIND_MAP, // 绑定程序到 Map
BPF_TOKEN_CREATE, // 创建 BPF Token
};
3. bpf() 系统调用入口
bpf() 系统调用的函数签名如下:
// include/linux/syscalls.h
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr *, attr, unsigned int, size)
三个参数的含义:
- cmd:指定要执行的命令,如
BPF_PROG_LOAD、BPF_MAP_CREATE等 - attr:指向
union bpf_attr的指针,这是一个巨大的联合体,不同命令使用不同字段 - size:attr 结构的大小,用于向前/向后兼容
size 参数的存在是 Linux 内核 ABI 兼容性的精妙体现。当内核新增字段到 bpf_attr 时,旧用户态程序传入的 size 较小,内核忽略新字段;新用户态程序传入更大的 size,内核填充新增字段。这使得 libbpf 可以跨内核版本工作。
3.1 调用链路概览
用户态到内核态的完整调用路径:
用户态: bpf() / bpf_prog_load()
↓ glibc wrapper
syscall instruction (x86) / svc #0 (ARM64)
↓
SYSCALL_DEFINE3(bpf, ...) [kernel/bpf/syscall.c]
↓
switch (cmd) {
case BPF_PROG_LOAD:
bpf_prog_load(attr, size);
case BPF_MAP_CREATE:
bpf_map_create(attr, ...);
...
}
4. libbpf:现代 BPF 加载的标准库
直接调用 bpf() 系统调用需要填充巨大的 union bpf_attr,实际上非常不便。libbpf(位于内核源码 tools/lib/bpf/)是官方维护的用户态库,封装了所有 BPF 操作,是现代 BPF 应用的事实标准。
4.1 核心 API
libbpf 主要为 BPF 程序加载提供了两层接口:
高层接口(Skeleton):通过 bpftool gen skeleton 从 BPF 目标文件(.bpf.o)生成 C 头文件,提供类型安全的 API:
struct my_prog *skel = my_prog__open();
my_prog__load(skel); // 自动加载程序和 Map
my_prog__attach(skel); // 自动附加到钩子
my_prog__destroy(skel); // 清理资源
底层接口(直接加载):通过 bpf_prog_load() 系列函数手动控制加载过程:
// 最简单的 BPF 程序加载流程
struct bpf_object *obj = bpf_object__open_file("my_prog.bpf.o", NULL);
bpf_object__load(obj); // 内部调用 BPF_PROG_LOAD 对所有程序
struct bpf_program *prog = bpf_object__find_program_by_name(obj, "xdp_main");
int prog_fd = bpf_program__fd(prog); // 获取文件描述符
// 然后附加...
4.2 自动加载 vs 手动控制
libbpf 的 bpf_object__open_mem() 和 bpf_object__open_file() 做了大量准备工作:
- ELF 解析:从 ELF 文件中提取 BPF 程序、Map 定义、BTF 数据、重定位信息
- Probe-Based 适配:根据目标内核版本,选择兼容的函数集
- License 检查:提取
__license符号并与 GPL 兼容许可证进行匹配 - BTF 提取:加载自定义 BTF(BPF Type Format)用于 CO-RE(Compile Once, Run Everywhere)
5. 内核入口:SYSCALL_DEFINE3(bpf)
深入内核源码,bpf() 系统调用的实现在 kernel/bpf/syscall.c 中。入口处理逻辑相当简洁——一个巨大的 switch 语句:
// kernel/bpf/syscall.c
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr *, attr, unsigned int, size)
{
bpf_enable_stats();
switch (cmd) {
case BPF_MAP_CREATE:
err = bpf_map_create(attr); break;
case BPF_PROG_LOAD:
err = bpf_prog_load(attr, size); break;
case BPF_LINK_CREATE:
err = bpf_link_create(attr, size); break;
case BPF_BTF_LOAD:
err = bpf_btf_load(attr, size); break;
case BPF_PROG_ATTACH:
err = bpf_prog_attach(attr); break;
default:
err = -EINVAL;
}
return err;
}
每个命令的处理前都会进行能力检查(capability check)。对于 BPF_PROG_LOAD,默认要求 CAP_BPF 能力(Linux 5.8+),在此之前要求 CAP_SYS_ADMIN。随着 BPF Token 的引入,权限模型变得更加灵活。
6. BPF_PROG_LOAD 核心流程详解
bpf_prog_load() 是整个 BPF 加载流程中最复杂的函数,它执行以下关键步骤:
6.1 许可证验证
BPF 程序在内核中运行的是 GPL 许可证的代码。内核会检查 BPF 程序声明的许可证是否与 GPL 兼容:
// kernel/bpf/syscall.c
static int bpf_prog_load(union bpf_attr *attr, unsigned int size)
{
enum bpf_prog_type type = attr->prog_type;
struct bpf_prog *prog;
// 1. 检查 BPF token 权限
// 2. 验证程序类型的特权要求
// 3. 许可证检查
if (!bpf_prog_is_gpl_compatible(license)) {
// 仅 GPL 兼容许可证的程序才能访问 GPL-only 辅助函数
}
}
许可证检查不在早期拒绝非 GPL 程序,而是标记一个标志。在后续验证器阶段,如果程序调用了 GPL-only 辅助函数但声明的许可证不兼容,才会被拒绝。这种设计允许非 GPL 程序(如 BSD 许可证)使用纯 MIT 兼容的辅助函数子集。
6.2 程序类型分发
根据 prog_type,加载流程会调用类型特定的预处理回调。每种 BPF 程序类型都有独特的限制和行为:
| 程序类型 | 特权要求 | 典型用途 |
|---|---|---|
| BPF_PROG_TYPE_SOCKET_FILTER | 无特权(需调整 RLIMIT_MEMLOCK) | 包过滤 |
| BPF_PROG_TYPE_KPROBE | CAP_BPF | 内核函数追踪 |
| BPF_PROG_TYPE_XDP | CAP_BPF + NET_ADMIN | 高性能网络 |
| BPF_PROG_TYPE_SCHED_CLS | CAP_BPF + NET_ADMIN | 流量控制 |
| BPF_PROG_TYPE_TRACEPOINT | CAP_BPF | 静态追踪点 |
| BPF_PROG_TYPE_LSM | CAP_BPF + CAP_SYS_ADMIN | 安全钩子 |
| BPF_PROG_TYPE_STRUCT_OPS | CAP_BPF + CAP_SYS_ADMIN | 内核操作替换 |
| BPF_PROG_TYPE_TRACING | CAP_BPF + CAP_PERFMON | 高级追踪 |
每种类型通过 struct bpf_prog_ops 注册特定的操作集:
// include/linux/bpf.h
struct bpf_prog_ops {
(*gen_prologue)(struct bpf_prog *prog, ...);
(*gen_ld_abs)(struct bpf_prog *prog, ...);
(*calc_tag)(struct bpf_prog *prog, ...);
(*test_run)(struct bpf_prog *prog, ...);
(*translate)(struct bpf_insn *dst, ...);
};
6.3 指令复制与初始验证
内核首先将用户态的 BPF 指令复制到内核空间,进行基本的 sanity 检查:
// kernel/bpf/syscall.c
insns = u64_to_user_ptr(attr->insns); // 指令数组指针
insn_cnt = attr->insn_cnt; // 指令数量
// 检查指令数量上限(100 万条)
if (insn_cnt >= BPF_MAXINSNS || insn_cnt == 0)
return -EINVAL;
// 从用户态复制指令到内核的 bpf_insn 数组
prog = bpf_prog_alloc(bpf_prog_size(insn_cnt), GFP_USER);
copy_from_user(prog->insns, insns, bpf_prog_insn_size(prog));
struct bpf_insn 是内核中表示单条 BPF 指令的结构:
// include/uapi/linux/bpf.h
struct bpf_insn {
__u8 code; // 操作码(opcode)
__u8 dst_reg:4, // 目标寄存器
src_reg:4; // 源寄存器
__s16 off; // 有符号偏移
__s32 imm; // 有符号立即数
};
每条指令 8 字节,操作码包含大类(BPF_LD/BPF_ST/BPF_ALU/BPF_JMP/BPF_JMP32/BPF_LDX 等)与子类(BPF_K 表示立即数操作,BPF_X 表示寄存器操作)。
6.4 验证器入口(Verifier)
指令复制完毕后,控制权转移到验证器。这是加载流程中最耗时的步骤。与本系列上一篇讨论验证器的文章不同,我们这里关注验证结果如何被加载流程消费:
// kernel/bpf/syscall.c - bpf_prog_load 中调用验证器的部分
prog->aux->env = bpf_check(&prog, attr);
if (IS_ERR(prog->aux->env))
return PTR_ERR(prog->aux->env);
验证器返回 struct bpf_verifier_env,其中包含:
- 日志字符串(用于 verifier_log 输出)
- 状态栈的精炼信息(用于 JIT 编译时的寄存器分配)
- Map 引用信息(哪些 Map 被 BPF 程序访问)
6.5 日志与调试
BPF 加载支持详细的验证器日志。用户态传入 log_buf 和 log_size 参数,验证器填充日志以帮助调试:
// 用户态调用示例
char log_buf[65536] = {};
union bpf_attr attr = {
.prog_type = BPF_PROG_TYPE_XDP,
.insns = (__u64)(unsigned long)insns,
.insn_cnt = insn_cnt,
.license = (__u64)"GPL",
.log_buf = (__u64)(unsigned long)log_buf,
.log_size = sizeof(log_buf),
.log_level = 2, // 0=关闭, 1=错误, 2=详细信息
};
int fd = bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
if (fd < 0) {
// 失败时,log_buf 包含验证器的拒绝信息
fprintf(stderr, "Verifier rejected:\n%s\n", log_buf);
}
log_level 有三个层次:0 无日志、1 仅错误、2 完整状态追踪。
7. Map 创建与指令修复
eBPF 程序不能直接引用 Map 对象,而是通过伪加载指令(pseudo-load)引用 Map 的文件描述符。验证之后,内核需要将这些伪引用替换为实际的 Map 地址——这就是 Map Fixup 阶段。
7.1 伪加载指令
在 BPF 代码中引用 Map 的标准方式:
// eBPF 伪代码:引用文件描述符为 fd 的 Map
BPF_LD_MAP_FD(R1, fd);
// 底层 BPF 指令(128 位立即数加载)
BPF_LD_IMM64(BPF_REG_1, (__s64)fd) ; 8 字节立即数 + BPF_PSEUDO_MAP_FD 标记
内核使用 BPF_PSEUDO_MAP_FD 和 BPF_PSEUDO_MAP_VALUE 特殊常量作为立即数标记。
7.2 Fixup 流程
验证器分析过程中记录了所有 Map 引用(存储在 prog->aux->used_map_cnt 和 prog->aux->used_maps 中)。加载阶段,内核遍历所有指令,将伪加载中的 Map FD 替换为 BPF Map 的实际内核地址:
// kernel/bpf/verifier.c - fixup_bpf_calls()
static int fixup_bpf_calls(struct bpf_verifier_env *env)
{
for (i = 0; i < prog->len; i++) {
struct bpf_insn *insn = &prog->insns[i];
if (insn->code == (BPF_LD | BPF_IMM | BPF_DW)) {
if (insn->imm == BPF_PSEUDO_MAP_FD ||
insn->imm == BPF_PSEUDO_MAP_VALUE) {
// 替换为 Map 地址
insn->imm = addr_of_map;
}
}
// 其他 fixup:函数内联、尾调用优化、常量折叠等
}
}
最终,原始指令序列中被标记为伪加载的指令被直接 patch 为 Map 结构指针。这使得 BPF 程序运行时可以直接通过寄存器调用 Map 操作,无需走系统调用。
7.3 辅助函数修复
BPF 程序调用的辅助函数(如 bpf_map_lookup_elem、bpf_trace_printk)也需要修复。这些函数编号存储在 CALL 类指令的 imm 字段中,内核将它们替换为实际的函数指针:
// kernel/bpf/verifier.c — 常见的 BPF 辅助函数映射
static const struct bpf_func_proto * const
bpf_funcIDA[__BPF_FUNC_MAX_ID] = {
[BPF_FUNC_map_lookup_elem] = &bpf_map_lookup_elem_proto,
[BPF_FUNC_map_update_elem] = &bpf_map_update_elem_proto,
[BPF_FUNC_probe_read] = &bpf_probe_read_proto,
[BPF_FUNC_ktime_get_ns] = &bpf_ktime_get_ns_proto,
[BPF_FUNC_trace_printk] = &bpf_trace_printk_proto,
[BPF_FUNC_get_smp_processor_id] = &bpf_get_smp_processor_id_proto,
// ... 200+ 个辅助函数
};
8. JIT 编译与优化
验证和修复之后,BPF 指令仍然存储在 BPF 虚拟机指令集中。为获得接近原生的性能,内核会将其编译为目标架构的机器代码,即 JIT 编译。
8.1 BPF JIT 架构
Linux 内核为每个支持的架构维护独立的 BPF JIT 后端:
arch/x86/net/bpf_jit_comp.c # x86/x86_64 — 最成熟、功能最全
arch/arm64/net/bpf_jit_comp.c # ARM64 — 服务器主流架构
arch/s390/net/bpf_jit_comp.c # IBM Z — 大型机架构
arch/powerpc/net/bpf_jit_comp.c # PowerPC — Power 系列
arch/sparc/net/bpf_jit_comp.c # SPARC — 小众但完整
arch/mips/net/bpf_jit_comp.c # MIPS — 嵌入式
arch/riscv/net/bpf_jit_comp.c # RISC-V — 新兴架构
arch/loongarch/net/bpf_jit_comp.c # 龙芯 LoongArch
JIT 编译默认启用,可通过 sysctl net.core.bpf_jit_enable 控制:
cat /proc/sys/net/core/bpf_jit_enable # 查看 JIT 状态
echo 1 | sudo tee /proc/sys/net/core/bpf_jit_enable # 启用 JIT
echo 0 | sudo tee /proc/sys/net/core/bpf_jit_enable # 禁用(解释执行)
echo 2 | sudo tee /proc/sys/net/core/bpf_jit_enable # 带日志的 JIT
8.2 x86 JIT 编译流水线
x86 的 BPF JIT 是最成熟的实现,其编译流水线包括:
- 栈帧建立:分配栈空间(BPF 栈帧固定 512 字节)
- 寄存器映射:
- R0(返回值)→ RAX
- R1-R5(参数/临时)→ RDI, RSI, RDX, RCX, R8(调用者保存)
- R6-R9(被调用者保存)→ RBX, R12, R13, R14
- R10(栈指针)→ RBP(固定映射到 BPF 栈底)
- 指令翻译:逐条将 BPF 指令序列翻译为 x86 指令
- 辅助函数调用:插入对 BPF 辅助函数的
call指令 - 守卫代码优化:对于边界检查等,JIT 可省略运行时检查——这正是 BPF JIT 性能极高的关键
- 尾声代码:返回值写入 R0,恢复栈帧
由于验证器已证明程序不会越界、不会死循环,JIT 编译器可以省略大量安全检查代码。这是 BPF 程序比普通内核模块快的重要原因之一。
8.3 JIT 编译时机
JIT 编译在 bpf_prog_select_runtime() 中触发。内核通过 bpf_jit_kallsyms 参数控制是否将 JIT 编译后的函数符号注册到 kallsyms,以便 perf top 分析:
// 启用 JIT kallsyms
echo 1 | sudo tee /proc/sys/net/core/bpf_jit_kallsyms
默认情况下 BPF JIT 按需触发——在 BPF 程序首次被运行时才 JIT。对于高吞吐量场景(如 XDP),可用 BPF_F_TEST_XDP_LIVE_FRAMES 在加载时触发即时编译。
9. 程序附加(Attach)机制
BPF 程序加载成功后获得了文件描述符,但只是在内存中等待执行。要让程序真正运行,需要将其附加到具体事件源。
9.1 Attach 方式演进
方式一:BPF_PROG_ATTACH(旧式系统调用)
union bpf_attr attr = {
.target_fd = cgroup_fd,
.attach_bpf_fd = prog_fd,
.attach_type = BPF_CGROUP_INET_INGRESS,
.attach_flags = BPF_F_ALLOW_MULTI,
};
bpf(BPF_PROG_ATTACH, &attr, sizeof(attr));
方式二:BPF_LINK(现代方式)
BPF Link 将抽象为可持久化的 Link 对象:
union bpf_attr link_attr = {
.link_create = {
.prog_fd = prog_fd,
.target_fd_ifindex = ifindex, // 或 target_fd
.attach_type = BPF_XDP,
},
};
int link_fd = bpf(BPF_LINK_CREATE, &link_attr, sizeof(link_attr));
BPF Link 的优势:独立生命周期、可追溯性、原子替换(BPF_LINK_UPDATE 可无中断替换附加程序)、权限分离。
方式三:Netlink 协议(XDP 专用)
XDP 通过 Netlink 附加,网络套接字权限检查更精细,libbpf 的 xdp_attach() 走这条路。
9.2 各类型程序的附加路径
| 程序类型 | 附加方式 | 触发条件 |
|---|---|---|
| XDP | Netlink / BPF_LINK | 网卡接收数据包 |
| TC | netlink / BPF_LINK | 包调度时 |
| Kprobe | perf_event / BPF_LINK | 函数入口命中 |
| Tracepoint | perf_event / BPF_LINK | 命中静态追踪点 |
| Cgroup | BPF_PROG_ATTACH | 进程进入 cgroup |
| LSM | BPF_PROG_ATTACH | LSM 钩子调用 |
| Iter | BPF_ITER_CREATE | 迭代内核数据结构 |
10. BTF 与 CO-RE:可移植性的基石
BPF Type Format(BTF)和 CO-RE 解决了 BPF 程序跨内核版本移植的核心问题。
10.1 BTF 加载
加载 BPF 程序前,内核需先加载其关联的 BTF 数据:
union bpf_attr btf_attr = {
.btf = (__u64)(unsigned long)btf_data,
.btf_size = btf_size,
.btf_log_buf = (__u64)(unsigned long)log_buf,
.btf_log_size = sizeof(log_buf),
};
int btf_fd = bpf(BPF_BTF_LOAD, &btf_attr, sizeof(btf_attr));
BTF 是 C 类型的二进制编码,含结构体、函数签名、全局变量等完整类型信息。每个 BTF 数据的 ID 0 为特殊的 void 类型。
10.2 CO-RE 重定位
编译时描述的内核结构体布局,可能因内核版本不同而不同。CO-RE 通过记录重定位信息,在加载时根据目标内核实际 BTF 自动修正偏移:
struct bpf_relocation {
insn_idx; // 需重定位的指令索引
type_id; // 结构体类型 BTF ID
offset_name; // 字段名(如 "skb->data")
relocation_kind; // FIELD_BYTE_OFFSET / FIELD_EXISTS 等
};
libbpf 在 bpf_object__relocate() 中完成:查询目标内核 BTF → 匹配类型和字段偏移 → 写回 BPF 指令。同一个目标文件可在不同内核运行,无需重新编译。
10.3 kfunc 调用验证
BPF 调用内核函数(kfunc)时,通过 BTF 传递函数签名。内核验证调用约定与目标 ABI 一致。这解决了传统 BPF 辅助函数固定编号的局限,使 BPF 可以安全调用任意导出内核函数。
11. BPF Token 与权限模型
Linux 6.9 引入 BPF Token,解决非特权容器内 BPF 加载问题。传统 BPF 需 CAP_BPF,通常不会赋予容器。BPF Token 允许特权进程将部分 BPF 权限委托给容器。
11.1 BPF Token 工作原理
- 特权进程在用户命名空间中创建 BPF Token,指定允许的命令集
- Token 文件描述符通过 Unix domain socket 传递给非特权容器
- 容器通过 Token 创建受限的 BPF 程序,绕过 CAP_BPF 要求,但受限于 Token 能力集
11.2 Token 的能力委托
- 程序类型限制:仅允许特定 prog_type
- 命令限制:仅允许特定 bpf_cmd 子集
- 可加载指令数上限:防止资源滥用
- BTF 类型范围:限制 kfunc 调用能力
BPF Token 是让容器编排器(如 Kubernetes)在安全范围内赋予工作负载 BPF 能力的关键一步。
12. 调试与故障诊断
12.1 常见错误码
| 错误码 | 含义 | 常见原因 |
|---|---|---|
| EPERM | 无权限 | 缺少 CAP_BPF;非特权环境未启用 unprivileged BPF |
| EINVAL | 无效参数 | prog_type 不支持;insn_cnt 超限 |
| EACCES | 验证器拒绝 | 程序违反安全规则;未初始化寄存器 |
| ENOMEM | 内存不足 | 指令数过大;Map 数过多 |
| ENOENT | Map 不存在 | 伪加载引用了不存在的 Map FD |
| EFAULT | 地址错误 | 用户态 attr 指针无效 |
12.2 常用调试工具
# 查看已加载的 BPF 程序
sudo bpftool prog show
# 查看程序详情和 JIT 后的机器码
sudo bpftool prog dump xlated id 123 # 翻译后 BPF 指令
sudo bpftool prog dump jited id 123 # JIT 后 x86 代码
# 加载并附带 Map
sudo bpftool prog load my_prog.bpf.o /sys/fs/bpf/my_prog
# 查看 Map
sudo bpftool map dump id 456
# 查看 BTF
sudo bpftool btf dump prog id 123
12.3 JIT 调试隔离
怀疑 JIT 引入 bug 时可临时关闭 JIT 来隔离问题:
echo 0 | sudo tee /proc/sys/net/core/bpf_jit_enable # 使用解释器
# 测试...
echo 1 | sudo tee /proc/sys/net/core/bpf_jit_enable # 恢复
13. 完整实战示例
13.1 BPF C 代码(xdp_drop.bpf.c)
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
char _license[] SEC("license") = "GPL";
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__type(key, __u32);
__type(value, __u64);
__uint(max_entries, 1);
} pkt_count SEC(".maps");
SEC("xdp")
int xdp_drop_fn(struct xdp_md *ctx)
{
void *data_end = (void *)(long)ctx->data_end;
void *data = (void *)(long)ctx->data;
// 验证器要求边界检查
if (data + sizeof(struct ethhdr) > data_end)
return XDP_DROP;
struct ethhdr *eth = data;
// 更新包计数
__u32 key = 0;
__u64 *count = bpf_map_lookup_elem(&pkt_count, &key);
if (count)
__sync_fetch_and_add(count, 1);
// 简单策略:丢弃 ARP 包
if (eth->h_proto == __bpf_constant_htons(0x0806))
return XDP_DROP;
return XDP_PASS;
}
13.2 编译与加载流程
# 编译 BPF 程序
clang -O2 -g -target bpf -c xdp_drop.bpf.c -o xdp_drop.bpf.o
# 生成 Skeleton(含 open/load/attach/destroy 全部 API)
bpftool gen skeleton xdp_drop.bpf.o > xdp_drop.skel.h
# 直接加载(bpftool 一步完成)
sudo bpftool prog load xdp_drop.bpf.o /sys/fs/bpf/xdp_drop
# 附加到网卡
sudo bpftool net attach xdp id 123 dev eth0
13.3 使用 libbpf Skeleton API
#include "xdp_drop.skel.h"
int main(int argc, char **argv)
{
struct xdp_drop *skel = xdp_drop__open();
if (!skel) return 1;
// 内部对所有 prog + map 按序调用 BPF_PROG_LOAD / BPF_MAP_CREATE
int err = xdp_drop__load(skel);
if (err) goto cleanup;
// 自动附加到 SEC() 中声明的钩子
err = xdp_drop__attach(skel);
if (err) goto cleanup;
printf("Program attached. Press Enter to exit.\n");
getchar();
cleanup:
xdp_drop__destroy(skel);
return err != 0;
}
14. 总结
BPF_PROG_LOAD 看似一个简单的加载接口,实则涉及许可证合规、指令集验证、控制流安全证明、内存访问检查、Map 关联修复、JIT 代码生成、权限多层校验等一系列精密机制。整个加载流程可分为五个阶段:
- 预处理:许可证校验、权限检查、Capability 验证
- 验证:验证器静态分析——保证安全性(更多讨论见系列文章《eBPF Verifier 深度剖析》)
- 修复:Map Fixup、辅助函数替换、CO-RE 重定位
- JIT 编译:BPF 字节码 → 目标架构原生代码
- 资源绑定:FD 分配、Map 注册、(可选)附加到钩子
理解 BPF 加载全貌是掌握 eBPF 生态的关键。无论是开发网络数据平面(XDP)、追踪工具(BCC/bpftrace)、安全策略(LSM BPF)、还是资源调度器(sched-ext),所有这些都建立在相同的加载链路之上。
随着 BPF Token 引入非特权场景、BTF 增强跨内核兼容性、BPF Link 统一附加模型,BPF 加载接口仍在快速演进。我们看到的不仅是技术细节演进,更是 Linux 内核将越来越多可编程能力安全高效交付给用户态的持续努力。
• 《eBPF Verifier 深度剖析——安全沙箱的验证之路》(ybb.press)
• BPF 内核文档 kernel.org/doc/html/latest/bpf/
• Brendan Gregg《BPF Performance Tools》
• Andrii Nakryiko《BPF CO-RE 指南》(nakryiko.com)
• Cilium《BPF 和 XDP 参考指南》

发表评论 取消回复