bpf2bpf 里使用 tailcall 时,可以达到意想不到的效果:

  1. tailcall 目标 bpf prog 复用当前 subprog 的栈空间,而不是 subprog caller 的栈空间。
  2. subprog caller 能获取到 tailcall 目标 bpf prog 的返回值。

tailcall in bpf2bpf 支持情况

从 5.10 内核开始,x86 架构里 bpf2bpf 支持了 tailcall 特性,即在 bpf2bpf 调用链中,可以通过 tailcall 调用另一个 eBPF 程序。

至于其它架构的支持情况,请参考:

tailcall in bpf2bpf 范例

要在 bpf2bpf 里使用 tailcall,如下:

1
2
3
4
5
6
7
static __noinline int
do_tailcall(void *ctx)
{
    bpf_tail_call(ctx, &jmp_table, 1);

    return 0;
}

其中 __noinline 表示当前定义的函数不会被内联,即会生成一个函数调用;函数调用的目标是一个 bpf prog。更多 bpf2bpf 内容请查看 bpf2bpf 合集

bpf2bpf 里使用 bpf_tail_call() helper 即可。

tailcall in bpf2bpf 踩坑分析

如果要获取 bpf_tail_call() 目标 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
struct {
    __uint(type, BPF_MAP_TYPE_PROG_ARRAY);
    __uint(max_entries, 1);
    __uint(key_size, sizeof(__u32));
    __uint(value_size, sizeof(__u32));
} prog_array SEC(".maps");

SEC("xdp")
int xdp_prog1(struct xdp_md *ctx)
{
    bpf_printk("tailcall-in-bpf2bpf: xdp_prog1\n");

    return XDP_PASS;
}

static __noinline int
tailcall1(struct xdp_md *ctx)
{
    int retval = XDP_ABORTED;

    bpf_tail_call_static(ctx, &prog_array, 0);

    return retval;
}

static __noinline int
tailcall2(struct xdp_md *ctx)
{
    volatile int retval = XDP_ABORTED;

    bpf_tail_call_static(ctx, &prog_array, 0);

    return retval;
}

SEC("xdp")
int xdp_entry(struct xdp_md *ctx)
{
    struct ethhdr *eth;
    struct iphdr *iph;
    int retval;

    eth = (struct ethhdr *) ctx_ptr(ctx, data);
    iph = (struct iphdr *)(eth + 1);

    if ((void *) (iph + 1) > ctx_ptr(ctx, data_end))
        return XDP_PASS;

    if (eth->h_proto != bpf_htons(ETH_P_IP) || iph->protocol != IPPROTO_ICMP)
        return XDP_PASS;

    bpf_printk("tailcall-in-bpf2bpf: xdp_entry\n");

    retval = tailcall1(ctx);
    bpf_printk("tailcall-in-bpf2bpf: tailcall1 retval: %d (0:aborted 1:drop 2:pass 3:tx 4:redirect)\n", retval);

    retval = tailcall2(ctx);
    bpf_printk("tailcall-in-bpf2bpf: tailcall2 retval: %d (0:aborted 1:drop 2:pass 3:tx 4:redirect)\n", retval);

    return XDP_PASS;
}

在上面的例子中,tailcall1()tailcall2() 都调用了 bpf_tail_call_static(),并且 tailcall1()tailcall2() 的返回值都是 XDP_ABORTED。但在 xdp_entry() 中,tailcall1()tailcall2() 的返回值却不一样:

1
2
3
4
5
6
# cat /sys/kernel/debug/tracing/trace_pipe
          <idle>-0       [001] ..s21 649220.920996: bpf_trace_printk: tailcall-in-bpf2bpf: xdp_entry
          <idle>-0       [001] .Ns21 649220.921020: bpf_trace_printk: tailcall-in-bpf2bpf: xdp_prog1
          <idle>-0       [001] .Ns21 649220.921021: bpf_trace_printk: tailcall-in-bpf2bpf: tailcall1 retval: 0 (0:aborted 1:drop 2:pass 3:tx 4:redirect)
          <idle>-0       [001] .Ns21 649220.921022: bpf_trace_printk: tailcall-in-bpf2bpf: xdp_prog1
          <idle>-0       [001] .Ns21 649220.921022: bpf_trace_printk: tailcall-in-bpf2bpf: tailcall2 retval: 2 (0:aborted 1:drop 2:pass 3:tx 4:redirect)

