在 bpf2bpf
里使用 tailcall
时,可以达到意想不到的效果:
tailcall
目标 bpf prog 复用当前 subprog 的栈空间,而不是 subprog caller 的栈空间。
- 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
函数返回值的处理。