花了几天时间,将 eBPF vm on eBPF 的想法实现了一下,这是一个非常有趣的想法。以前用 Go 实现过 eBPF vm,所以这回轻车熟路,很快就实现了一个 eBPF vm on eBPF demo。

该想法的灵感来源于 “eBPF Talk 读者群” 的一位大佬,他在群里说,可以在 eBPF vm 里跑一个 mini vm;结合最近在修的 bpf 子系统 issue,该 issue 可用来延长 bpf prog 的运行时间,何不在 eBPF vm 里跑一个 eBPF vm 呢?

eBPF vm on eBPF 是什么?

eBPF vm on eBPF 是一个跑在已有 eBPF vm 上的 eBPF vm,复用已有的指令集,以解释器的形式逐条执行指令。

根据 demo 实践,eBPF vm on eBPF 的指令集是已有指令集的子集,不支持以下指令:

  • BPF_PROBE_MEM 相关指令
  • atomic 相关指令
  • 非 bpf2bpf 的 CALL 指令,包括 tailcall

因为无法像 eBPF vm 一样可以直接读写内核内存,所以 eBPF vm on eBPF 对内存读写做了限制:只允许读写栈内存。从而,导致 eBPF vm on eBPF 无法支持 bpf maps 和 bpf helpers。

eBPF vm on eBPF 怎么实现的?

eBPF vm on eBPF 作为一个 vm,也有其组成部分:

  • 程序区:存放待执行的 bpf prog
  • 栈空间:存放 bpf prog 执行过程中的临时数据
  • 寄存器:一一对应 eBPF vm 的寄存器

执行过程便是:从程序区取出一条指令,解析并执行,然后取出下一条指令,如此循环,直到程序区为空;指令可操作寄存器和栈空间。

eBPF vm on eBPF bpf2bpf 函数调用

在 bpf C 代码里,函数有助于代码复用,使用 __noinline 标识的函数,会防止编译器内联,从而生成 CALL 指令去调用函数。

因而,在用户态处理好 bpf2bpf 函数调用关系后,将它们保存到程序区;在 eBPF vm on eBPF 执行时,遇到 bpf2bpf 的 CALL 指令,便做如下处理:

  • 缓存当前的指令位置
  • 缓存当前的栈指针
  • 调整栈指针
  • 跳转到被调用函数的指令位置

而在函数调用结束后,需要做如下处理:

  • 恢复栈指针
  • 恢复指令位置

此时,需要区分 entry bpf prog 和 bpf2bpf 函数 bpf prog;entry bpf prog 不需要做上述处理,遇到 EXIT 指令则结束当前 vm 的运行。

eBPF vm on eBPF 计算斐波那契数列

使用如下 bpf C 代码,计算斐波那契数列:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
SEC("xdp")
int xdp_fib(struct xdp_md *ctx)
{
    volatile int fibs[10];
    fibs[0] = 1;
    fibs[1] = 1;

    for (int i = 2; i < 10; i++) {
        fibs[i] = fibs[i - 1] + fibs[i - 2];
    }

    return fibs[9];
}

clang 编译得到 .o 文件后,使用 go-ebpf dump .o 文件里的 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
xdp_fib:
      ; int xdp_fib(struct xdp_md *ctx)
     0: MovImm dst: r1 imm: 1
      ; fibs[0] = 1;
     1: StXMemW dst: rfp src: r1 off: -4 imm: 0
      ; fibs[1] = 1;
     2: StXMemW dst: rfp src: r1 off: -8 imm: 0
      ; fibs[i] = fibs[i - 1] + fibs[i - 2];
     3: LdXMemW dst: r1 src: rfp off: -8 imm: 0
      ; fibs[i] = fibs[i - 1] + fibs[i - 2];
     4: LdXMemW dst: r2 src: rfp off: -4 imm: 0
      ; fibs[i] = fibs[i - 1] + fibs[i - 2];
     5: AddReg dst: r2 src: r1
      ; fibs[i] = fibs[i - 1] + fibs[i - 2];
     6: StXMemW dst: rfp src: r2 off: -12 imm: 0
      ; fibs[i] = fibs[i - 1] + fibs[i - 2];
    // ...
    31: LdXMemW dst: r1 src: rfp off: -36 imm: 0
      ; fibs[i] = fibs[i - 1] + fibs[i - 2];
    32: LdXMemW dst: r2 src: rfp off: -32 imm: 0
      ; fibs[i] = fibs[i - 1] + fibs[i - 2];
    33: AddReg dst: r2 src: r1
      ; fibs[i] = fibs[i - 1] + fibs[i - 2];
    34: StXMemW dst: rfp src: r2 off: -40 imm: 0
      ; return fibs[9];
    35: LdXMemW dst: r0 src: rfp off: -40 imm: 0
    36: Exit

