通过学习 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_elem
、bpf_map_update_elem
、bpf_map_delete_elem
等 helpers 的 bpf_func_proto
里的 func
,还是会调用 bpf map 对应的 bpf_map_ops
里的 map_lookup_elem
、map_update_elem
、map_delete_elem
等函数呢?
内联 bpf map helpers
在该 commit 里,将 bpf map 的 helpers 函数编号直接转换为 bpf_map_ops
里的函数指针,从而避免了非常消耗性能的间接调用。
在 verifier.c
的 bpf_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
指令。
小结
通过上面的分析,我们可以看到:
- helpers 函数在编译时就被编译成带有函数编号的
BPF_CALL
指令。
- 在校验时,会将
BPF_CALL
指令的 imm
值转换为相对 __bpf_call_base
的偏移量。
- 在 JIT 时,会将
BPF_CALL
指令的 imm
值加上 __bpf_call_base
得到目标函数的地址,然后通过 emit_call()
函数将目标函数的地址转为 x86 平台的 call
指令。