BPF 辅助函数注册
bpf_func_proto是Linux 4.17+ 内核注册 BPF 辅助函数的标准接口,直接替代了废弃的bpf_register_helper,是自定义 BPF 辅助函数的唯一正确方式。
- 作用:描述一个 BPF 辅助函数的所有信息(函数指针、参数类型、返回值、权限等)
- 内核通过它安全校验 BPF 程序,防止越权访问
bpf_register_helper4.17+ 已删除bpf_func_proto是内核官方唯一支持的方式- 带安全校验,防止 BPF 程序非法访问内存
- 所有新内核(5.x/ 6.x)都必须用它;
这个函数是 Linux 内核早期 BPF 接口(<5.0 版本)的 API,早已被内核官方废弃、移除。
- 内核v4.15~v4.20还保留兼容,内核v5.0 及以上彻底删除了该函数;
- 现在的内核(包括 CentOS 7/8、Ubuntu 20.04+、Android 内核)全部不支持
结构体定义(内核标准)
struct bpf_func_proto { u64 (*func)(u64 u1, u64 u2, u64 u3, u64 u4, u64 u5); // 辅助函数本体 enum bpf_arg_type arg1_type:8; // 参数1类型 enum bpf_arg_type arg2_type:8; // 参数2类型 enum bpf_arg_type arg3_type:8; // 参数3类型 enum bpf_arg_type arg4_type:8; // 参数4类型 enum bpf_arg_type arg5_type:8; // 参数5类型 enum bpf_return_type ret_type:8; // 返回值类型 bool gpl_only; // 是否仅限 GPL 协议模块使用 bool pkt_access; // 是否允许访问数据包 u32 ctx_arg; // 上下文参数索引 };最常用字段:
func:你的辅助函数指针argX_type:参数类型(内核用来做安全检查)ret_type:返回值类型gpl_only:是否 GPL 许可
最关键的回调函数
你必须实现一个回调函数,内核会调用它来获取辅助函数的原型:
const struct bpf_func_proto *(*func_proto)(enum bpf_func_id id, const struct bpf_prog *prog);作用:
- 根据
func_id返回对应的bpf_func_proto - 不认识的 ID 统一返回
bpf_base_func_proto(id)
完整可编译示例
步骤 1:实现自定义辅助函数
// 真正的辅助函数逻辑 static u64 bpf_helper_add(u64 a, u64 b, u64 unused3, u64 unused4, u64 unused5) { return a + b; }步骤 2:定义函数原型
static const struct bpf_func_proto add_func_proto = { .func = bpf_helper_add, .gpl_only = false, .ret_type = RET_INTEGER, .arg1_type = ARG_ANYTHING, .arg2_type = ARG_ANYTHING, };步骤 3:实现查询回调
static const struct bpf_func_proto * my_bpf_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog) { switch (func_id) { case BPF_FUNC_my_add: // 自定义函数ID return &add_func_proto; default: return bpf_base_func_proto(func_id); // 其他用内核默认 } }步骤 4:绑定到 BPF 程序类型
static struct bpf_prog_type my_prog_type __read_mostly = { .name = "my_bpf_prog", .func_proto = my_bpf_func_proto, // 绑定回调 };步骤 5:注册 / 卸载
// 模块初始化 static int __init my_init(void) { return register_bpf_prog_type(&my_prog_type); } // 模块退出 static void __exit my_exit(void) { unregister_bpf_prog_type(&my_prog_type); } module_init(my_init); module_exit(my_exit); MODULE_LICENSE("GPL");常用参数类型(arg_type)
你必须正确填写,否则 BPF 校验器会拒绝加载程序:
ARG_ANYTHING // 任意值 ARG_PTR_TO_MEM // 指向内存的指针 ARG_CONST_SIZE // 常量大小 ARG_PTR_TO_CTX // 指向上下文 ARG_DONTCARE // 不关心常用返回值:
RET_INTEGER // 返回整数 RET_PTR_TO_MEM // 返回内存指针 RET_VOID // 无返回BPF_CALL_*宏:BPF 辅助函数标准写法
BPF_CALL_0 / BPF_CALL_1 / BPF_CALL_2 ... BPF_CALL_5是Linux 内核定义 BPF 辅助函数的官方标准宏,必须配合bpf_func_proto使用,也是替代bpf_register_helper的核心。
- 内核定义的辅助函数声明宏
- 数字代表参数个数:
BPF_CALL_0:0 个参数BPF_CALL_1:1 个参数- ...
BPF_CALL_5:5 个参数(BPF 最大支持)
- 作用:统一封装 BPF 辅助函数的参数和返回值格式
- 所有内核自带 BPF 辅助函数(如
bpf_trace_printk)都用它
强制统一 BPF 辅助函数签名
BPF 规定辅助函数必须是:
u64 (*func)(u64, u64, u64, u64, u64);BPF_CALL_*帮你自动写好,不会写错。
示例 1:2 个参数 → 用BPF_CALL_2
// 辅助函数:a + b BPF_CALL_2(bpf_my_add, u64, a, u64, b) { return a + b; }示例 2:0 个参数 → 用BPF_CALL_0
BPF_CALL_0(bpf_get_pid) { return (u64)current->tgid; }示例 3:3 个参数
BPF_CALL_3(bpf_test, u64, a, u64, b, u64, c) { return a + b + c; }struct bpf_verifier_ops 与bpf程序
struct bpf_verifier_ops= BPF 程序的规则手册 + 裁判
BPF 程序 = 运动员
- 内核加载 BPF 程序时
- 必须先找它绑定的
bpf_verifier_ops - 用里面的函数来校验 BPF 代码是否合法
- 其中最重要的函数:
→它决定了这个 BPF 程序能调用哪些辅助函数!const struct bpf_func_proto *(*func_proto)(...);
每一种 BPF 程序(如 XDP、tracepoint、fentry、socket)都绑定一个固定的struct bpf_verifier_ops
例如:
BPF_PROG_TYPE_XDP → xdp_verifier_ops BPF_PROG_TYPE_TRACEPOINT → tracepoint_verifier_ops BPF_PROG_TYPE_TRACING → tracing_verifier_ops <-- 我们用这个 BPF_PROG_TYPE_SOCKET_FILTER → sock_filter_ops当你加载一个 BPF 程序:
- 内核看你是什么程序类型(TRACING / XDP...)
- 找到对应的
struct bpf_verifier_ops - 调用
ops->func_proto(func_id) - 返回函数是否可用 + 函数指针
这就是:
BPF 程序能调用哪些辅助函数,完全由 verifier_ops 决定!
最简洁的关系图
BPF 程序类型 ↓ (绑定) struct bpf_verifier_ops ↓ (包含) func_proto() <-- 你要改的就是这个 ↓ (查询) struct bpf_func_proto ↓ (指向) BPF_CALL_* 定义的辅助函数struct bpf_verifier_ops是 BPF 程序与辅助函数之间的唯一桥梁
想给 BPF 程序加自定义函数 → 必须修改它的 verifier_ops
- 每种 BPF 程序都有一个 verifier_ops
- func_proto 决定能调用哪些辅助函数
- 5.15 注册辅助函数 = 替换 func_proto
