在上一集 eBPF Talk: 动态或静态 tailcall 中,我们知道了 tailcall 有动态和静态之分。

对于动态 tailcall 而言,更新 PROG_ARRAY bpf map 时就比较简单,只需要更新一下 array->ptrs 数组即可。

但对于静态 tailcall 而言,更新 PROG_ARRAY bpf map 时,是如何更新那些 bpf prog 的汇编指令的呢?

内核函数调用栈

直接翻看内核源码:

 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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
__sys_bpf()                                     // ${KERNEL}/kernel/bpf/syscall.c
|-->map_update_elem()
    |-->bpf_map_update_value()
        |-->bpf_fd_array_map_update_elem() {    // ${KERNEL}/kernel/bpf/arraymap.c
                ufd = *(u32 *)value;
                new_ptr = map->ops->map_fd_get_ptr(map, map_file, ufd);
                if (IS_ERR(new_ptr))
                    return PTR_ERR(new_ptr);

                if (map->ops->map_poke_run) {
                    mutex_lock(&array->aux->poke_mutex);
                    old_ptr = xchg(array->ptrs + index, new_ptr);
                    map->ops->map_poke_run(map, index, old_ptr, new_ptr);
                    mutex_unlock(&array->aux->poke_mutex);
                } else {
                    old_ptr = xchg(array->ptrs + index, new_ptr);
                }

                if (old_ptr)
                    map->ops->map_fd_put_ptr(old_ptr);
            }
           /
          /
         /
        |-->prog_array_map_poke_run() {         // ${KERNEL}/kernel/bpf/arraymap.c
                list_for_each_entry(elem, &aux->poke_progs, list) {
                    struct bpf_jit_poke_descriptor *poke;
                    int i, ret;

                    aux = container_of(map, struct bpf_array, map)->aux;

                    for (i = 0; i < elem->aux->size_poke_tab; i++) {
                        poke = &elem->aux->poke_tab[i];

                        // ...

                        if (poke->tail_call.map != map ||
                            poke->tail_call.key != key)
                            continue;

                        old_bypass_addr = old ? NULL : poke->bypass_addr;
                        old_addr = old ? (u8 *)old->bpf_func + poke->adj_off : NULL;
                        new_addr = new ? (u8 *)new->bpf_func + poke->adj_off : NULL;

                        if (new) {
                            ret = bpf_arch_text_poke(poke->tailcall_target,
                                        BPF_MOD_JUMP,
                                        old_addr, new_addr);
                            BUG_ON(ret < 0 && ret != -EINVAL);
                            if (!old) {
                                ret = bpf_arch_text_poke(poke->tailcall_bypass,
                                            BPF_MOD_JUMP,
                                            poke->bypass_addr,
                                            NULL);
                                BUG_ON(ret < 0 && ret != -EINVAL);
                            }
                        } else {
                            ret = bpf_arch_text_poke(poke->tailcall_bypass,
                                        BPF_MOD_JUMP,
                                        old_bypass_addr,
                                        poke->bypass_addr);
                            BUG_ON(ret < 0 && ret != -EINVAL);
                            /* let other CPUs finish the execution of program
                            * so that it will not possible to expose them
                            * to invalid nop, stack unwind, nop state
                            */
                            if (!ret)
                                synchronize_rcu();
                            ret = bpf_arch_text_poke(poke->tailcall_target,
                                        BPF_MOD_JUMP,
                                        old_addr, NULL);
                            BUG_ON(ret < 0 && ret != -EINVAL);
                        }
                    }
                }
            }

上面代码片段的关键地方在于:

  1. array->ptrs 数组保存的是 bpf prog 的指针。
  2. array->aux->poke_progs 链表保存的是 静态地 使用该 PROG_ARRAY bpf map 的 bpf prog 的 aux 信息。
  3. bpf prog 的 aux 里保存有 poke 数组。
  4. poke 信息里保存有两个关键地址:tailcall_bypasstailcall_target
  5. bpf_arch_text_poke() 用于更新汇编指令。

