此汇编:在 C 代码里使用的 asm volatile
汇编代码。
彼汇编:eBPF verifier、JIT、runtime VM 等地方使用的汇编指令。
在文章 BPF 尾调用简介 里,我照猫画虎地尝试了下_此汇编_:
使用 asm volatile
去实现在 bpf_tail_call()
前后两个函数间传递变量。结果失败了。
此汇编 的尝试
在使用 bpf_tail_call()
的时候,嫌弃使用 per-CPU map 去传递变量比较繁琐,有没有更简便的方法呢?
想到了在 C 代码里内嵌汇编代码,因为纯粹的 C 代码是不能直接操作寄存器的。譬如 bpf_helpers.c
里的 bpf_tail_call_static()
:
1
2
3
4
5
6
7
8
9
10
11
|
static __always_inline void
bpf_tail_call_static(void *ctx, const void *map, const __u32 slot)
{
// ...
asm volatile("r1 = %[ctx]\n\t"
"r2 = %[map]\n\t"
"r3 = %[slot]\n\t"
"call 12"
:: [ctx]"r"(ctx), [map]"r"(map), [slot]"i"(slot)
: "r0", "r1", "r2", "r3", "r4", "r5");
}
|
那就依样画葫芦,使用汇编通过寄存器去传递变量吧。
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
|
SEC("kprobe/handle_new_connection")
int handle_new_connection(void *ctx)
{
struct sock *sk = NULL;
asm volatile(
"%[sk] = r9\n\t"
: [sk] "=r"(sk)
:
: "r9");
event_t ev = {};
ev.saddr = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
ev.daddr = BPF_CORE_READ(sk, __sk_common.skc_daddr);
ev.sport = BPF_CORE_READ(sk, __sk_common.skc_num);
ev.dport = bpf_ntohs(BPF_CORE_READ(sk, __sk_common.skc_dport));
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &ev, sizeof(ev));
return 0;
}
SEC("kprobe/tcp_connect")
int k_tcp_connect(struct pt_regs *ctx)
{
struct sock *sk;
sk = (typeof(sk))PT_REGS_PARM1(ctx);
asm volatile(
"r9 = %[sk]\n\t"
:
: [sk] "r"(sk)
: "r9");
bpf_tail_call_static(ctx, &progs, 0);
return 0;
}
|
Emm,想像很丰满,现实很骨感。这段代码成功通过了编译阶段,却折戟于校验阶段。
1
|
Failed to load bpf obj: field HandleNewConnection: program handle_new_connection: load program: permission denied: 1: (bf) r8 = r9: R9 !read_ok (5 line(s) omitted)
|
这日志的意思是,尾调用的目标 eBPF 程序 handle_new_connection
没有通过校验,因为寄存器 R9 不可读。
相比 per-CPU map 的方式,asm volatile
的实现是多么优美,却没卵用。
成功编写了 asm volatile
汇编代码,但看不懂啊。那就查资料学习一波吧。
彼汇编 的学习
学习 彼汇编 时,没有捷径可言。
我是通过阅读 ___bpf_prog_run()
的源代码理解了 彼汇编。并且,参考 ___bpf_prog_run()
,使用 Go 去模拟了一个能够执行 彼汇编 的 VM,包括寄存器和栈哦。(P.S. 该 VM 的源代码并未上传到 GitHub。)从而进一步加深了对 彼汇编 的理解。
小结
不断折腾,不断发现新天地。