Hotdry.
compiler-design

从零实现最小 WebAssembly VM:解析器、验证器、栈机执行器、内存与陷阱处理

手把手实现简易 Wasm VM 的核心组件:二进制解析、模块验证、栈式执行、线性内存、陷阱机制与主机绑定,提供工程参数、阈值与监控清单。

实现一个最小 WebAssembly(Wasm)虚拟机(VM),是深入理解其栈式架构、安全沙箱与高效执行机制的最佳途径。这种从零构建的 VM 聚焦 MVP(Minimum Viable Product)规范,支持核心功能如模块解析、类型验证、指令评估、线性内存管理和陷阱处理,同时提供 Wasm 到主机的绑定接口,确保沙箱隔离。不同于浏览器内置引擎,此实现适用于嵌入式场景、自定义运行时或教育目的,能在 Rust/Go/C 等语言中快速原型化。

二进制格式解析器:LEB128 解码与 Section 提取

Wasm 二进制模块以魔数 \0asm(4 字节)和版本号(1,通常为 0x01)开头,后续是多个 Section(如 Type ID=1、Function ID=3、Code ID=10),每个 Section 包含大小前缀(u32 LEB128 变长编码)和内容向量。

观点:解析器必须高效处理 LEB128(无符号整数变长编码,最高 5 字节 / 值),以最小内存足迹解码模块为内存结构,避免字符串化以节省开销。

证据:规范要求 Section 按 ID 递增出现,Type Section 定义函数签名(functype: 0x60 + vec (param types) + vec (result types)),Function Section 只存类型索引,Code Section 匹配 Function 并含指令序列。实际模块大小常 <100KB,解码只需单次线性扫描。

可落地参数 / 清单

  • 缓冲区:预分配 64KB,超出 trap。
  • LEB128 解码阈值:单值 >2^32 trap(非法模块)。
  • Section 限制:Type ≤1024,Function ≤512(小型 VM)。
  • 伪代码(Rust 风格):
    fn decode_leb128(buf: &[u8]) -> (u32, usize) {
        let mut val = 0u32; let mut shift = 0; let mut i = 0;
        while i < 5 {
            let byte = buf[i]; i += 1;
            val |= ((byte & 0x7f) as u32) << shift;
            if byte & 0x80 == 0 { break; }
            shift += 7;
        }
        (val, i)
    }
    struct Module { types: Vec<Functype>, funcs: Vec<u32>, codes: Vec<Vec<Instr>> /* etc */ }
    

实现中,逐字节迭代,提取 12 种核心 Section,忽略自定义(ID=0)。

引用 wasmgroundup.com:“To really understand what WebAssembly is and what makes it special, you need to dive into the low-level details.”

模块验证器:静态类型检查与结构完整性

验证确保模块安全:函数签名匹配、指令类型一致、无无效索引。

观点:验证是沙箱第一道防线,失败立即 trap,避免运行时崩溃;聚焦控制流与类型栈模拟。

证据:每个函数体(Code)须匹配对应 Function 的类型索引;指令如 i32.add 需栈顶两个 i32,pop 后 push i32。分支(block/if/loop)需块类型匹配(空块无结果,单一结果类型检查)。

可落地参数 / 清单

  • 类型栈模拟:Vec(i32=0x7F, i64=0x7E 等),深度上限 1000 / 帧。
  • 索引检查:funcidx <func_sec.len (),globalidx < globals.len ()。
  • 验证流程:for each func {simulate_stack (insts); assert stack.pop () == result_type }
  • 风险阈值:循环深度 >50 trap(防无限验证)。

栈基执行器:指令分派与调用栈

Wasm 是栈机:单一操作数栈(values)+ 控制栈(标签 / 调用帧)。

观点:执行循环用 PC(program counter)分派指令 opcode(0x41 i32.const 等),支持基本数值 / 控制指令即可运行 hello.wasm。

证据:指令如 local.get idx(push local [idx])、i32.add(pop2, add, push)、call idx(参数入栈,跳转执行)。调用栈管理帧:locals、PC、label_stack。

可落地参数 / 清单

  • 栈大小:默认 1MB(~256K slots),grow 失败 trap。
  • 指令表:switch (opcode) { 0x41 => push (Const32 (decode_u32 ())), 0x6A => i32.add () /* etc */ }
  • 调用栈帧:struct Frame { pc: usize, locals: Vec, label_stack: Vec }
  • 初始栈:empty;end/br/end 标签 pop 检查类型匹配。

线性内存管理:页式增长与边界检查

内存是 64KB 页(2^16)数组,初始 min=1,max 可选。

观点:所有 load/store 地址 + offset < current_pages * 65536,失败 trap;支持 grow(原子增加页)。

证据:指令 memory.grow delta(push 新页数,-1 失败);i32.load offset=0 align=4(pop addr, load [addr+offset])。

可落地参数 / 清单

  • 内存:Vec,初始 1 页(65536 字节),上限 1024 页(~64MB)。
  • 边界检查:if addr + offset + size > mem.len () { trap () }
  • Grow 参数:delta u32,new_size = old + delta,>max trap。
  • 监控:日志 mem.grow 调用,阈值 >512 页告警(内存泄漏)。

陷阱处理与主机绑定:沙箱安全

陷阱(trap)是显式异常:整数溢出、除零、非法内存、出界栈。

观点:统一 trap handler 回滚栈帧、重置 PC=0 或 unwind 到 host;绑定用 import table(env.printf 等)。

证据:指令如 i32.div_u(div0 trap)、unreachable(立即 trap)。主机绑定:实例化时注入 imports(host func),export 如 _start。

可落地参数 / 清单

  • Trap 类型:enum Trap {Overflow, OutOfBounds, InvalidIdx /* etc */}
  • 处理:panic 或 longjmp 回 host;栈 unwind 逐帧 pop。
  • 绑定接口:HashMap<String, HostFunc>,call 时 switch 名("env.log" => host_log (args))。
  • 沙箱参数:no_fs(禁用文件 import)、timeout 10s / 调用。
  • 回滚策略:trap 后 reset 栈 / 内存到入口帧。

工程化参数与监控清单

  • 性能阈值:指令 / 秒 >1M(基准 fib (30) <100ms);栈深度监控,>80% 容量 trap。
  • 安全清单:验证全覆盖;内存零初始化;无 JIT 时禁用 volatile。
  • 测试用例:wasm spec test suite 子集(add.wasm, fib.wasm);fuzz 二进制输入。
  • 部署参数:Rust wasm-rs 库起步;内存 mmap 加速;Prometheus 指标(traps/sec, mem_usage)。

此最小 VM 已能运行简单 Wasm(如 Wafer 语言示例),扩展 GC/Threads 需提案支持。实际项目中,结合 wasmtime/wasmer 优化。

资料来源

查看归档