在 eBPF Talk: 巨献 eBPF vm on eBPF 一文中,简要介绍了 eBPF vm on eBPF;经过一番考虑后,决定将 eBPF vm on eBPF 的源代码开放出来。
TL;DR 源代码:eBPF vm on eBPF。
eBPF vm
该 vm 主要由以下几个部分组成:
- 程序区:存放待执行的 bpf prog。
- 栈空间:存放 bpf prog 执行过程中的临时数据。
- 寄存器:一一对应内核里 eBPF vm 的寄存器。
- vm 本身:特殊的 IP 寄存器、栈深度、函数调用深度等。
- 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_idx
和 insn_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,复用已有的指令集,以解释器的形式逐条执行指令。