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

【eBPF 内核实现深度拆解】验证器框架:从 BPF_PROG_LOAD 到 do_check()

文章导航

分类入口
kernelebpf
标签入口
#ebpf#bpf-verifier#do_check#bpf_prog_load#check_cfg#control-flow-graph#linux-kernel

目录

bpftool prog load my_prog.o /sys/fs/bpf/my_prog 返回 Error: program rejected by verifier。verifier 日志从某一行开始输出,但你不知道它是在哪一步检查失败的——是在控制流图构建时就死了,还是在逐条模拟执行指令时发现 R1 的类型不对?不知道 verifier 的执行顺序和阶段划分,读 verifier 日志就像摸黑走路。

源码基准:本系列以 Linux 6.6 / 6.8 LTS 为主线;本篇引用的路径与行号以 Linux 6.6 为准。

本文跟踪 bpf(BPF_PROG_LOAD) 系统调用从用户态到 verifier 主循环的完整内核路径。建立 bpf_verifier_env 这个核心状态的字段理解,拆解 check_cfg() 如何构建控制流图并检测不可达代码和死循环,最后进入 do_check() 的主指令模拟循环。读完这篇,你知道 verifier 分为几个阶段,每个阶段做什么,以及在哪个阶段你的程序更可能被拒绝。

一、系统调用入口:从用户态到内核

用户态调用 bpf() 系统调用,内核通过 CONFIG_BPF_SYSCALL 编译的支持代码分派:

/* Linux 6.6: kernel/bpf/syscall.c:5006 */
SYSCALL_DEFINE3(bpf, int, cmd, union bpf_attr __user *, uattr, unsigned int, size)
{
    return __sys_bpf(cmd, USER_BPFPTR(uattr), size);
}

/* Linux 6.6: kernel/bpf/syscall.c:5013 */
static int __sys_bpf(int cmd, bpfptr_t uattr, unsigned int size)
{
    ...
    switch (cmd) {
    case BPF_PROG_LOAD:
        err = bpf_prog_load(&attr, uattr, size);
        break;
    case BPF_MAP_CREATE:
        ...
    }
}

cmd == BPF_PROG_LOAD 时,控制流进入 bpf_prog_load(),这是 BPF 程序加载的核心入口。

1.1 bpf_prog_load() 的职责

bpf_prog_load()kernel/bpf/syscall.c)执行以下步骤(简化):

/* Linux 6.6: kernel/bpf/syscall.c */
static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, u32 uattr_size)
{
    struct bpf_prog *prog;
    int ret;
    ...

    /* 1. 将 BPF 指令从用户态复制到内核缓冲区 */
    prog = bpf_prog_alloc(bpf_prog_size(attr->insn_cnt), GFP_USER);
    if (copy_from_bpfptr(prog->insns,
               make_bpfptr(attr->insns, uattr.is_kernel),
               bpf_prog_insn_size(prog)))
        goto free_prog;

    prog->len = attr->insn_cnt;

    /* 2. 分配辅助信息(每个指令的 verifier 元数据 + func_info) */
    ret = bpf_prog_alloc_aux(prog, attr);

    /* 3. 分配和复制程序名 */
    ...

    /* 4. 调用 verifier —— 核心安全检查 */
    ret = bpf_check(&prog, attr);

    /* 5. 验证通过后,分配 prog_array 引用 ID */
    ...

    /* 6. JIT 编译(如果启用) */
    ...

    /* 7. 分配文件描述符,返回给用户态 */
    ...
}

第 4 步的 bpf_check() 就是我们关注的 verifier 入口。整个调用链为:

