eBPF Talk: 巨献 eBPF vm on eBPF 一文中,简要介绍了 eBPF vm on eBPF;经过一番考虑后,决定将 eBPF vm on eBPF 的源代码开放出来。

TL;DR 源代码:eBPF vm on eBPF

eBPF vm

该 vm 主要由以下几个部分组成:

  1. 程序区:存放待执行的 bpf prog。
  2. 栈空间:存放 bpf prog 执行过程中的临时数据。
  3. 寄存器:一一对应内核里 eBPF vm 的寄存器。
  4. vm 本身:特殊的 IP 寄存器、栈深度、函数调用深度等。
  5. bpf 指令解释器。

eBPF vm 程序区

在该 vm 中,程序区是一个数组,每个元素是一个 bpf prog。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#define BPF_MAX_PROGS       1024
#define BPF_MAX_PROG_INSNS  4 * 1024

struct bpf_vm_prog {
    struct bpf_insn insns[BPF_MAX_PROG_INSNS];
    u32 insns_cnt;
    int32 stack_depth;
};

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, BPF_MAX_PROGS);
    __type(key, __u32);
    __type(value, struct bpf_vm_prog);
} bpf_vm_progs SEC(".maps");

如上,该 vm 支持存放 1024 个 bpf prog,每个 prog 最多 4 * 1024 条指令。

eBPF vm 栈空间

在该 vm 中,栈空间是一个长度为 1 的数组,数组元素为字节数组。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#define BPF_MAX_STACK       2 * 1024 * 1024

struct bpf_vm_stack {
    u8 stack[BPF_MAX_STACK];
};

struct {
    __uint(type, BPF_MAP_TYPE_ARRAY);
    __uint(max_entries, 1);
    __type(key, __u32);
    __type(value, struct bpf_vm_stack);
} bpf_vm_stacks SEC(".maps");

如上,该 vm 支持最大 2MB 的栈空间。

eBPF vm 寄存器

在该 vm 中,寄存器是一个数组,每个元素是一个 64 位整数。

1
2
3
4
5
6
struct bpf_vm {
    // ...
    u64 regs[MAX_BPF_REG + 1]; // add 1 hidden reg

    // ...
};

如上,该 vm 支持 12 个寄存器:R0 ~ R9、R10、一个临时使用的特殊寄存器。

eBPF vm 本身

在该 vm 中,vm 本身是一个结构体,包含了特殊的 IP 寄存器、栈深度、函数调用深度等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum bpf_vm_state {
    BPF_VM_STATE_UNSPEC,

    BPF_VM_STATE_VM_INTERNAL_ERR,   // vm 内部错误

    BPF_VM_STATE_INSN_CALL,         // CALL 指令
    BPF_VM_STATE_INSN_PROBE_MEM,    // PROBE_MEM 指令
    BPF_VM_STATE_INSN_ATOMIC,       // atomic 指令
    BPF_VM_STATE_INSN_INVALID,      // 无效指令
};

struct bpf_vm {
    u64 reg_ip;                 // instruction pointer combined with prog_idx|insn_idx
    u64 regs[MAX_BPF_REG + 1];  // add 1 hidden reg

    int stack_curr_depth;       // 当前栈深度
    u32 func_call_depth;        // 当前函数调用深度
    u64 func_call_stack[BPF_MAX_PROGS];
    int stack_depth_stack[BPF_MAX_PROGS];

    enum bpf_vm_state state;    // 描述当前 vm 的状态
};

其中,reg_ip 是一个特殊的 IP 寄存器,用于指示当前指令的位置,包括了 prog_idxinsn_idx,分别表示当前 prog 的索引和当前指令在当前 prog 中的索引。

regs 是 vm 的寄存器;这么定义的好处非常明显,在解释器中访问寄存器时,可以直接使用 regs 数组,而不需要额外的内存范围检查。

eBPF vm 指令解释器

在该 vm 中,使用 bpf_loop() helper 增加可执行指令的数量。

 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
enum bpf_vm_action {
    BPF_VM_ACTION_ABORTED,  // vm 运行被中止
    BPF_VM_ACTION_CONTINUE, // vm 继续运行
    BPF_VM_ACTION_FINISH,   // vm 运行结束
};

struct bpf_vm_ctx {
    enum bpf_vm_action action;
};

static long
__vm_loop_callback(__u32 index, struct bpf_vm_ctx *ctx)
{
    if (!ctx)
        return 1;

    ctx->action = run_vm();

    return ctx->action == BPF_VM_ACTION_CONTINUE ? 0 : 1;
}

static __always_inline bool
__vm_loop(void)
{
    struct bpf_vm_ctx ctx = {};

    bpf_loop(BPF_MAX_LOOPS, __vm_loop_callback, &ctx, 0);

    return ctx.action == BPF_VM_ACTION_FINISH;
}

如上,通过 enum bpf_vm_action 控制 vm 的运行状态;最终判断 ctx.action 是否为 BPF_VM_ACTION_FINISH 来决定 vm 是否正常结束。

其中,run_vm() 是 vm 的指令解释器的 “前端”,负责从一个 prog 切换到另一个 prog。

然后,从 vm->reg_ip 中取出当前指令的索引,读取并解释该指令,然后执行该指令。

最终,解释执行指令的函数 __exec_insn() 接近 500 行;该函数参考了内核函数 ___bpf_prog_run() 的实现。

就不在此展开 __exec_insn() 的实现了,有兴趣的读者可以参考源代码。

构建并运行该 vm

不需要编译 bpf 代码,编译 Go 代码即可运行该 vm。

1
2
3
4
5
git clone https://github.com/Asphaltt/ebpf-vm-on-ebpf.git
cd ebpf-vm-on-ebpf
go build
./ebpf-vm-on-ebpf -h
# Usage: ./ebpf-vm-on-ebpf -d ${INTERFACE}

然后,在另一台机器上 traceroute -n ${IP} 该 INTERFACE 的 IP,即可触发该 vm 的运行。

通过 cat /sys/kernel/debug/tracing/trace_pipe 可以查看 vm 的运行日志。

小结

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