上面代码片段的主要逻辑是:

  1. 更新 array->ptrs 数组。
  2. 遍历 array->aux->poke_progs 链表,拿到 bpf prog 的 aux 信息。
  3. 遍历 aux->poke_tab 数组,更新 poke 信息里的 tailcall_bypasstailcall_target 地址上的汇编指令。

不过,poke 信息是何时添加到 array->aux->poke_progs 链表的呢?

添加 poke 信息

在 verifier 阶段,对 PROG_ARRAY bpf map 进行处理的时候,就会添加 poke 信息。

verifier 阶段较为复杂,在 JIT bpf prog 时生成 aux->poke_tab 数组;在 verifier 阶段后期的 do_misc_fixups() 函数中,会将 bpf prog 和 PROG_ARRAY bpf map 关联起来。

JIT bpf prog 时生成 aux->poke_tab 数组的代码如下:

 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
bpf_check()                                             // ${KERNEL}/kernel/bpf/verifier.c
|-->do_misc_fixups() {
|       if (insn->imm == BPF_FUNC_tail_call)
|           bpf_jit_add_poke_descriptor(prog, &desc)    // ${KERNEL}/kernel/bpf/core.c
|
|       /* Since poke tab is now finalized, publish aux to tracker. */
|       for (i = 0; i < prog->aux->size_poke_tab; i++) {
|           map_ptr = prog->aux->poke_tab[i].tail_call.map;
|           // ...
|           ret = map_ptr->ops->map_poke_track(map_ptr, prog->aux);
|           // ...
|       }
|   }
|    \
|     \
|      \
|       |-->prog_array_map_poke_track()                 // ${KERNEL}/kernel/bpf/arraymap.c
|           list_add_tail(&elem->list, &aux->poke_progs);
|-->fixup_call_args()
    |-->jit_subprogs()
        |-->bpf_int_jit_compile()                       // ${KERNEL}/arch/x86/net/bpf_jit_comp.c
            |-->do_jit() {
                    case BPF_JMP | BPF_TAIL_CALL:
                        if (imm32)
                            emit_bpf_tail_call_direct(&bpf_prog->aux->poke_tab[imm32 - 1],
                                        &prog, image + addrs[i - 1],
                                        callee_regs_used,
                                        bpf_prog->aux->stack_depth,
                                        ctx);
                        else
                            emit_bpf_tail_call_indirect(&prog,
                                            callee_regs_used,
                                            bpf_prog->aux->stack_depth,
                                            image + addrs[i - 1],
                                            ctx);
                        break;
                }
                |-->emit_bpf_tail_call_direct() {
                        poke->tailcall_bypass = ip + (prog - start);
                        poke->adj_off = X86_TAIL_CALL_OFFSET;
                        poke->tailcall_target = ip + ctx->tail_call_direct_label - X86_PATCH_SIZE;
                        poke->bypass_addr = (u8 *)poke->tailcall_target + X86_PATCH_SIZE;
                    }

上面代码片段的主要处理逻辑:

  1. 分配 aux->poke_tab 数组。
  2. 将 bpf prog 和 PROG_ARRAY bpf map 关联起来。
  3. JIT bpf prog 时,更新 poke 信息。

小结

通过上面的分析,我们知道了 PROG_ARRAY bpf map 的实现细节:

  1. 在 verifier 阶段准备好 bpf prog 的 aux->poke_tab 数组。
  2. 在更新 PROG_ARRAY bpf map 时,更新 poke 信息里的 tailcall_bypasstailcall_target 地址上的汇编指令。

尽管静态 tailcall 的实现细节繁多,相信你对静态 tailcall 有了更深的理解。

P.S. 从中学到了一点经验:控制逻辑让位于运行时;为了更高的运行时性能,带来更复杂的控制逻辑。

纸上得来终觉浅,绝知此事要躬行。弄个 demo 验证一下静态 tailcall 的实现细节:eBPF Talk: 共享的 tailcall PROG_ARRAY bpf map