bpf(BPF_PROG_LOAD)
  └── __sys_bpf(BPF_PROG_LOAD)
        └── bpf_prog_load(attr)
              ├── bpf_prog_alloc()          // 分配 bpf_prog + 复制指令
              ├── bpf_prog_alloc_aux()      // 分配辅助数据
              └── bpf_check(&prog, attr)    // ★ 验证器入口
                    ├── check_cfg()          // 阶段 1:控制流图构建
                    ├── check_subprogs()     // 阶段 2:子程序边界分析
                    ├── do_check_main()      // 阶段 3:主验证循环
                    │     └── do_check()     // 逐条指令模拟执行
                    └── check_max_stack_depth() // 阶段 4:栈深度检查
                          (固定栈大小 512)

1.2 bpf_verifier_env:验证器的全局状态

bpf_verifier_env 是整个验证过程的上下文结构体,定义了 verifier 需要的所有状态(kernel/bpf/verifier.c):

/* Linux 6.6: kernel/bpf/verifier.c */
struct bpf_verifier_env {
    struct bpf_prog *prog;              /* 正在验证的程序 */
    struct bpf_verifier_stack_elem *head;/* 验证 DFS 栈顶 */
    int stack_size;                     /* 当前栈深度 */
    
    struct bpf_verifier_state cur_state;/* 当前分析点的寄存器/栈状态 */
    struct bpf_verifier_state_list **explored_states;
                                        /* 已探索状态哈希表(用于 pruning) */
    struct bpf_insn_aux_data *insn_aux_data;
                                        /* 每条指令的辅助数据 */
    
    u32 pass_cnt;                       /* 验证的 pass 计数 */
    u32 subprog_cnt;                    /* 子程序数量 */
    
    /* 日志控制 */
    u32 log_level;
    char *log_buf;
    u32 log_size;
    u32 log_len;
    
    /* 限制 */
    u32 insn_processed;                 /* 已处理指令数 */
    u32 prev_insn_processed;
    
    /* BTF 相关信息 */
    struct btf *btf;
    struct btf *btf_vmlinux;
    ...
};

关键字段解释:

二、bpf_check():验证总控

bpf_check() 是 verifier 的顶层协调函数:

/* Linux 6.6: kernel/bpf/verifier.c - bpf_check() 简化 */
int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr, ...)
{
    struct bpf_verifier_env *env;
    int ret;

    /* 1. 分配和初始化 bpf_verifier_env */
    env = kzalloc(sizeof(struct bpf_verifier_env), GFP_KERNEL);

    /* 2. 设置日志级别 */
    env->log_level = attr->log_level;

    /* 3. 初始化每条指令的辅助数据 */
    env->insn_aux_data = kvcalloc(prog->len, sizeof(*env->insn_aux_data), ...);

    /* 4. 阶段 1:构建控制流图 + 检测不可达代码 */
    ret = check_cfg(env);
    if (ret < 0)
        goto err_release;

    /* 5. 阶段 2:分析子程序边界(如果程序包含 BPF_PSEUDO_CALL) */
    ret = check_subprogs(env);
    if (ret < 0)
        goto err_release;

    /* 6. 阶段 3:主验证循环 —— 逐条模拟执行所有指令 */
    ret = do_check_main(env);
    if (ret < 0)
        goto err_release;

    /* 7. 阶段 4:检查最大栈深度(确保在 512 字节内) */
    ret = check_max_stack_depth(env);
    if (ret < 0)
        goto err_release;
    
    /* 8. 优化:移除不需要精确跟踪的指令标记 */
    ...
}

三、check_cfg():控制流图构建

3.1 控制流图要解决的问题

check_cfg()kernel/bpf/verifier.c)构建程序的控制流图(CFG),并回答以下问题:

  1. 是否有不可达指令(DAG 中的孤立节点)?
  2. 是否所有路径都到达 BPF_EXIT(无死循环/无限循环)?
  3. 是否有回边(back-edge),即循环的存在?如果有,是否可接受?
  4. 每个基本块的后继和前驱是否正确?

控制流图是后续 do_check() 逐条模拟执行的前提——没有有效的 CFG,就没有安全的路径遍历。

3.2 DFS 遍历算法

