此汇编:在 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。)从而进一步加深了对 彼汇编 的理解。

小结

不断折腾,不断发现新天地。