为什么 tailcall1()tailcall2() 的返回值不一样呢?

为什么加了 volatile 修饰符后,tailcall2() 的返回值就是 XDP_PASS 了呢?

答案隐藏在汇编中:

P.S. 使用 go-ebpf Instructions.Format 写的 elfdump 工具,可以打印 bpf object 文件里的汇编代码。

 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
77
78
79
80
81
# elfdump tailcall_bpfel.o
2024/01/07 16:16:42 Program xdp_prog1:
xdp_prog1:
         ; bpf_printk("tailcall-in-bpf2bpf: xdp_prog1\n");
        0: LoadMapValue dst: r1, fd: 0 off: 0 <.rodata>
        2: MovImm dst: r2 imm: 32
        3: Call FnTracePrintk
         ; return XDP_PASS;
        4: MovImm dst: r0 imm: 2
        5: Exit
2024/01/07 16:16:42 Program xdp_entry:
xdp_entry:
          ; int xdp_entry(struct xdp_md *ctx)
         0: MovReg dst: r6 src: r1
          ; eth = (struct ethhdr *) ctx_ptr(ctx, data);
         1: LdXMemW dst: r1 src: r6 off: 0 imm: 0
          ; if ((void *) (iph + 1) > ctx_ptr(ctx, data_end))
         2: LdXMemW dst: r2 src: r6 off: 4 imm: 0
          ; if ((void *) (iph + 1) > ctx_ptr(ctx, data_end))
         3: MovReg dst: r3 src: r1
         4: AddImm dst: r3 imm: 34
          ; if ((void *) (iph + 1) > ctx_ptr(ctx, data_end))
         5: JGTReg dst: r3 off: 23 src: r2
          ; if (eth->h_proto != bpf_htons(ETH_P_IP) || iph->protocol != IPPROTO_ICMP)
         6: LdXMemH dst: r2 src: r1 off: 12 imm: 0
          ; if (eth->h_proto != bpf_htons(ETH_P_IP) || iph->protocol != IPPROTO_ICMP)
         7: JNEImm dst: r2 off: 21 imm: 8
         8: AddImm dst: r1 imm: 14
          ; if (eth->h_proto != bpf_htons(ETH_P_IP) || iph->protocol != IPPROTO_ICMP)
         9: LdXMemB dst: r1 src: r1 off: 9 imm: 0
          ; if (eth->h_proto != bpf_htons(ETH_P_IP) || iph->protocol != IPPROTO_ICMP)
        10: JNEImm dst: r1 off: 18 imm: 1
          ; bpf_printk("tailcall-in-bpf2bpf: xdp_entry\n");
        11: LoadMapValue dst: r1, fd: 0 off: 32 <.rodata>
        13: MovImm dst: r2 imm: 32
        14: Call FnTracePrintk
          ; retval = tailcall1(ctx);
        15: MovReg dst: r1 src: r6
        16: Call -1 <tailcall1>
          ; bpf_printk("tailcall-in-bpf2bpf: tailcall1 retval: %d (0:aborted 1:drop 2:pass 3:tx 4:redirect)\n", retval);
        17: LoadMapValue dst: r1, fd: 0 off: 64 <.rodata>
        19: MovImm dst: r2 imm: 85
        20: MovImm dst: r3 imm: 0
        21: Call FnTracePrintk
          ; retval = tailcall2(ctx);
        22: MovReg dst: r1 src: r6
        23: Call -1 <tailcall2>
          ; bpf_printk("tailcall-in-bpf2bpf: tailcall2 retval: %d (0:aborted 1:drop 2:pass 3:tx 4:redirect)\n", retval);
        24: LoadMapValue dst: r1, fd: 0 off: 149 <.rodata>
        26: MovImm dst: r2 imm: 85
        27: MovReg dst: r3 src: r0
        28: Call FnTracePrintk
          ; }
        29: MovImm dst: r0 imm: 2
        30: Exit
