本文不讲解 bpf helpers 的使用,也不讲解 bpf helpers 的源代码。本文讲解的是,verifier 是怎么处理 bpf helpers 的

书接上回 eBPF Talk: 揭秘 XDP 转发网络包,本文解答: 为什么 XDP 具体的 bpf_redirect() 函数的实现是 bpf_xdp_redirect() 而不是 bpf_redirect() 呢?

bpf helpers ID

bpf helpers ID 是什么?简单来说,是内核跟 eBPF 程序之间关于 bpf helpers 的一个约定:

  • 约定 eBPF 程序中如何使用 bpf helpers。
  • 约定内核在执行 eBPF 汇编指令时如何调用对应的函数。

所以,bpf helpers ID 是约定俗成的东西,请查看 bpf_helper_defs.h。 对应内核中的源代码 enum bpf_func_id

例如,void *val = bpf_map_lookup_elem(map, key); 在编译后用命令行 llvm-objdump -S xxx.o 可以参考有汇编 call 1;其中,1 就是 bpf_map_lookup_elem()static void *(*bpf_map_lookup_elem)(void *map, const void *key) = (void *) 1;

bpftool feature

在深入讲解 verifier 是怎么处理 bpf helpers 的 之前,有必要简单介绍一下 bpftool feature。 执行命令行 bpftool feature 后,可以看到命令行输出的大部分内容都是某类型的 eBPF 程序支持哪些 bpf helpers。譬如 XDP 程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ebpf helpers supported for program type xdp:
        - bpf_map_lookup_elem
        - bpf_map_update_elem
        - bpf_map_delete_elem
        - bpf_redirect
        - bpf_xdp_adjust_head
        - bpf_redirect_map
        - bpf_xdp_adjust_meta
        - bpf_xdp_adjust_tail
        - ...

这对于日常编写 eBPF 程序 C 代码是非常有用的,能够避免等到校验阶段报错的时间浪费。

bpf verifier

简单推理可知,编译阶段、加载阶段都无需对 bpf helpers 进行特定处理。而在校验阶 段,需要对 bpf helpers 进行如下处理:

  1. 判断当前 eBPF 程序类型是否支持汇编指令 call xxx 中的 bpf helpers ID。
  2. 找到 bpf helpers ID 对应的函数。
  3. 重新计算汇编指令 call xxximm 字段的值,以备运行阶段使用。

似乎需要处理的事情不多,就来看下内核中的相关函数执行路径吧:

 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
SYSCALL_DEFINE3(bpf) // ${KERNEL}/kernel/bpf/syscall.c
|-->bpf_prog_load()
    |-->bpf_check()  // ${KERNEL}/kernel/bpf/verifier.c
        |-->env->ops = bpf_verifier_ops[env->prog->type]; // 关键步骤
        |-->do_check_main()
        |   |-->do_check_common()
        |       |-->do_check()
        |           |-->if (opcode == BPF_CALL) {
        |           |       if (insn->src_reg == BPF_PSEUDO_CALL)
        |           |           err = check_func_call(env, insn, &env->insn_idx); // bpf2bpf functions call 走这里
        |           |       else
        |           |           err = check_helper_call(env, insn->imm, env->insn_idx); // bpf helpers 走这里
        |           |   }
        |           |-->check_helper_call() // 步骤 1
        |               |-->if (func_id < 0 || func_id >= __BPF_FUNC_MAX_ID) {
        |                       verbose(env, "invalid func %s#%d\n", func_id_name(func_id),
        |                           func_id);
        |                       return -EINVAL;
        |                   }
        |
        |                   if (env->ops->get_func_proto)
        |                       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;
        |                   }
        |
        |                   // 后面还有很多检查工作,略过
        |-->fixup_bpf_calls()
            |-->fn = env->ops->get_func_proto(insn->imm, env->prog); // 步骤 2
                /* all functions that have prototype and verifier allowed
                * programs to call them, must be real in-kernel functions
                */
                if (!fn->func) {
                    verbose(env,
                        "kernel subsystem misconfigured func %s#%d\n",
                        func_id_name(insn->imm), insn->imm);
                    return -EFAULT;
                }
                insn->imm = fn->func - __bpf_call_base; // 步骤 3

注册 bpf_verifier_ops

以下分析以 XDP 程序为例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// ${KERNEL}/kernel/bpf/verifier.c

static const struct bpf_verifier_ops * const bpf_verifier_ops[] = {
#define BPF_PROG_TYPE(_id, _name, prog_ctx_type, kern_ctx_type) \
    [_id] = & _name ## _verifier_ops,
#define BPF_MAP_TYPE(_id, _ops)
#define BPF_LINK_TYPE(_id, _name)
#include <linux/bpf_types.h>
#undef BPF_PROG_TYPE
#undef BPF_MAP_TYPE
#undef BPF_LINK_TYPE
};

eBPF Talk: bpf map 源码导读 中有提到 bpf_types.h,该头文件里列举 了所有 eBPF 程序类型和 eBPF map 类型。其中 BPF_PROG_TYPE_xxxBPF_MAP_TYPE_xxx 的定义都在 ${KERNEL}/include/uapi/linux/bpf.h 头文件中。

其中 XDP 程序类型的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ${KERNEL}/include/linux/bpf_types.h

BPF_PROG_TYPE(BPF_PROG_TYPE_XDP, xdp,
          struct xdp_md, struct xdp_buff)

