本文不讲解 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;
。
在深入讲解 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 进行如下处理:
- 判断当前 eBPF 程序类型是否支持汇编指令
call xxx
中的 bpf helpers ID。
- 找到 bpf helpers ID 对应的函数。
- 重新计算汇编指令
call xxx
中 imm
字段的值,以备运行阶段使用。
似乎需要处理的事情不多,就来看下内核中的相关函数执行路径吧:
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_xxx 和
BPF_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_ops
的 get_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 大编译器 的最后端
的一部分:
- 校验汇编指令的合法性、安全性
- 进行一定的汇编指令优化(更多时候是 dead code 优化)
- 将汇编指令 JIT 翻译成机器码
- ……
目前,能力所限,只能按图索骥地阅读 verifier 的部分源代码。
终究还是对 verifier 望而生畏。