check_cfg() 使用深度优先搜索(DFS)遍历指令的跳转图:

/* Linux 6.6: kernel/bpf/verifier.c - check_cfg() 核心逻辑简化 */
static int check_cfg(struct bpf_verifier_env *env)
{
    struct bpf_insn *insns = env->prog->insnsi;
    int insn_cnt = env->prog->len;
    int *insn_state;     /* 每条指令的遍历状态 */
    int *insn_stack;     /* DFS 栈 */
    int i, w = 0, peek_insn = 0;
    int t;

    insn_state = kvcalloc(insn_cnt, sizeof(int), GFP_KERNEL);
    insn_stack = kvcalloc(insn_cnt, sizeof(int), GFP_KERNEL);

    /* 状态值 */
    /* 0 = NOT_VISITED, 1 = EXPLORING, 2 = DONE */

    /* 将入口基本块推入栈 */
    insn_state[0] = EXPLORING;
    insn_stack[0] = 0;
    w = 1;   /* 栈指针 */

    while (w > 0) {
        t = insn_stack[w - 1];   /* peek 栈顶指令索引 */
        ...

核心逻辑从指令 0(入口)开始,追踪所有跳转边:

对于每条指令 t:
  - 如果 t 是 BPF_JA (无条件跳转):边 t → t + off + 1
  - 如果 t 是条件跳转 (BPF_JEQ)
      两条边:t → t + 1 (fallthrough)  t → t + off + 1 (branch)
  - 如果 t 是 BPF_EXIT 或 caller 的末尾:叶子节点
  - 否则:单条边 t → t + 1 (顺序执行)

算法检测以下错误:

3.3 insn_aux_data 的元数据填充

check_cfg() 在遍历过程中为每条指令填充 insn_aux_data

/* kernel/bpf/verifier.c - insn_aux_data 结构体(简化) */
struct bpf_insn_aux_data {
    union {
        struct bpf_loop_info loop_info;    /* 循环信息 */
    };
    u64 seen;                               /* 指令被访问的次数 */
    u32 loop_depth;                         /* 当前循环嵌套深度 */
    bool goto_visited;                      /* 跳转目标是否已访问 */
    ...
};

循环深度跟踪对于 verifier 的性能至关重要——深度嵌套的循环导致状态爆炸,verifier 会在复杂度过高时拒绝程序。

四、do_check_main()do_check():主验证循环

4.1 入口设置

do_check_main() 为验证做最后的准备:

/* Linux 6.6: kernel/bpf/verifier.c - do_check_main() 简化 */
static int do_check_main(struct bpf_verifier_env *env)
{
    struct bpf_verifier_state *state;
    
    /* 创建入口状态 */
    state = kzalloc(sizeof(*state), GFP_KERNEL);
    
    /* 初始化 R1 = ctx (PTR_TO_CTX) */
    state->regs[BPF_REG_1].type = PTR_TO_CTX;
    state->regs[BPF_REG_1].off = 0;
    
    /* 初始化 R10 = frame pointer */
    state->regs[BPF_REG_10].type = PTR_TO_STACK;
    state->regs[BPF_REG_10].frameno = 0;
    
    /* 其他寄存器标记为 NOT_INIT */
    for (i = 0; i < MAX_BPF_REG; i++) {
        if (i != BPF_REG_1 && i != BPF_REG_10)
            mark_reg_not_init(env, state->regs, i);
    }
    
    /* 启动主循环——从基本块 0 开始 */
    ret = do_check(env);
    
    return ret;
}

关键状态初始化:

4.2 主循环结构

do_check() 是 verifier 的核心引擎——它逐条模拟执行 BPF 指令,跟踪每条指令执行后的寄存器/栈状态:

/* Linux 6.6: kernel/bpf/verifier.c - do_check() 循环简化 */
static int do_check(struct bpf_verifier_env *env)
{
    struct bpf_insn *insns = env->prog->insnsi;
    struct bpf_reg_state *regs = state->regs;
    int insn_cnt = env->prog->len;

    env->prev_insn_processed = 0;

    for (;;) {
        struct bpf_insn *insn;
        u32 class;
        int err;

        /* 1. 获取当前状态下的下一条指令 */
        insn = &insns[env->insn_idx];

        /* 2. 复杂度检查(每次处理前检查) */
        if (env->insn_processed >= BPF_COMPLEXITY_LIMIT_INSNS) {
            verbose(env, "BPF program is too large. "
                "Processed %d insn\n", env->insn_processed);
            return -E2BIG;
        }

        /* 3. 分类并分发到类型特定的检查器 */
        class = BPF_CLASS(insn->code);

        switch (class) {
        case BPF_ALU:
        case BPF_ALU64:
            err = check_alu_op(env, insn);
            break;

        case BPF_LDX:
            err = check_mem_access(env, insn_idx, insn->src_reg,
                         insn->off, BPF_SIZE(insn->code),
                         BPF_READ, insn->dst_reg, false, false);
            break;

        case BPF_STX:
            err = check_mem_access(env, insn_idx, insn->dst_reg,
                         insn->off, BPF_SIZE(insn->code),
                         BPF_WRITE, insn->src_reg, false, false);
            break;

        case BPF_ST:
            err = check_mem_access(env, insn_idx, insn->dst_reg,
                         insn->off, BPF_SIZE(insn->code),
                         BPF_WRITE, -1, false, false);
            break;

        case BPF_JMP:
        case BPF_JMP32:
            err = check_jmp_op(env, insn_idx, insn);
            break;

        case BPF_LD:
            err = check_ld_imm(env, insn);
            break;
        }

        /* 4. 错误检查 */
        if (err)
            return err;

        /* 5. 更新指令计数器 */
        env->insn_processed++;
    }
}

说明:以上为教学用简化伪代码。内核 do_check() 的实际主循环通过 pop_stack() 取待验证状态、更新 env->insn_idx,在 fallthrough 路径上顺序推进,遇到条件跳转时 push_stack() 保存分支;并非简单的 for 循环线性扫描全部指令。

4.3 分支处理:check_cond_jmp_op()

条件跳转是 verifier 最复杂的地方。当遇到条件跳转时,verifier 必须考虑两条路径(分支和 fallthrough):

/* kernel/bpf/verifier.c - check_cond_jmp_op() 简化 */
static int check_cond_jmp_op(struct bpf_verifier_env *env,
               struct bpf_insn *insn, int insn_idx)
{
    struct bpf_verifier_state *other_branch;
    int err;

    /* fork 当前状态——用于分支路径 */
    other_branch = push_stack(env, insn_idx + insn->off + 1, insn_idx);
    
    /* 在当前状态中,应用条件为 FALSE 时的范围约束(fallthrough) */
    /* 例如:if r0 > 5 ... 则 fallthrough 时 r0 <= 5 */
    find_good_pkt_pointers(other_branch, insn, insn->dst_reg);
    reg_set_min_max(&other_branch->regs[insn->dst_reg], ...);
    
    /* 在分支状态中,应用条件为 TRUE 时的范围约束 */
    /* 例如:if r0 > 5 ... 则分支中 r0 > 5 */
    reg_set_min_max_inv(&state->regs[insn->dst_reg], ...);

    /* 继续在当前路径上执行(fallthrough) */
    /* 分支路径中的状态会在之后被 pop 出来处理 */
    return 0;
}

push_stack() 创建一个新的验证状态,保存分支路径的寄存器/栈快照。当 verifier 完成了当前路径的验证(到达 EXIT 或遇到已探索状态),它通过 pop_stack() 回溯到之前的 fork 点,继续验证另一条路径。这套 DFS 状态探索逻辑内嵌在 do_check() + pop_stack() 中(详见第 03 篇)。

4.4 helper 调用检查

check_helper_call()do_check() 中最复杂的子函数之一:

/* 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_proto *fn;
    int meta_size, i;

    /* 1. 查找 helper 函数原型 */
    fn = env->prog->aux->ops->get_func_proto(insn->imm, env->prog);
    if (!fn) {
        verbose(env, "unknown func %s#%d\n", func_id_name(insn->imm), insn->imm);
        return -EINVAL;
    }

    /* 2. 验证每个参数的类型匹配 */
    for (i = 0; i < MAX_BPF_FUNC_REG_ARGS; i++) {  /* R1-R5 */
        if (fn->arg_type[i] == ARG_DONTCARE)
            continue;
        
        err = check_func_arg(env, i, &meta, fn, insn_idx_p);
        if (err)
            return err;
    }

    /* 3. R1-R5 在调用后被标记为 NOT_INIT */
    for (i = BPF_REG_1; i <= BPF_REG_5; i++)
        mark_reg_not_init(env, regs, i);

    /* 4. R0 根据 helper 的返回类型设置 */
    if (fn->ret_type == RET_PTR_TO_MAP_VALUE_OR_NULL)
        mark_reg_ptr_or_null(env, regs, BPF_REG_0, PTR_TO_MAP_VALUE);
    else if (fn->ret_type == RET_INTEGER)
        mark_reg_unknown(env, regs, BPF_REG_0);
    
    return 0;
}

helper 调用的验证步骤:

  1. 通过函数的 imm 字段查找 bpf_func_proto
  2. 遍历 R1–R5,检查每个参数的类型是否匹配(PTR_TO_CTX, PTR_TO_MAP_KEY, ARG_CONST_SIZE 等)。类型不匹配时返回错误。
  3. 调用后,R1–R5 被标记为 NOT_INIT——调用者不能信任 helper 调用后这些寄存器的值。
  4. R0 被设置为 helper 的返回类型和范围。例如 bpf_map_lookup_elem 返回 PTR_TO_MAP_VALUE_OR_NULL,verifier 同时设置 ref_obj_id 用于后续的 NULL 检查强制要求。

五、verifier 日志级别

verifier 日志是排障的最重要工具。通过 log_level 控制详细程度:

log_level 输出内容
0 无输出(默认)
1 基本日志:每个基本块的处理、错误信息
2 完整日志:每条指令执行后的寄存器状态、栈状态、分支探索、pruning 决策

获取日志的典型命令:

# 通过 bpftool 加载并获取完整日志
bpftool prog load my_prog.o /sys/fs/bpf/my_prog \
    type xdp \
    log_level 2 \
    log_buf /dev/stdout

# 或通过 bpftool 查看已加载程序的 verifier 日志
bpftool prog show id <PROG_ID> --verbose

日志解读示例:

0: (b7) r1 = 0
1: (7b) *(u64 *)(r10 - 8) = r1
2: (bf) r2 = r10
3: (07) r2 += -8
4: (b7) r3 = 0
5: (85) call bpf_map_lookup_elem#1
6: (15) if r0 == 0x0 goto pc+2
 R0=map_value(id=0,off=0,ks=4,vs=8,imm=0) R2_w=ptr_to_stack(id=0,off=0)
 R10=fp0
7: (61) r1 = *(u32 *)(r0 + 0)
8: (95) exit

from 6 to 9: R0=inv(id=0) R10=fp0
9: (b7) r0 = 0
10: (95) exit

注释: - 指令 6 之后:R0 被标记为 map_value(非 NULL 路径继续),R2 标记为 ptr_to_stack(带范围信息)。 - from 6 to 9:分支路径——当 R0 == NULL 时,跳转到指令 9,R0 被标记为 inv(类型变为 scalar,值为 0)。

六、Mermaid:BPF_PROG_LOAD 完整调用链

flowchart TD
    subgraph USER["用户态"]
        BPFTOOL["bpftool prog load<br/>my_prog.o"]
        LIBCALL["libbpf: bpf_object__load()"]
    end

    subgraph SYSCALL["bpf() 系统调用"]
        SYSCALL_ENTRY["__sys_bpf(BPF_PROG_LOAD)"]
        PROG_LOAD["bpf_prog_load()"]
    end

    subgraph VERIFY["verifier 框架 (kernel/bpf/verifier.c)"]
        B_CHECK["bpf_check()<br/>verifier 总控"]

        subgraph PHASE1["阶段 1: 控制流图"]
            CFG["check_cfg()<br/>DFS 遍历指令<br/>构建 CFG<br/>检测不可达代码"]
        end

        subgraph PHASE2["阶段 2: 子程序"]
            SUBPROG["check_subprogs()<br/>分析 BPF_PSEUDO_CALL<br/>标记子程序边界"]
        end

        subgraph PHASE3["阶段 3: 主验证"]
            DO_MAIN["do_check_main()<br/>初始化入口状态<br/>R1=PTR_TO_CTX<br/>R10=PTR_TO_STACK"]
            DO_CHECK["do_check()<br/>逐条指令模拟执行<br/>for each bpf_insn:"]
            CHECK_OPS["check_alu_op()<br/>check_mem_access()<br/>check_helper_call()<br/>check_cond_jmp_op()<br/>check_ld_imm()"]
        end

        subgraph PHASE4["阶段 4: 收尾"]
            STACK_DEPTH["check_max_stack_depth()<br/>验证栈深度 ≤ 512"]
        end
    end

    subgraph POST["验证通过后"]
        JIT["bpf_jit_compile()"]
        ALLOC_FD["分配文件描述符"]
        RETURN["返回 FD 给用户态"]
    end

    subgraph ERROR["验证失败"]
        LOG["生成 verifier log<br/>返回 -EACCES / -EINVAL"]
    end

    BPFTOOL --> LIBCALL --> SYSCALL_ENTRY --> PROG_LOAD
    PROG_LOAD --> B_CHECK
    B_CHECK --> CFG
    CFG -->|CFG 有效| SUBPROG
    CFG -->|不可达/循环| LOG
    SUBPROG --> DO_MAIN
    DO_MAIN --> DO_CHECK
    DO_CHECK --> CHECK_OPS
    CHECK_OPS -->|指令序列结束| STACK_DEPTH
    CHECK_OPS -->|检查失败| LOG
    STACK_DEPTH -->|栈深度 OK| JIT
    STACK_DEPTH -->|栈深度超限| LOG
    JIT --> ALLOC_FD --> RETURN

七、小结

verifier 框架不是黑盒魔法,而是一个结构清晰的四阶段流水线:

  1. check_cfg() 构建指令的控制流图,确保所有指令可达且所有路径都返回。这是最快速的失败点——如果程序有不可达代码或无限循环,在此阶段就会被拒绝。
  2. check_subprogs() 分析函数调用边界,标记子程序的入口和出口。
  3. do_check() 是核心引擎——逐条模拟执行每条 BPF 指令,调用类型特定的检查器(ALU、内存访问、helper 调用、条件跳转)。对于每个条件跳转,fork 出分支状态并在两条路径上继续检查。这是最耗时也最容易失败的阶段。
  4. check_max_stack_depth() 确保程序使用的栈空间不超过 512 字节。

bpf_verifier_env 伴随整个验证生命周期,cur_state 跟踪当前寄存器/栈状态,explored_states 记录已探索的程序点用于状态裁剪。这两者之间如何交互——也就是 verifier 如何通过抽象解释和状态等价判定来裁剪搜索空间——是第 03 篇的核心内容。

参考

内核源码(A 级,Linux 6.6)


上一篇BPF 指令集解码(第 01 篇)

下一篇验证器核心算法:抽象解释、状态跟踪与路径裁剪(第 03 篇)

同主题继续阅读

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

2026-06-12 · kernel / ebpf

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

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


By .