将上述指令保存到程序区,然后执行 eBPF vm on eBPF,得到如下结果:

1
bpf_trace_printk: bpf_vm: R0=55

在 bpf C 代码里使用 bpf2bpf 函数调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
static __noinline int
__add(int a, int b)
{
    volatile int sum = a + b;
    return sum;
}

SEC("xdp")
int xdp_fib2(struct xdp_md *ctx)
{
    volatile int fibs[10];
    fibs[0] = 1;
    fibs[1] = 1;

    for (int i = 2; i < 10; i++) {
        fibs[i] = __add(fibs[i - 1], fibs[i - 2]);
    }

    return fibs[9];
}

clang 编译得到 .o 文件后,使用 go-ebpf dump .o 文件里的 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
xdp_fib2:
      ; int xdp_fib2(struct xdp_md *ctx)
     0: MovImm dst: r1 imm: 1
      ; fibs[0] = 1;
     1: StXMemW dst: rfp src: r1 off: -4 imm: 0
      ; fibs[1] = 1;
     2: StXMemW dst: rfp src: r1 off: -8 imm: 0
      ; fibs[i] = __add(fibs[i - 1], fibs[i - 2]);
     3: LdXMemW dst: r1 src: rfp off: -8 imm: 0
     4: LdXMemW dst: r2 src: rfp off: -4 imm: 0
     5: Call -1 <__add>
      ; fibs[i] = __add(fibs[i - 1], fibs[i - 2]);
     6: StXMemW dst: rfp src: r0 off: -12 imm: 0
      ; fibs[i] = __add(fibs[i - 1], fibs[i - 2]);
     7: LdXMemW dst: r1 src: rfp off: -12 imm: 0
     8: LdXMemW dst: r2 src: rfp off: -8 imm: 0
    // ...
      ; fibs[i] = __add(fibs[i - 1], fibs[i - 2]);
    30: StXMemW dst: rfp src: r0 off: -36 imm: 0
      ; fibs[i] = __add(fibs[i - 1], fibs[i - 2]);
    31: LdXMemW dst: r1 src: rfp off: -36 imm: 0
    32: LdXMemW dst: r2 src: rfp off: -32 imm: 0
    33: Call -1 <__add>
      ; fibs[i] = __add(fibs[i - 1], fibs[i - 2]);
    34: StXMemW dst: rfp src: r0 off: -40 imm: 0
      ; return fibs[9];
    35: LdXMemW dst: r0 src: rfp off: -40 imm: 0
    36: Exit
__add:
      ; __add(int a, int b)
    37: MovReg dst: r0 src: r2
      ; return a + b;
    38: AddReg dst: r0 src: r1
      ; return a + b;
    39: Exit

用来验证 eBPF vm on eBPF bpf2bpf 函数调用的正确性,执行 eBPF vm on eBPF,得到如下结果:

1
bpf_trace_printk: bpf_vm: R0=55

eBPF vm on eBPF 有什么用?

似乎没什么用,just for fun。

该 eBPF vm on eBPF 能解决的问题,eBPF vm 几乎也能解决。

不过,eBPF vm on eBPF 并没有严格的 verifier 校验,可以用来跑 clang 编译的 bpf 代码。

只要 bpf 子系统 issue 利用恰到好处,eBPF vm on eBPF 就能发挥更大作用。

小结

eBPF vm on eBPF 是一个非常有趣的想法,也是一个非常有趣的实践。

Build a feature-less eBPF vm on eBPF, just for fun.