tailcall1:
          ; tailcall1(struct xdp_md *ctx)
        31: MovReg dst: r6 src: r1
          ; asm volatile("r1 = %[ctx]\n\t"
        32: LoadMapPtr dst: r7 fd: 0 <prog_array>
        34: MovReg dst: r1 src: r6
        35: MovReg dst: r2 src: r7
        36: MovImm dst: r3 imm: 0
        37: Call FnTailCall
          ; return retval;
        38: Exit
tailcall2:
          ; tailcall2(struct xdp_md *ctx)
        39: MovReg dst: r6 src: r1
        40: MovImm dst: r1 imm: 0
          ; volatile int retval = XDP_ABORTED;
        41: StXMemW dst: rfp src: r1 off: -4 imm: 0
          ; asm volatile("r1 = %[ctx]\n\t"
        42: LoadMapPtr dst: r7 fd: 0 <prog_array>
        44: MovReg dst: r1 src: r6
        45: MovReg dst: r2 src: r7
        46: MovImm dst: r3 imm: 0
        47: Call FnTailCall
          ; return retval;
        48: LdXMemW dst: r0 src: rfp off: -4 imm: 0
        49: Exit

先看 tailcall1() 的汇编代码、及其调用处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xdp_entry:
          ; int xdp_entry(struct xdp_md *ctx)
         0: MovReg dst: r6 src: r1
         # ...
        15: MovReg dst: r1 src: r6
        16: Call -1 <tailcall1>
          ; bpf_printk("tailcall-in-bpf2bpf: tailcall1 retval: %d (0:aborted 1:drop 2:pass 3:tx 4:redirect)\n", retval);
        17: LoadMapValue dst: r1, fd: 0 off: 64 <.rodata>
        19: MovImm dst: r2 imm: 43
        20: MovImm dst: r3 imm: 0
        21: Call FnTracePrintk
         # ...
tailcall1:
          ; tailcall1(struct xdp_md *ctx)
        31: MovReg dst: r6 src: r1
          ; asm volatile("r1 = %[ctx]\n\t"
        32: LoadMapPtr dst: r7 fd: 0 <prog_array>
        34: MovReg dst: r1 src: r6
        35: MovReg dst: r2 src: r7
        36: MovImm dst: r3 imm: 0
        37: Call FnTailCall
          ; return retval;
        38: Exit

可以分析出,在 xdp_entry() 里,压根就没有处理 tailcall1() 的返回值,而是直接使用了 tailcall1() 的返回值:20: MovImm dst: r3 imm: 0,即使在 tailcall1() 调用了 bpf_tail_call()

再看 tailcall2() 的汇编代码、及其调用处理:

 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
xdp_entry:
          ; int xdp_entry(struct xdp_md *ctx)
         0: MovReg dst: r6 src: r1
         # ...
          ; retval = tailcall2(ctx);
        22: MovReg dst: r1 src: r6
        23: Call -1 <tailcall2>
          ; bpf_printk("tailcall-in-bpf2bpf: tailcall2 retval: %d (0:aborted 1:drop 2:pass 3:tx 4:redirect)\n", retval);
        24: LoadMapValue dst: r1, fd: 0 off: 107 <.rodata>
        26: MovImm dst: r2 imm: 43
        27: MovReg dst: r3 src: r0
        28: Call FnTracePrintk
          ; }
        29: MovImm dst: r0 imm: 2
        30: Exit
tailcall2:
          ; tailcall2(struct xdp_md *ctx)
        39: MovReg dst: r6 src: r1
        40: MovImm dst: r1 imm: 0
          ; volatile int retval = XDP_ABORTED;
        41: StXMemW dst: rfp src: r1 off: -4 imm: 0
          ; asm volatile("r1 = %[ctx]\n\t"
        42: LoadMapPtr dst: r7 fd: 0 <prog_array>
        44: MovReg dst: r1 src: r6
        45: MovReg dst: r2 src: r7
        46: MovImm dst: r3 imm: 0
        47: Call FnTailCall
          ; return retval;
        48: LdXMemW dst: r0 src: rfp off: -4 imm: 0
        49: Exit

对比 tailcall1() 的情况,可以分析出 xdp_entry() 里,处理了 tailcall2() 的返回值:27: MovReg dst: r3 src: r0

再回头来对比 tailcall1()tailcall2() 有何不同:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
static __noinline int
tailcall1(struct xdp_md *ctx)
{
    int retval = XDP_ABORTED;

    bpf_tail_call_static(ctx, &prog_array, 0);

    return retval;
}

static __noinline int
tailcall2(struct xdp_md *ctx)
{
    volatile int retval = XDP_ABORTED;

    bpf_tail_call_static(ctx, &prog_array, 0);

    return retval;
}

tailcall2() 在定义 retval 时,使用了 volatile 修饰符,而 tailcall1() 没有使用 volatile 修饰符。

在 bpf 里,使用 volatile 修饰的变量,都会保存到栈上,无论读写都会访问栈空间。

所以,tailcall2() 会比 tailcall1() 多一次栈操作:

 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
tailcall1:
          ; tailcall1(struct xdp_md *ctx)
        31: MovReg dst: r6 src: r1
          ; asm volatile("r1 = %[ctx]\n\t"
        32: LoadMapPtr dst: r7 fd: 0 <prog_array>
        34: MovReg dst: r1 src: r6
        35: MovReg dst: r2 src: r7
        36: MovImm dst: r3 imm: 0
        37: Call FnTailCall
          ; return retval;
        38: Exit
tailcall2:
          ; tailcall2(struct xdp_md *ctx)
        39: MovReg dst: r6 src: r1
        40: MovImm dst: r1 imm: 0
          ; volatile int retval = XDP_ABORTED;
        41: StXMemW dst: rfp src: r1 off: -4 imm: 0
          ; asm volatile("r1 = %[ctx]\n\t"
        42: LoadMapPtr dst: r7 fd: 0 <prog_array>
        44: MovReg dst: r1 src: r6
        45: MovReg dst: r2 src: r7
        46: MovImm dst: r3 imm: 0
        47: Call FnTailCall
          ; return retval;
        48: LdXMemW dst: r0 src: rfp off: -4 imm: 0 # <--- 读取栈空间
        49: Exit

但因为 tailcall2() 里调用了 bpf_tail_call(),所以这条读取栈空间的指令并不会被执行。

再看 xdp_prog1() 的汇编代码:

1
2
3
4
5
6
7
8
xdp_prog1:
         ; bpf_printk("tailcall-in-bpf2bpf: xdp_prog1\n");
        0: LoadMapValue dst: r1, fd: 0 off: 0 <.rodata>
        2: MovImm dst: r2 imm: 32
        3: Call FnTracePrintk
         ; return XDP_PASS;
        4: MovImm dst: r0 imm: 2
        5: Exit

xdp_prog1() 的最后 MovImm dst: r0 imm: 2,将 XDP_PASS 的值保存到了 r0 寄存器中。

所以,当 xdp_entry() 读取 r0 时,就读取到了 XDP_PASS 的值。

P.S. demo 源代码:GitHub learn-by-example/tailcall-in-bpf2bpf

小结

bpf2bpf 里使用 tailcall 时,可以达到意想不到的效果:bpf2bpf 的调用者能够获取 tailcall 目标 bpf prog 的返回值。

为了达到这预期出人意料的效果,就需要在 bpf2bpf 里使用 volatile 修饰符修饰一下返回值变量,从而阻止编译器优化掉对 bpf2bpf 函数返回值的处理。