通过学习 eBPF Talk: eBPF helpers 的另一面,我们知道 eBPF helpers 是通过 bpf_func_proto 结构体来实现的,不同的 helpers 函数对应不同的 bpf_func_proto;甚至,对于同一个 helpers 函数,不同类型的 bpf prog 会有不同的 bpf_func_proto

对于 bpf map 的 helpers,在 runtime 时,会调用 bpf_map_lookup_elembpf_map_update_elembpf_map_delete_elem 等 helpers 的 bpf_func_proto 里的 func,还是会调用 bpf map 对应的 bpf_map_ops 里的 map_lookup_elemmap_update_elemmap_delete_elem 等函数呢?

内联 bpf map helpers

在该 commit 里,将 bpf map 的 helpers 函数编号直接转换为 bpf_map_ops 里的函数指针,从而避免了非常消耗性能的间接调用。

verifier.cbpf_check() 主函数最后阶段,即在做完所有检查之后,调用 fixup_bpf_calls() 函数修复 BPF_CALL 指令。

在该 commit 里,内联的具体实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// https://github.com/torvalds/linux/commit/09772d92cd5ad998b0d5f6f46cd1658f8cb698cf

/* fixup insn->imm field of bpf_call instructions
 * and inline eligible helpers as explicit sequence of BPF instructions
 *
 * this function is called after eBPF program passed verification
 */
static int fixup_bpf_calls(struct bpf_verifier_env *env)
{
    // ...

        /* BPF_EMIT_CALL() assumptions in some of the map_gen_lookup
         * and other inlining handlers are currently limited to 64 bit
         * only.
         */
        if (prog->jit_requested && BITS_PER_LONG == 64 &&
            insn->imm == BPF_FUNC_map_lookup_elem) {
            (insn->imm == BPF_FUNC_map_lookup_elem ||
             insn->imm == BPF_FUNC_map_update_elem ||
             insn->imm == BPF_FUNC_map_delete_elem)) {
            aux = &env->insn_aux_data[i + delta];
            if (bpf_map_ptr_poisoned(aux))
                goto patch_call_imm;

            map_ptr = BPF_MAP_PTR(aux->map_state);
            ops = map_ptr->ops;

            // ...

            switch (insn->imm) {
            case BPF_FUNC_map_lookup_elem:
                insn->imm = BPF_CAST_CALL(ops->map_lookup_elem) -
                        __bpf_call_base;
                continue;
            case BPF_FUNC_map_update_elem:
                insn->imm = BPF_CAST_CALL(ops->map_update_elem) -
                        __bpf_call_base;
                continue;
            case BPF_FUNC_map_delete_elem:
                insn->imm = BPF_CAST_CALL(ops->map_delete_elem) -
                        __bpf_call_base;
                continue;
            }

            goto patch_call_imm;
        }

    // ...
}

以上代码片段的处理逻辑:将 BPF_CALL 指令的 imm 值转换为相对 __bpf_call_base 的偏移量,为了给后面的 JIT 时或者解释执行时使用。

JIT 函数调用

不像解释执行 bpf insn,在 JIT 时就将 BPF_CALL 指令转换为函数调用。

P.S. 以下代码基于 bpf-next 分支的较新本版的内核仓库。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// ${KERNEL}/arch/x86/net/bpf_jit_comp.c

static int do_jit(struct bpf_prog *bpf_prog, int *addrs, u8 *image, u8 *rw_image,
          int oldproglen, struct jit_context *ctx, bool jmp_padding)
{
    // ...

        case BPF_JMP | BPF_CALL:
            func = (u8 *) __bpf_call_base + imm32;
            if (tail_call_reachable) {
                /* mov rax, qword ptr [rbp - rounded_stack_depth - 8] */
                EMIT3_off32(0x48, 0x8B, 0x85,
                        -round_up(bpf_prog->aux->stack_depth, 8) - 8);
                if (!imm32 || emit_call(&prog, func, image + addrs[i - 1] + 7))
                    return -EINVAL;
            } else {
                if (!imm32 || emit_call(&prog, func, image + addrs[i - 1]))
                    return -EINVAL;
            }
            break;

    // ...
}

static int emit_patch(u8 **pprog, void *func, void *ip, u8 opcode)
{
    u8 *prog = *pprog;
    s64 offset;

    offset = func - (ip + X86_PATCH_SIZE);
    if (!is_simm32(offset)) {
        pr_err("Target call %p is out of range\n", func);
        return -ERANGE;
    }
    EMIT1_off32(opcode, offset);
    *pprog = prog;
    return 0;
}

static int emit_call(u8 **pprog, void *func, void *ip)
{
    return emit_patch(pprog, func, ip, 0xE8); // 0xE8 is x86 call opcode
}

以上代码片段的处理逻辑:通过 BPF_CALL 指令的 imm 值加上 __bpf_call_base 得到目标函数的地址,然后通过 emit_call() 函数将目标函数的地址转为 x86 平台的 call 指令。

小结

通过上面的分析,我们可以看到:

  1. helpers 函数在编译时就被编译成带有函数编号的 BPF_CALL 指令。
  2. 在校验时,会将 BPF_CALL 指令的 imm 值转换为相对 __bpf_call_base 的偏移量。
  3. 在 JIT 时,会将 BPF_CALL 指令的 imm 值加上 __bpf_call_base 得到目标函数的地址,然后通过 emit_call() 函数将目标函数的地址转为 x86 平台的 call 指令。