// 等价于

static const struct bpf_verifier_ops * const bpf_verifier_ops[] = {
    [BPF_PROG_TYPE_XDP] = &xdp_verifier_ops,
};

这就好办了,继续往下看 xdp_verifier_ops

Tips: 通过 xxx_verifier_ops 可以准确找到 eBPF 程序类型所支持的 bpf helpers 的具体函数的源代码。

1
2
3
4
5
6
7
8
// ${KERNEL}/net/core/filter.c

const struct bpf_verifier_ops xdp_verifier_ops = {
    .get_func_proto     = xdp_func_proto,
    .is_valid_access    = xdp_is_valid_access,
    .convert_ctx_access = xdp_convert_ctx_access,
    .gen_prologue       = bpf_noop_prologue,
};

对于步骤 1 和 2,会调用 xdp_verifier_opsget_func_proto 函数,即 xdp_func_proto 函数。

 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
50
// ${KERNEL}/net/core/filter.c

static const struct bpf_func_proto *
xdp_func_proto(enum bpf_func_id func_id, const struct bpf_prog *prog)
{
    switch (func_id) {

    // 以下所有 case 都是 XDP 专门实现的函数,譬如 BPF_FUNC_redirect 会调用 bpf_xdp_redirect() 函数

    case BPF_FUNC_perf_event_output:
        return &bpf_xdp_event_output_proto;
    case BPF_FUNC_get_smp_processor_id:
        return &bpf_get_smp_processor_id_proto;
    case BPF_FUNC_csum_diff:
        return &bpf_csum_diff_proto;
    case BPF_FUNC_xdp_adjust_head:
        return &bpf_xdp_adjust_head_proto;
    case BPF_FUNC_xdp_adjust_meta:
        return &bpf_xdp_adjust_meta_proto;
    case BPF_FUNC_redirect:
        return &bpf_xdp_redirect_proto;
    case BPF_FUNC_redirect_map:
        return &bpf_xdp_redirect_map_proto;
    case BPF_FUNC_xdp_adjust_tail:
        return &bpf_xdp_adjust_tail_proto;
    case BPF_FUNC_fib_lookup:
        return &bpf_xdp_fib_lookup_proto;
#ifdef CONFIG_INET
    case BPF_FUNC_sk_lookup_udp:
        return &bpf_xdp_sk_lookup_udp_proto;
    case BPF_FUNC_sk_lookup_tcp:
        return &bpf_xdp_sk_lookup_tcp_proto;
    case BPF_FUNC_sk_release:
        return &bpf_sk_release_proto;
    case BPF_FUNC_skc_lookup_tcp:
        return &bpf_xdp_skc_lookup_tcp_proto;
    case BPF_FUNC_tcp_check_syncookie:
        return &bpf_tcp_check_syncookie_proto;
    case BPF_FUNC_tcp_gen_syncookie:
        return &bpf_tcp_gen_syncookie_proto;
#endif

    default:

    // 其他函数则包括 sk 相关的一些基础函数、和 eBPF map 相关的函数,还有一众基础函数。
    // 此处不再展开 bpf_sk_base_func_proto()

        return bpf_sk_base_func_proto(func_id); // ${KERNEL}/kernel/bpf/helpers.c
    }
}

bpf_xdp_redirect()

BPF_FUNC_redirect 为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ${KERNEL}/net/core/filter.c

BPF_CALL_2(bpf_xdp_redirect, u32, ifindex, u64, flags)
{
    struct bpf_redirect_info *ri = this_cpu_ptr(&bpf_redirect_info);

    if (unlikely(flags))
        return XDP_ABORTED;

    ri->flags = flags;
    ri->tgt_index = ifindex;
    ri->tgt_value = NULL;
    WRITE_ONCE(ri->map, NULL);

    return XDP_REDIRECT;
}

static const struct bpf_func_proto bpf_xdp_redirect_proto = {
    .func           = bpf_xdp_redirect,
    .gpl_only       = false,
    .ret_type       = RET_INTEGER,
    .arg1_type      = ARG_ANYTHING,
    .arg2_type      = ARG_ANYTHING,
};

此时,便能非常有信心地回答 eBPF Talk: 揭秘 XDP 转发网络包 中的那个 为什么 了。

运行阶段调用 bpf helpers

一切皆已就绪,运行阶段就比较简单了,通过 insn->imm 计算得到目标函数,执行函数;也就一行代码完事。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// ${KERNEL}/kernel/bpf/core.c
___bpf_prog_run()
|-->JMP_CALL:
    /* Function call scratches BPF_R1-BPF_R5 registers,
        * preserves BPF_R6-BPF_R9, and stores return value
        * into BPF_R0.
        */
    BPF_R0 = (__bpf_call_base + insn->imm)(BPF_R1, BPF_R2, BPF_R3,
                            BPF_R4, BPF_R5);
    CONT;

小结

只看了一部分 verifier 的源代码,觉得 verifier 就是 eBPF 大编译器 的最后端 的一部分:

  1. 校验汇编指令的合法性、安全性
  2. 进行一定的汇编指令优化(更多时候是 dead code 优化)
  3. 将汇编指令 JIT 翻译成机器码
  4. ……

目前,能力所限,只能按图索骥地阅读 verifier 的部分源代码。

终究还是对 verifier 望而生畏。