BPF
程序被限制在沙盒中执行——不能调用任意内核函数,不能访问任意内存地址,不能做系统调用。但它需要读写
map、输出事件、读取内核数据结构。这些能力通过 helper
函数提供:内核中预定义的一组函数,BPF 程序可以通过
call N 指令调用,其中 N 是 helper
的编号。
但不是所有 helper 都可以在所有 BPF 程序类型中调用——XDP
程序中不能调用
bpf_get_current_pid_tgid(),kprobe
程序中不能调用
bpf_redirect()。这不是随便限制的,而是因为每个
helper 的语义要求特定的调用上下文(是否有
sk_buff?是否有
pt_regs?是否在进程上下文?)。这个”谁可以在哪调用哪个
helper”的规则,由 helper 函数的注册机制和 verifier
的类型检查共同实施。
本文从 struct bpf_func_proto 出发,拆解
helper 函数的注册机制、参数类型编码系统、以及 verifier 在
check_helper_call() 中的逐参数验证逻辑。
源码基准:Linux 6.8,路径
kernel/bpf/helpers.c、kernel/bpf/verifier.c、include/linux/bpf.h、include/uapi/linux/bpf.h。
一、Helper 不是什么:澄清一个常见的误解
初学 eBPF 的人经常有一个误解:“BPF helper 就是内核暴露给 BPF 的系统调用。”这个类比在意图上正确(都是受限环境请求外部服务),但在机制上完全错误。
系统调用通过软中断或 syscall
指令从用户态切换到内核态。BPF helper 不是——BPF
程序已经在内核态运行。helper
函数是同一特权级别内的函数调用(call N
指令),只不过 N
号函数是内核预先注册的,verifier
在加载时检查参数和返回值的安全性。
更准确的说法:helper 函数是内核提供给 BPF 程序的受信函数库。它不是远程调用,不是 RPC,不是上下文切换——它就是一次函数调用,没有页表切换,没有栈切换,没有调度延迟。verifier 充当这个库的”类型检查器 + 权限控制”。
二、BPF 调用约定的硬件基础
在理解 helper 注册之前,需要先建立 BPF 调用约定的心智模型。
BPF 虚拟机有 11 个 64-bit 通用寄存器:R0 到
R10。R10
是只读帧指针,R1 到 R5
用于向被调函数传参,R0 承载返回值。BPF 的
call N 指令触发一个 helper 调用:
BPF 程序执行流:
...
BPF_MOV64_REG(BPF_REG_1, BPF_REG_6) // 第一个参数放入 R1
BPF_MOV64_IMM(BPF_REG_2, 0) // 第二个参数放入 R2
BPF_CALL_INSN(helper_func_id) // call N
// R0 现在包含 helper 的返回值
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 2) // 检查 R0 != NULL
...
struct bpf_insn 中 call
指令的编码(64 位):
位域:
[63:32] imm = helper_func_id (目标函数编号)
[31:16] off = 0 (call 指令不使用 off)
[15:8] src_reg = BPF_PSEUDO_CALL 或 BPF_PSEUDO_KFUNC_CALL 或 0
[7:0] code = BPF_JMP | BPF_CALL
imm == 0且src_reg == 0:调用”下一个函数”(BPF-to-BPF 调用)imm != 0且src_reg == 0:调用 helper 函数(imm是 helper ID)imm != 0且src_reg == BPF_PSEUDO_KFUNC_CALL:调用 kfunc(BTF 标识的函数)
三、struct bpf_func_proto:Helper 的类型签名
每个 helper 函数有一个对应的
struct bpf_func_proto,描述它的参数类型和返回值类型。定义在
include/linux/bpf.h:
/* include/linux/bpf.h — Linux 6.8 */
struct bpf_func_proto {
/* C 函数指针(NULL 表示该 helper 未实现) */
u64 (*func)(u64 r1, u64 r2, u64 r3, u64 r4, u64 r5);
/* GPL 限制:如果为 true,只有 GPL 许可证的 BPF 程序才能调用 */
bool gpl_only;
/* 返回值类型(enum bpf_return_type) */
enum bpf_return_type ret_type;
union {
struct {
/* 参数类型(最多 5 个,对应 R1-R5) */
enum bpf_arg_type arg1_type;
enum bpf_arg_type arg2_type;
enum bpf_arg_type arg3_type;
enum bpf_arg_type arg4_type;
enum bpf_arg_type arg5_type;
};
/* 当参数超过 5 个时(kfunc 使用) */
enum bpf_arg_type arg_type[5];
};
/* 变长参数标记:如果非 0,arg[var_idx] 的长度由 arg[size_idx] 指定 */
int *arg_btf_id[5]; /* kfunc 的 BTF ID(用于 typed pointer) */
/* 与上下文的关联要求 */
u32 *arg1_btf; /* 第一个参数必须是此 BTF 类型 */
u32 *arg_btf[5]; /* 每个参数可选的 BTF 类型约束 */
u32 *ret_btf_id; /* 返回值的 BTF 类型 */
};
/* 参数类型枚举 — include/uapi/linux/bpf.h */
enum bpf_arg_type {
ARG_DONTCARE = 0, /* 不检查 */
/* 指针类型:核心参数 */
ARG_ANYTHING, /* 任何值(已废弃,用 ARG_DONTCARE) */
ARG_PTR_TO_MAP_KEY, /* 指向 map key 的指针 */
ARG_PTR_TO_MAP_VALUE, /* 指向 map value 的指针 */
ARG_PTR_TO_MEM, /* 指向已初始化内存的指针 */
ARG_PTR_TO_MEM_OR_NULL, /* 指向内存或 NULL */
ARG_PTR_TO_UNINIT_MEM, /* 指向未初始化内存(仅写) */
ARG_PTR_TO_CTX, /* 指向 BPF 上下文的指针 */
ARG_PTR_TO_STACK, /* 指向栈上变量的指针 */
ARG_PTR_TO_STACK_OR_NULL, /* 指向栈或 NULL */
ARG_PTR_TO_BTF_ID, /* 指向 BTF 类型化内核对象的指针 */
/* 大小类型:约束与关联 */
ARG_CONST_SIZE, /* 编译时常量(大小) */
ARG_CONST_SIZE_OR_ZERO, /* 编译时常量或 0 */
ARG_CONST_ALLOC_SIZE_OR_ZERO, /* 与 alloc 关联的大小 */
ARG_PTR_TO_ALLOC_MEM_OR_NULL, /* 指向分配器分配的未初始化内存 */
/* 特殊约束 */
ARG_CONST_MAP_PTR, /* 指向特定 map 的 const 指针 */
ARG_PTR_TO_RINGBUF_MEM, /* 指向 ringbuf 预留内存的指针 */
ARG_PTR_TO_ALLOC_MEM, /* 指向分配器分配的内存 */
ARG_PTR_TO_FIXED_SIZE_MEM, /* 指向固定大小内存 */
ARG_PTR_TO_MAP_VALUE_OR_NULL, /* 指向 map value 或 NULL */
/* 组合类型 */
ARG_PTR_TO_UNINIT_MEM,
ARG_PTR_TO_LONG, /* 指向 64-bit 整数的指针 */
__BPF_ARG_TYPE_MAX,
};
/* 返回值类型 — include/linux/bpf.h */
enum bpf_return_type {
RET_INTEGER, /* 整数(或错误码) */
RET_VOID, /* 无返回值 */
RET_PTR_TO_MAP_VALUE, /* 指向 map value 的指针 */
RET_PTR_TO_MAP_VALUE_OR_NULL, /* 指向 map value 或 NULL */
RET_PTR_TO_SOCKET_OR_NULL, /* 指向 socket 或 NULL */
RET_PTR_TO_TCP_SOCK_OR_NULL, /* 指向 tcp_sock 或 NULL */
RET_PTR_TO_SOCK_COMMON_OR_NULL, /* 指向 sock_common 或 NULL */
RET_PTR_TO_MEM_OR_BTF_ID_OR_NULL, /* 指向内存或 BTF 类型或 NULL */
RET_PTR_TO_MEM_OR_BTF_ID, /* 指向内存或 BTF 类型 */
RET_PTR_TO_BTF_ID_OR_NULL, /* 指向 BTF 类型对象或 NULL */
RET_PTR_TO_BTF_ID, /* 指向 BTF 类型对象 */
};这是一套完整类型系统。verifier 用这些类型编码来决定: - 一个参数来自哪里(map key、栈、内核对象) - 一个参数是否需要边界检查、初始化检查 - helper 返回值是什么类型——是否需要 NULL 检查、是否可以直接解引用
四、BPF_CALL_n 宏:包装 C 函数为 BPF Helper
BPF helper 不是直接写一个 C 函数,而是通过
BPF_CALL_n 宏系列包装。这些宏负责把 C
参数映射到 R1-R5 寄存器。定义在
include/linux/filter.h:
/* include/linux/filter.h — Linux 6.8 */
/*
* BPF_CALL_0(name, ...)
* BPF_CALL_1(name, type1, arg1)
* BPF_CALL_2(name, type1, arg1, type2, arg2)
* BPF_CALL_3(name, ...)
* BPF_CALL_4(name, ...)
* BPF_CALL_5(name, ...)
*
* 展开效果:定义一个函数,从 R1-R5 读取参数,并放在
* 特定的 ELF section 中以便内核定位。
*/
/* 以 BPF_CALL_2 为例的展开 */
#define BPF_CALL_2(name, t1, a1, t2, a2) \
static inline u64 ____##name(u64 r1, u64 r2, \
u64 r3 __maybe_unused, \
u64 r4 __maybe_unused, \
u64 r5 __maybe_unused); \
u64 name(u64 r1, u64 r2, \
u64 r3 __maybe_unused, \
u64 r4 __maybe_unused, \
u64 r5 __maybe_unused) \
{ \
t1 a1 = (t1)r1; \
t2 a2 = (t2)r2; \
return ____##name(r1, r2, r3, r4, r5); \
} \
static __always_inline u64 ____##name(u64 r1, u64 r2, \
u64 r3 __maybe_unused, \
u64 r4 __maybe_unused, \
u64 r5 __maybe_unused)实际例子——bpf_map_lookup_elem:
/* kernel/bpf/helpers.c — Linux 6.8 */
BPF_CALL_2(bpf_map_lookup_elem, struct bpf_map *, map, void *, key)
{
/* 此时 map 已是 struct bpf_map * 类型(从 R1 转型而来) */
/* key 已是 void * 类型(从 R2 转型而来) */
WARN_ON_ONCE(!rcu_read_lock_held() && !rcu_read_lock_trace_held() &&
!rcu_read_lock_bh_held());
return (u64)(unsigned long)map->ops->map_lookup_elem(map, key);
}
const struct bpf_func_proto bpf_map_lookup_elem_proto = {
.func = bpf_map_lookup_elem,
.gpl_only = false,
.ret_type = RET_PTR_TO_MAP_VALUE_OR_NULL,
.arg1_type = ARG_CONST_MAP_PTR, /* 必须是 map 指针 */
.arg2_type = ARG_PTR_TO_MAP_KEY, /* 必须是 map key 指针 */
};关键细节: - BPF_CALL_2
的第一个参数是函数名,后面每对 (type, name)
是一个参数。 - 包装函数从 u64
转型到实际类型——这个转型不做任何运行时检查,安全检查完全由
verifier 在加载时完成。 - .func 指向生成的
bpf_map_lookup_elem(u64, u64, ...) 函数。 -
返回值用 RET_PTR_TO_MAP_VALUE_OR_NULL
标记,告诉 verifier:调用后 R0 要么是合法的 value
指针,要么是 NULL,BPF
程序必须检查后再解引用。
五、Helper 注册:程序类型决定可用集合
不是每个 helper 对所有 BPF
程序类型都可用。每种程序类型通过 get_func_proto
回调声明自己支持哪些 helper。
5.1 基础 helpers:bpf_base_func_proto
/* kernel/bpf/helpers.c — bpf_base_func_proto() 简化 */
const struct bpf_func_proto *
bpf_base_func_proto(enum bpf_func_id func_id)
{
switch (func_id) {
case BPF_FUNC_map_lookup_elem:
return &bpf_map_lookup_elem_proto;
case BPF_FUNC_map_update_elem:
return &bpf_map_update_elem_proto;
case BPF_FUNC_map_delete_elem:
return &bpf_map_delete_elem_proto;
case BPF_FUNC_map_push_elem:
return &bpf_map_push_elem_proto;
case BPF_FUNC_map_pop_elem:
return &bpf_map_pop_elem_proto;
case BPF_FUNC_map_peek_elem:
return &bpf_map_peek_elem_proto;
case BPF_FUNC_get_prandom_u32:
return &bpf_get_prandom_u32_proto;
case BPF_FUNC_get_smp_processor_id:
return &bpf_get_smp_processor_id_proto;
case BPF_FUNC_get_numa_node_id:
return &bpf_get_numa_node_id_proto;
case BPF_FUNC_tail_call:
return &bpf_tail_call_proto;
case BPF_FUNC_ktime_get_ns:
return &bpf_ktime_get_ns_proto;
case BPF_FUNC_ktime_get_boot_ns:
return &bpf_ktime_get_boot_ns_proto;
case BPF_FUNC_ringbuf_reserve:
return &bpf_ringbuf_reserve_proto;
case BPF_FUNC_ringbuf_submit:
return &bpf_ringbuf_submit_proto;
case BPF_FUNC_ringbuf_discard:
return &bpf_ringbuf_discard_proto;
case BPF_FUNC_ringbuf_query:
return &bpf_ringbuf_query_proto;
case BPF_FUNC_trace_printk:
return &bpf_trace_printk_proto;
case BPF_FUNC_spin_lock:
return &bpf_spin_lock_proto;
case BPF_FUNC_spin_unlock:
return &bpf_spin_unlock_proto;
case BPF_FUNC_timer_init:
return &bpf_timer_init_proto;
case BPF_FUNC_timer_set_callback:
return &bpf_timer_set_callback_proto;
case BPF_FUNC_timer_start:
return &bpf_timer_start_proto;
case BPF_FUNC_timer_cancel:
return &bpf_timer_cancel_proto;
case BPF_FUNC_loop:
return &bpf_loop_proto;
case BPF_FUNC_dynptr_data:
return &bpf_dynptr_data_proto;
...
default:
return NULL; /* unknown func_id */
}
}bpf_base_func_proto() 返回的是”所有 BPF
程序类型共享的最基础 helper”。map 操作、获取 CPU
ID、获取时间戳、ringbuf
操作——这些在任何上下文中都有意义。
5.2 程序类型特定的 helper:以 tracing 为例
每种 BPF 程序类型注册自己的
get_func_proto。以 tracing
程序为例(kernel/trace/bpf_trace.c):
/* kernel/trace/bpf_trace.c — tracing_func_proto() 简化 */
static const struct bpf_func_proto *
tracing_func_proto(enum bpf_func_id func_id)
{
switch (func_id) {
/* 先查 tracing 特有的 helper */
case BPF_FUNC_map_lookup_elem:
return &bpf_map_lookup_elem_proto;
case BPF_FUNC_map_update_elem:
return &bpf_map_update_elem_proto;
/* tracing 特有:访问当前进程信息 */
case BPF_FUNC_get_current_pid_tgid:
return &bpf_get_current_pid_tgid_proto;
case BPF_FUNC_get_current_uid_gid:
return &bpf_get_current_uid_gid_proto;
case BPF_FUNC_get_current_comm:
return &bpf_get_current_comm_proto;
case BPF_FUNC_get_current_task:
return &bpf_get_current_task_proto;
/* tracing 特有:读取内核/用户内存 */
case BPF_FUNC_probe_read:
return &bpf_probe_read_proto;
case BPF_FUNC_probe_read_kernel:
return &bpf_probe_read_kernel_proto;
case BPF_FUNC_probe_read_user:
return &bpf_probe_read_user_proto;
case BPF_FUNC_probe_read_kernel_str:
return &bpf_probe_read_kernel_str_proto;
case BPF_FUNC_probe_read_user_str:
return &bpf_probe_read_user_str_proto;
/* tracing 特有:获取追踪上下文 */
case BPF_FUNC_get_func_ip:
return &bpf_get_func_ip_proto_tracing;
case BPF_FUNC_get_attach_cookie:
return &bpf_get_attach_cookie_proto_tracing;
/* tracing 特有:perf_event_output */
case BPF_FUNC_perf_event_output:
return &bpf_perf_event_output_proto;
/* tracing 特有:栈回溯 */
case BPF_FUNC_get_stackid:
return &bpf_get_stackid_proto_tracing;
case BPF_FUNC_get_stack:
return &bpf_get_stack_proto_tracing;
default:
return bpf_base_func_proto(func_id); /* 回退到基础 helpers */
}
}而 XDP
程序(net/core/filter.c)提供的是完全不同的
helper 集合:
/* net/core/filter.c — xdp_func_proto() 简化 */
static const struct bpf_func_proto *
xdp_func_proto(enum bpf_func_id func_id)
{
switch (func_id) {
/* 基础 map 操作 */
case BPF_FUNC_map_lookup_elem:
return &bpf_map_lookup_elem_proto;
case BPF_FUNC_map_update_elem:
return &bpf_map_update_elem_proto;
case BPF_FUNC_map_delete_elem:
return &bpf_map_delete_elem_proto;
/* XDP 特有:包操作 */
case BPF_FUNC_xdp_adjust_head:
return &bpf_xdp_adjust_head_proto;
case BPF_FUNC_xdp_adjust_tail:
return &bpf_xdp_adjust_tail_proto;
/* XDP 特有:重定向 */
case BPF_FUNC_redirect:
return &bpf_xdp_redirect_proto;
case BPF_FUNC_redirect_map:
return &bpf_xdp_redirect_map_proto;
/* XDP 特有:校验和 */
case BPF_FUNC_csum_diff:
return &bpf_csum_diff_proto;
default:
return bpf_base_func_proto(func_id);
}
}注意:XDP 的 xdp_func_proto
不提供
BPF_FUNC_get_current_pid_tgid 和
BPF_FUNC_probe_read_user。这不是疏忽——XDP 在
NAPI softirq
上下文中运行,没有”当前进程”的概念,current
指针是随机的。内核通过”不提供对应的
proto”从源头禁止了语义上不安全的调用。
5.3 调用的完整路径
BPF 程序编译时:
call N → N 是 helper ID
BPF 程序加载时:
verifier 检查:
1. 查询 prog_type->get_func_proto(N)
2. 如果返回 NULL → helper 不可用 → 拒绝加载
3. 如果返回 &proto → 提取 proto,进入 check_helper_call()
BPF 程序运行时 (JIT 后):
call N 指令 → JIT 将其翻译为:
x86: call <address_of_helper_function>
helper 函数通过 R1-R5 接收参数,通过 R0 返回结果
六、check_helper_call:Verifier 的参数验证
check_helper_call() 是 verifier
中最复杂的函数之一(位于
kernel/bpf/verifier.c)。它在 BPF
加载时模拟执行 call N 指令,验证:
- Helper 对这个程序类型可用
- 每个参数的类型正确
- 指针参数在合法范围内
- 返回值被正确处理
6.1 入口逻辑
/* kernel/bpf/verifier.c — check_helper_call() 简化入口 */
static int check_helper_call(struct bpf_verifier_env *env,
struct bpf_insn *insn, int *insn_idx_p)
{
struct bpf_func_state *state = cur_func(env);
enum bpf_func_id func_id = insn->imm; /* call 指令的 imm 字段是 helper ID */
const struct bpf_func_proto *fn = NULL;
struct bpf_reg_state *regs;
int meta, err;
/* 1. 检查 helper 是否对当前程序类型可用 */
fn = env->ops->get_func_proto(func_id, env->prog);
if (!fn) {
verbose(env, "unknown func %s#%d\n", func_id_name(func_id), func_id);
return -EINVAL;
}
/* 2. 检查 GPL 限制 */
if (fn->gpl_only && !env->prog->gpl_compatible) {
verbose(env, "cannot call GPL-restricted function from non-GPL program\n");
return -EINVAL;
}
/* 3. 设置返回寄存器 R0 的类型 */
/* (在参数检查之前预先设置,因为某些参数类型需要知道返回值上下文) */
err = check_helper_ret_type(env, fn);
if (err)
return err;
/* 4. 逐个检查参数:arg1_type 到 arg5_type */
regs = cur_regs(env);
err = check_func_arg(env, fn->arg1_type, ®s[BPF_REG_1], &meta, fn, 1);
if (err)
return err;
err = check_func_arg(env, fn->arg2_type, ®s[BPF_REG_2], &meta, fn, 2);
if (err)
return err;
err = check_func_arg(env, fn->arg3_type, ®s[BPF_REG_3], &meta, fn, 3);
if (err)
return err;
err = check_func_arg(env, fn->arg4_type, ®s[BPF_REG_4], &meta, fn, 4);
if (err)
return err;
err = check_func_arg(env, fn->arg5_type, ®s[BPF_REG_5], &meta, fn, 5);
if (err)
return err;
/* 5. 设置 R1-R5 为 NOT_INIT(确保 BPF 程序不会在返回后再用参数寄存器) */
int i;
for (i = BPF_REG_1; i <= BPF_REG_5; i++)
mark_reg_not_init(env, regs, i);
return 0;
}6.2 check_func_arg:逐参数类型验证
/* kernel/bpf/verifier.c — check_func_arg() 简化 */
static int check_func_arg(struct bpf_verifier_env *env, u32 arg_type,
struct bpf_reg_state *reg, struct bpf_call_arg_meta *meta,
const struct bpf_func_proto *fn, int argno)
{
enum bpf_reg_type type = reg->type;
int err = 0;
/* 跳过未使用的参数 */
if (arg_type == ARG_DONTCARE)
return 0;
/* 确保参数不是未初始化的寄存器 */
err = check_reg_arg(env, reg, SRC_OP);
if (err)
return err;
/* 根据参数类型进行具体检查 */
switch (arg_type) {
case ARG_PTR_TO_MAP_KEY:
case ARG_PTR_TO_MAP_VALUE:
case ARG_PTR_TO_UNINIT_MAP_VALUE:
/* 期望是指向栈的指针 */
if (type != PTR_TO_STACK) {
if (type_is_pkt_pointer(type))
err = check_pkt_ptr_alignment(env, reg, 0, fn->arg5_type);
else
goto err_type;
}
/* 检查 key/value 的大小是否匹配 map 的 key_size/value_size */
if (arg_type == ARG_PTR_TO_MAP_KEY) {
if (meta->map_ptr && reg->smax_value != meta->map_ptr->key_size)
goto err_type;
}
break;
case ARG_PTR_TO_MEM:
case ARG_PTR_TO_MEM_OR_NULL:
case ARG_PTR_TO_UNINIT_MEM:
/* 检查内存指针 */
if (type_is_pkt_pointer(type) ||
type == PTR_TO_MAP_VALUE ||
type == PTR_TO_MEM ||
type == PTR_TO_BUF ||
type == PTR_TO_STACK) {
/* 合法的内存指针来源 */
} else {
verbose(env, "R%d type=%s expected=%s\n",
regno, reg_type_str(env, type), "ptr_to_mem");
return -EACCES;
}
/* 检查 mem_size 匹配(通过 arg5_type 验证) */
break;
case ARG_PTR_TO_BTF_ID:
/* 必须是 BTF 类型化指针 */
if (type != PTR_TO_BTF_ID) {
verbose(env, "arg %d expected btf_id pointer\n", argno);
return -EINVAL;
}
/* 验证 BTF ID 匹配 */
break;
case ARG_CONST_SIZE:
case ARG_CONST_SIZE_OR_ZERO:
/* 必须是编译期已知的常量 */
if (!tnum_is_const(reg->var_off)) {
verbose(env, "R%d is not a known constant\n", regno);
return -EACCES;
}
break;
case ARG_CONST_MAP_PTR:
/* 必须是 const map 指针 */
if (type != CONST_PTR_TO_MAP) {
verbose(env, "R%d not a const map ptr\n", regno);
return -EACCES;
}
break;
case ARG_PTR_TO_CTX:
/* 必须指向上下文(不同程序类型不同) */
if (type != PTR_TO_CTX) {
verbose(env, "R%d not a ctx ptr\n", regno);
return -EACCES;
}
break;
case ARG_PTR_TO_STACK:
case ARG_PTR_TO_STACK_OR_NULL:
if (type != PTR_TO_STACK) {
/* 允许 NULL */
if (arg_type == ARG_PTR_TO_STACK_OR_NULL &&
register_is_null(reg))
break;
goto err_type;
}
/* 检查栈访问是否在栈范围内 */
break;
case ARG_PTR_TO_RINGBUF_MEM:
/* ringbuf_reserve 的返回值 */
if (type != PTR_TO_MEM) {
/* 检查 ringbuf 分配器特定标记 */
goto err_type;
}
break;
case ARG_ANYTHING:
/* 可以是任何值 */
break;
default:
verbose(env, "unsupported arg_type %d\n", arg_type);
return -EFAULT;
}
return 0;
err_type:
verbose(env, "R%d type=%s expected=%s\n",
regno, reg_type_str(env, type),
reg_type_str(env, arg_type_to_reg_type(arg_type)));
return -EACCES;
}6.3 类型检查的具体案例
案例
1:bpf_map_lookup_elem(map, key)
/* check_helper_call 对 bpf_map_lookup_elem 的检查: */
/*
* R1: ARG_CONST_MAP_PTR
* → 检查 R1 类型必须是 CONST_PTR_TO_MAP
* → 同时记录 meta.map_ptr 用于后续 key/value 大小检查
*
* R2: ARG_PTR_TO_MAP_KEY
* → 检查 R2 必须是 PTR_TO_STACK(或 pkt pointer)
* → 检查 R2 指向的大小 == map->key_size
*
* R0(返回值): RET_PTR_TO_MAP_VALUE_OR_NULL
* → 设置 R0 类型为 PTR_TO_MAP_VALUE_OR_NULL
* → R0 附带 map_ptr 和 value_size 信息
*/案例
2:bpf_probe_read_kernel(dst, size, src)
/* check_helper_call 对 bpf_probe_read_kernel 的检查: */
/*
* R1: ARG_PTR_TO_UNINIT_MEM
* → R1 必须是 PTR_TO_STACK 或其他可写内存
* → 内存不需要初始化(是我们写入的目标)
*
* R2: ARG_CONST_SIZE_OR_ZERO
* → R2 必须是编译期已知常量或 0
* → 如果 R2 == 0,则不做任何检查(实际上不会读取任何数据)
*
* R3: ARG_ANYTHING
* → 可以是任意类型的指针或整数(src 地址)
* → HELPER 内部有运行时保护(probe_kernel_read 返回 -EFAULT 而非崩溃)
*/案例 3:参数大小约束检查(CONST_SIZE)
/* kernel/bpf/verifier.c — 大小约束检查逻辑 */
/*
* 对于带有 CONST_SIZE 参数的 helper:
* verifier 要求 size 参数必须是"已知常量"——即 tnum_is_const(reg->var_off) 为 true
*
* 这意味着以下 BPF 代码合法:
* bpf_probe_read_kernel(buf, 16, ptr); // 16 是立即数 → 常量
* bpf_probe_read_kernel(buf, sizeof(*ptr), ptr); // sizeof 编译期确定 → 常量
*
* 以下不合法:
* bpf_probe_read_kernel(buf, var, ptr); // var 可能运行时可变 → 不是常量
* // verifier 报错:R2 is not a known constant
*
* 解决办法:用 bpf_dynptr 或固定大小包装(如 bpf_probe_read_kernel_str)
*/6.4 返回值类型与 NULL 检查要求
Verifier 不仅检查参数,还在调用后设置 R0 的类型约束:
/* kernel/bpf/verifier.c — set_callee_state() 返回值类型处理 */
static void set_callee_return_type(struct bpf_verifier_env *env,
struct bpf_func_state *caller,
const struct bpf_func_proto *fn)
{
struct bpf_reg_state *r0 = &caller->regs[BPF_REG_0];
switch (fn->ret_type) {
case RET_INTEGER:
/* R0 = 标量值 */
mark_reg_unknown(env, caller->regs, BPF_REG_0);
break;
case RET_VOID:
/* R0 = 未定义,不应被使用 */
caller->regs[BPF_REG_0].type = NOT_INIT;
break;
case RET_PTR_TO_MAP_VALUE_OR_NULL:
/* R0 = map value 指针或 NULL */
r0->type = PTR_TO_MAP_VALUE_OR_NULL;
r0->map_ptr = meta->map_ptr;
r0->off = 0;
r0->range = meta->map_ptr->value_size;
/* 关键:标记为 OR_NULL → BPF 程序 MUST 检查 NULL */
break;
case RET_PTR_TO_MEM_OR_BTF_ID_OR_NULL:
/* R0 = 内存指针或 BTF 类型化指针或 NULL */
r0->type = PTR_TO_MEM_OR_NULL;
r0->btf = meta->ret_btf;
r0->btf_id = meta->ret_btf_id;
break;
case RET_PTR_TO_SOCKET_OR_NULL:
r0->type = PTR_TO_SOCKET;
r0->id = ++env->id_gen;
mark_ptr_or_null_regs(env->cur_state, caller, BPF_REG_0,
meta->map_ptr, true);
break;
/* ... 更多返回值类型 ... */
}
}_OR_NULL 后缀的返回值类型是关键设计——它告诉
verifier:“这个返回值可能是 NULL”。BPF 程序在解引用 R0
之前必须做 NULL 检查。如果 verifier
探测到没有 NULL 检查就直接使用 R0,它会拒绝程序:
R0 invalid mem access 'map_value_or_null'
这个强制 NULL 检查机制是 eBPF 安全保证的核心组成部分——它防止了内核中的空指针解引用。
七、kfunc:超越 BPF_CALL_n 的新范式
Linux 5.13 引入了 kfunc(BPF kernel function),允许 BPF
程序调用未通过 BPF_CALL_n
包装的普通内核函数,而是通过 BTF 类型信息来验证调用。
kfunc 的声明使用 __bpf_kfunc 标记:
/* 例:cpumap 的 kfunc */
__bpf_kfunc u32 bpf_cpumap_acquire(struct bpf_cpumap *map)
{
...
}
/* 对应的 BTF 声明(由 BTF_ID_FLAGS 宏生成) */
BTF_ID_FLAGS(func, bpf_cpumap_acquire, KF_ACQUIRE)Kfunc 和传统 helper 的关键区别:
| 维度 | 传统 Helper | Kfunc |
|---|---|---|
| 注册方式 | BPF_CALL_n +
bpf_func_proto |
__bpf_kfunc + BTF_ID_FLAGS |
| 参数检查 | enum bpf_arg_type |
BTF 类型信息(天然支持任意类型) |
| 参数数量 | 最多 5 个(R1-R5) | 理论上无限制(通过 BTF 描述) |
| 函数发现 | 编译到特定 ELF section | BTF 函数列表 |
| 动态注册 | 无 | 支持模块注册/注销(register_btf_kfunc_id_set) |
| 可见性 | 全局 | 可按子系统过滤(BPF_PROG_TYPE_XDP 等) |
Kfunc 的出现改变了 helper 生态——新功能越来越多地通过
kfunc 暴露。例如 sched_ext 的调度操作全部是
kfunc(scx_bpf_dispatch、scx_bpf_kick_cpu
等),而不是传统 helper。
八、关键 Helper 速查表
以下列出 20 个最重要 helper 的类型签名和使用限制:
| func_id | Helper 函数 | 参数类型(R1..R5) | 返回类型 | 程序类型 | 用途 |
|---|---|---|---|---|---|
BPF_FUNC_map_lookup_elem |
bpf_map_lookup_elem |
CONST_MAP_PTR, PTR_TO_MAP_KEY | MAP_VALUE_OR_NULL | 所有 | 读取 map 元素 |
BPF_FUNC_map_update_elem |
bpf_map_update_elem |
CONST_MAP_PTR, PTR_TO_MAP_KEY, PTR_TO_MAP_VALUE, ANYTHING | INTEGER | 所有 | 写入 map 元素 |
BPF_FUNC_map_delete_elem |
bpf_map_delete_elem |
CONST_MAP_PTR, PTR_TO_MAP_KEY | INTEGER | 所有 | 删除 map 元素 |
BPF_FUNC_get_prandom_u32 |
bpf_get_prandom_u32 |
(无参数) | INTEGER | 所有 | 随机数 |
BPF_FUNC_get_smp_processor_id |
bpf_get_smp_processor_id |
(无参数) | INTEGER | 所有 | 当前 CPU ID |
BPF_FUNC_ktime_get_ns |
bpf_ktime_get_ns |
(无参数) | INTEGER | 所有 | 单调时间戳(ns) |
BPF_FUNC_tail_call |
bpf_tail_call |
PTR_TO_CTX, CONST_MAP_PTR, ANYTHING | VOID | 所有 | 尾调用 |
BPF_FUNC_ringbuf_reserve |
bpf_ringbuf_reserve |
CONST_MAP_PTR, ANYTHING, ANYTHING | ALLOC_MEM_OR_NULL | 所有 | ringbuf 预留空间 |
BPF_FUNC_ringbuf_submit |
bpf_ringbuf_submit |
PTR_TO_RINGBUF_MEM, ANYTHING | VOID | 所有 | ringbuf 提交 |
BPF_FUNC_ringbuf_discard |
bpf_ringbuf_discard |
PTR_TO_RINGBUF_MEM, ANYTHING | VOID | 所有 | ringbuf 丢弃 |
BPF_FUNC_probe_read_kernel |
bpf_probe_read_kernel |
PTR_TO_UNINIT_MEM, CONST_SIZE, ANYTHING | INTEGER | tracing | 读内核内存 |
BPF_FUNC_probe_read_user |
bpf_probe_read_user |
PTR_TO_UNINIT_MEM, CONST_SIZE, ANYTHING | INTEGER | tracing | 读用户内存 |
BPF_FUNC_get_current_pid_tgid |
bpf_get_current_pid_tgid |
(无参数) | INTEGER | tracing | 获取 PID/TGID |
BPF_FUNC_get_current_task |
bpf_get_current_task |
(无参数) | BTF_ID | tracing | 获取 task_struct |
BPF_FUNC_redirect |
bpf_redirect |
ANYTHING, ANYTHING | INTEGER | XDP/TC | 包重定向 |
BPF_FUNC_xdp_adjust_head |
bpf_xdp_adjust_head |
PTR_TO_CTX, ANYTHING | INTEGER | XDP | 调整包头部 |
BPF_FUNC_perf_event_output |
bpf_perf_event_output |
PTR_TO_CTX, CONST_MAP_PTR, ANYTHING, PTR_TO_MEM, CONST_SIZE | INTEGER | tracing | perf 事件输出 |
BPF_FUNC_csum_diff |
bpf_csum_diff |
PTR_TO_MEM, CONST_SIZE, PTR_TO_MEM, CONST_SIZE, ANYTHING | INTEGER | XDP/TC | 校验和差分 |
BPF_FUNC_spin_lock |
bpf_spin_lock |
PTR_TO_MAP_VALUE | VOID | 所有 | spin lock acquire |
BPF_FUNC_spin_unlock |
bpf_spin_unlock |
PTR_TO_MAP_VALUE | VOID | 所有 | spin lock release |
九、总结
BPF helper 函数子系统是三个机制的组合:
- 注册机制:
BPF_CALL_n宏将 C 函数包装为 BPF 可调用的形式,bpf_func_proto描述其类型签名,get_func_proto按程序类型过滤可用 helper。 - 类型系统:
enum bpf_arg_type和enum bpf_return_type编码了参数的来源(stack、map key、BTF object)、约束(常量、大小已知)和返回值的语义(OR_NULL、BTF typed)。 - 验证机制:
check_helper_call()在加载时逐个参数验证类型匹配、范围合法、大小正确,并设置 R0 的类型约束——特别是_OR_NULL强制 NULL 检查。
理解这套机制的实践价值:当 verifier 拒绝
bpf_probe_read_kernel(buf, var, ptr) 并报 “R2
is not a known constant” 时,你知道这是因为
ARG_CONST_SIZE 要求大小参数是编译期常量——要么用
sizeof(),要么用
bpf_probe_read_kernel_str()(它接受可变长度),要么用
bpf_dynptr 系列 API。
下一篇(第 09 篇)将跟踪一个 BPF 程序从
bpf(BPF_PROG_LOAD)
系统调用开始的完整生命周期——加载、验证、JIT、attach、detach、卸载,以及
refcnt 与 aux->refcnt
双重引用计数的精密设计。
参考
内核源码
- Linux 6.8
include/linux/bpf.h—struct bpf_func_proto、枚举定义 - Linux 6.8
include/linux/filter.h—BPF_CALL_n宏定义 - Linux 6.8
include/uapi/linux/bpf.h—enum bpf_func_id、enum bpf_arg_type - Linux 6.8
kernel/bpf/verifier.c—check_helper_call()、check_func_arg() - Linux 6.8
kernel/bpf/helpers.c—bpf_base_func_proto()、基础 helper 实现 - Linux 6.8
kernel/trace/bpf_trace.c—tracing_func_proto()、tracing helper 实现 - Linux 6.8
net/core/filter.c—xdp_func_proto()、网络 helper 实现
规范与文档
Documentation/bpf/btf.rst— BTF 与 kfunc 文档include/uapi/linux/bpf.h— helper function ID 完整枚举
补充资料
上一篇:Map 内核实现(下):ringbuf / bloom / queue-stack / LPM(第 07 篇)
下一篇:程序生命周期:load、attach、detach、pin 与引用计数(第 09 篇)
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【eBPF 内核实现深度拆解】验证器框架:从 BPF_PROG_LOAD 到 do_check()
跟踪 BPF_PROG_LOAD 系统调用的内核执行路径,逐层拆解 bpf_prog_load()→bpf_check()→do_check_main() 的调用链,建立 verifier 执行全景——这是理解 verifier 安全保证的入口。
【eBPF 内核实现深度拆解】从验证器到 JIT,从 BTF 到调度器
eBPF 内核虚拟机内部实现系统讲解:BPF 指令集与寄存器机器、验证器的抽象解释与状态裁剪、JIT 编译器后端、Map 各类型的并发与内存模型、helper 函数注册与类型检查、BTF 格式规范与 CO-RE 重定位引擎、libbpf 加载器工程、fentry/fexit 蹦床机制、sched_ext 调度器内核接口。面向想读懂 eBPF 内核源码、写生产级 BPF 程序的系统工程师。
【eBPF 内核实现深度拆解】BPF 指令集解码:寄存器机器、调用约定与指令编码
从 eBPF 虚拟机的 11 个 64-bit 寄存器和 struct bpf_insn 出发,逐条拆解 ALU64/ALU32、跳转、加载存储、call 四类指令的字段语义与编码格式,建立后续 verifier 和 JIT 讨论的精确基础。
【eBPF 内核实现深度拆解】验证器核心算法:抽象解释、状态跟踪与路径裁剪
深入 verifier 的静态分析引擎——寄存器状态 reg_state 的类型/值域表示、栈状态 stack_state 的初始化标记、explore_state 的 DFS 搜索、states_equal 的等价判定、precision tracking——这是整个系列最难也最核心的一篇。