该篇文章是记录rbpf虚拟机即时编译器(JIT)方法过程。
本文记录的是基于 x86-64 架构的 eBPF(Extended Berkeley Packet Filter)即时编译器(JIT)。
(学习该虚拟机的目的是为了搞懂solana合约的执行方式,solana使用的rbpf是在该虚拟机上进行扩展。)
eBPF 有 11 个通用寄存器,x86-64 有更多的寄存器。
指令发射是 JIT 编译的核心部分(emit*),在本编译器中主要由下述指令完成:
emit_alu32
和 emit_alu64
生成算术逻辑单元(ALU)指令。emit_mov
生成数据传输指令。emit_jcc
和 emit_jmp
生成条件跳转和无条件跳转指令。eBPF 中的跳转指令(如 JEQ、JGT 等)动态计算目标地址。
eBPF 支持调用辅助函数(如哈希表查找、随机数生成等)。
先保存寄存器的当前值,以便在 JIT 编译的代码执行完成后可以恢复它们。 保存寄存器
self.emit_push(mem, RBP);
self.emit_push(mem, RBX);
self.emit_push(mem, R13);
self.emit_push(mem, R14);
self.emit_push(mem, R15);
self.emit_mov(mem, RDX, R10);
RDX用于传递内存指针,这里将RDX赋值给R10,即使用R10存储临时数据。
根据use_mbuff和update_data_ptr的值来判断:
为JIT编译的函数序言准备寄存器和内存指针状态。
match (use_mbuff, update_data_ptr) {
(false, _) => {
//不使用任何mbuff,内存指针移至寄存器1
if map_register(1) != RDX {
self.emit_mov(mem, RDX, map_register(1));
}
}
(true, false) => {
// 使用已经指向mem和mem_end的mbuff:将其移动到寄存器1。
// We use a mbuff already pointing to mem and mem_end: move it to register 1.
if map_register(1) != RDI {
self.emit_mov(mem, RDI, map_register(1));
}
}
//使用 mbuff,并且需要更新数据指针(mem 和 mem_end 的偏移值)
(true, true) => {
self.emit_alu64(mem, 0x01, RDI, R8); // add mbuff to mem_offset in R8
self.emit_store(mem, OperandSize::S64, RDX, R8, 0); // set mem at mbuff + mem_offset
// Store mem_end at mbuff + mem_end_offset. Trash R9.
self.emit_load(mem, OperandSize::S64, RDX, R8, 0); // load mem into R8
self.emit_alu64(mem, 0x01, RCX, R8); // add mem_len to mem (= mem_end)
self.emit_alu64(mem, 0x01, RDI, R9); // add mbuff to mem_end_offset
self.emit_store(mem, OperandSize::S64, R8, R9, 0); // store mem_end
// Move rdi into register 1
if map_register(1) != RDI {
self.emit_mov(mem, RDI, map_register(1));
}
}
}
对应功能 当不使用任何mbuff,内存指针移至寄存器1 当使用已经指向mem和mem_end的mbuff:将其移动到寄存器1。 当使用 mbuff,并且需要更新数据指针(mem 和 mem_end 的偏移值)
self.emit_mov(mem, RSP, map_register(10));
self.emit_alu64_imm32(mem, 0x81, 5, RSP, ebpf::STACK_SIZE as i32);
将RSP(栈指针)赋值给寄存器R10,分配栈空间(emit_alu64_imm32 调整 RSP)。
while insn_ptr * ebpf::INSN_SIZE < prog.len() {
let insn = ebpf::get_insn(prog, insn_ptr);
self.pc_locs[insn_ptr] = mem.offset;
let dst = map_register(insn.dst);
let src = map_register(insn.src);
let target_pc = insn_ptr as isize + insn.off as isize + 1;
match insn.opc {
// BPF_LD class
// R10 is a constant pointer to mem.
ebpf::LD_ABS_B => self.emit_load(mem, OperandSize::S8, R10, RAX, insn.imm),
....}
}
这里类似与汇编器和反汇编器,对不同指令进行翻译。
//1.
self.set_anchor(mem, TARGET_PC_EXIT);
//2.
// Move register 0 into rax
if map_register(0) != RAX {
self.emit_mov(mem, map_register(0), RAX);
}
//3.
// Deallocate stack space
self.emit_alu64_imm32(mem, 0x81, 0, RSP, ebpf::STACK_SIZE as i32);
EXIT
。 self.emit_pop(mem, R15);
self.emit_pop(mem, R14);
self.emit_pop(mem, R13);
self.emit_pop(mem, RBX);
self.emit_pop(mem, RBP);
//返回
self.emit1(mem, 0xc3); // ret
什么是x86-64 调用约定(System V ABI )?
通过对即时编译器的执行进行分步骤记录,知道该JIT是按照x86-64调用标准进行编写,其寄存器和堆栈分配以及返回值都符合对应标准。对执行过程有了一些了解。
不同的(如arm架构)的JIT与X86其标准不同,所以其JIT过程也不同。