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

【eBPF 内核实现深度拆解】Helper 函数子系统:注册、类型检查与参数传递

文章导航

分类入口
kernelebpf
标签入口
#ebpf#bpf-helpers#bpf-verifier#bpf_func_proto#arg-type#linux-kernel

目录

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.ckernel/bpf/verifier.cinclude/linux/bpf.hinclude/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 通用寄存器:R0R10R10 是只读帧指针,R1R5 用于向被调函数传参,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_insncall 指令的编码(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

三、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_tgidBPF_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 指令,验证:

  1. Helper 对这个程序类型可用
  2. 每个参数的类型正确
  3. 指针参数在合法范围内
  4. 返回值被正确处理

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, &regs[BPF_REG_1], &meta, fn, 1);
    if (err)
        return err;
    err = check_func_arg(env, fn->arg2_type, &regs[BPF_REG_2], &meta, fn, 2);
    if (err)
        return err;
    err = check_func_arg(env, fn->arg3_type, &regs[BPF_REG_3], &meta, fn, 3);
    if (err)
        return err;
    err = check_func_arg(env, fn->arg4_type, &regs[BPF_REG_4], &meta, fn, 4);
    if (err)
        return err;
    err = check_func_arg(env, fn->arg5_type, &regs[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_dispatchscx_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 函数子系统是三个机制的组合:

  1. 注册机制BPF_CALL_n 宏将 C 函数包装为 BPF 可调用的形式,bpf_func_proto 描述其类型签名,get_func_proto 按程序类型过滤可用 helper。
  2. 类型系统enum bpf_arg_typeenum bpf_return_type 编码了参数的来源(stack、map key、BTF object)、约束(常量、大小已知)和返回值的语义(OR_NULL、BTF typed)。
  3. 验证机制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、卸载,以及 refcntaux->refcnt 双重引用计数的精密设计。

参考

内核源码

规范与文档

补充资料

上一篇Map 内核实现(下):ringbuf / bloom / queue-stack / LPM(第 07 篇)

下一篇程序生命周期:load、attach、detach、pin 与引用计数(第 09 篇)

同主题继续阅读

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

2026-06-12 · kernel / ebpf

【eBPF 内核实现深度拆解】从验证器到 JIT,从 BTF 到调度器

eBPF 内核虚拟机内部实现系统讲解:BPF 指令集与寄存器机器、验证器的抽象解释与状态裁剪、JIT 编译器后端、Map 各类型的并发与内存模型、helper 函数注册与类型检查、BTF 格式规范与 CO-RE 重定位引擎、libbpf 加载器工程、fentry/fexit 蹦床机制、sched_ext 调度器内核接口。面向想读懂 eBPF 内核源码、写生产级 BPF 程序的系统工程师。


By .