拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Boa 虚拟机(VM)架构深度解析:从字节码编译到执行引擎

编程语言编译器开发工具【免费下载链接】boaBoa is an embeddable Javascript engine written in Rust.项目地址https://gitcode.com/gh_mirrors/bo/boa点击查看免费下载Boa 是一个用 Rust 编写的可嵌入式 JavaScript 引擎其执行核心是一套自研的字节码虚拟机VM。本文基于 docs/vm.md 完整梳理 Boa 的 VM 架构从源码如何经过解析、编译生成CodeBlock到 VM 如何通过统一值栈、调用帧CallFrame与数百个操作码Opcode执行 JavaScript并深入讲解异常处理、生成器/异步函数、内联缓存与运行时限制等关键机制。读完本文你将理解 Boa 字节码的编码方式、栈式调用约定、预算式异步执行循环并能够自行解读 VM 的 trace 输出与字节码转储为嵌入 Boa 或参与其内核开发打下基础。架构总览一条从源码到结果的流水线当 Boa 执行 JavaScript 时源代码依次经过以下阶段Source Code → Parser → AST → ByteCompiler → CodeBlock → VM → Result解析器Parser把源代码转换为 ASTByteCompiler将 AST 编译为存储在CodeBlock中的字节码最后由 VM 逐条执行这些字节码并产出结果。这条流水线对应的模块位于 core/engine/src/vm其中mod.rs定义了 VM 主体opcode/目录存放全部指令定义与执行逻辑。上图展示了从源代码到执行结果的整体架构解析器产出 AST字节码编译器将其编译为字节码VM 负责解释执行。CodeBlock函数的编译形态每一个被ByteCompiler处理的函数或脚本/模块都会生成一个独立的CodeBlock。可以把它理解为函数的编译产物——它承载该函数的字节码、常量池、绑定信息、异常处理器和元数据。文档给出了简化视图struct CodeBlock { bytecode: ByteCode, // the actual instruction bytes constants: ThinVecConstant, // strings, nested functions, bigints, scopes bindings: Box[BindingLocator], // variable binding locators handlers: ThinVecHandler, // try/catch/finally handler ranges ic: Box[InlineCache], // inline caches for fast property access register_count: u32, // how many local registers this function uses length: u32, // the .length property of the function parameter_length: u32, // number of formal parameters this_mode: ThisMode, // Global, Strict, or Lexical flags: CellCodeBlockFlags, // strict mode, async, generator, etc. source_info: SourceInfo, // source maps and function name }对照真实源码 core/engine/src/vm/code_block.rs实际结构还包含mapped_arguments_binding_indices用于构造MappedArguments对象、global_lexs/global_fns/global_vars全局词法/函数/变量索引以及debug_id用于在编译输出和调用帧中标识匿名函数。字节码通过索引引用常量池中的元素enum Constant { String(JsString), // property names, string literals Function(GcCodeBlock), // nested function declarations/expressions BigInt(JsBigInt), // bigint literals Scope(Scope), // declarative or function scopes }位标志bitflagsflags字段使用 bitflags 追踪函数的各种属性。源码 core/engine/src/vm/code_block.rs#L26-L58 定义了标志含义STRICT函数是否处于严格模式HAS_BINDING_IDENTIFIER函数是表达式且带有绑定标识符IS_CLASS_CONSTRUCTOR[[IsClassConstructor]]内部槽IN_CLASS_FIELD_INITIALIZER[[ClassFieldInitializerName]]内部槽IS_DERIVED_CONSTRUCTOR[[ConstructorKind]]派生构造函数IS_ASYNC异步函数IS_GENERATOR生成器函数HAS_PROTOTYPE_PROPERTY箭头函数与方法没有prototype属性HAS_FUNCTION_SCOPE函数需要函数作用域TRACEABLE仅trace特性逐条追踪该函数的指令执行此外CodeBlock还提供了is_async_generator()IS_ASYNC | IS_GENERATOR组合判断、is_ordinary()、has_function_scope()等便捷方法这些属性会直接影响函数对象的创建与调用路径。字节码与操作码Opcodes指令存放在ByteCode结构体中本质是Box[u8]。每条指令以一个字节的操作码开头随后是操作数┌────────┬──────────┬──────────┬───┐ │ opcode │ operand1 │ operand2 │...│ │ (1 B) │ (varies) │ (varies) │ │ └────────┴──────────┴──────────┴───┘变长操作数编码大多数操作数使用VaryingOperand这类按需选择最小编码的方式值 ≤ 255 用u8≤ 65535 用u16否则用u32从而在常见场景下保持字节码紧凑。在真实实现中操作数编码通过 core/engine/src/vm/opcode/args.rs 的Argumenttrait 完成——它定义了encode/decode方法支持u8/i8/u16/i16/u32/i32/u64/f32/f64以及最多 5 元组的原生类型组合并实现了IndexOperand常量池索引、RegisterOperand寄存器索引与Address跳转地址等语义化操作数类型。read()函数从字节切片中按偏移读取操作数并返回新的偏移供 handler 推进程序计数器。宏驱动的指令集定义所有操作码都在一次generate_opcodes!宏调用中定义见 core/engine/src/vm/opcode/mod.rs#L350-L511。该宏从一份定义中生成大量产物Opcode枚举#[repr(u8)]每个操作码对应一个字节值Instruction枚举解码后的形态字段带类型OPCODE_HANDLERS长度为 256 的静态函数指针分发表每个可能的操作码字节对应一个 handlerOPCODE_HANDLERS_BUDGET带预算扣除的 handler 表用于异步执行ByteCodeEmitter上每个操作码对应的emit_*方法。每个操作码的行为通过Operationtrait 实现trait Operation { const NAME: static str; const INSTRUCTION: static str; const COST: u8; // used for budget-based async execution }COST用于基于预算的异步执行见下文执行循环。操作码数量超过 100 个大致可分为以下类别push/pop— 推送常量、弹出值PushZero、PushInt8、PushLiteral、Pop等binary ops— 算术与比较Add、Sub、Mul、Eq、LessThan等unary ops—Neg、Pos、BitNot、LogicalNot、TypeOf、Inc、Deccontrol flow—Jump、JumpIfTrue、JumpIfFalse、Returncall/new—Call、CallEval、New、SuperCallget/set—GetName、GetPropertyByName、SetName、SetPropertyByNamedefine/delete—DefVar、DefineOwnPropertyByName、DeletePropertyByValueenvironment—PushScope、PopScope、PushObjectEnvironmentgenerator/async—Generator、GeneratorYield、Awaititeration—GetIterator、IteratorNext、IteratorDonecopy—Move、SetRegisterFromAccumulator在 core/engine/src/vm/opcode 目录下每个操作码族都有对应的子模块call/、get/、set/、generator/、binary_ops/、iteration/等分别实现对应的运行时语义。值栈与调用约定VM 使用一个共享于所有调用帧的VecJsValue作为值栈见 core/engine/src/vm/mod.rs 中的Stack结构。当函数被调用时它在栈上的区域布局如下Setup by the caller ┌─────────────────────────────────────────────────────────┐ ┌───── register pointer (rp) ▼ ▼ ▼ | -(2N): this | -(1N): func | -N: arg1 | ... | -1: argN | 0: reg1 | ... | K: regK | ▲ ▲ ▲ ▲ ▲ ▲ └──────────────────────────────┘ └──────────────────────┘ └────────────────────┘ function prologue arguments Setup by the callee ▲ └─ Frame pointer (fp)前两个槽位始终是this和函数对象本身构成函数序言prologue之后是实参再往后由被调用方按register_count分配本地寄存器。寄存器指针rp恰好位于分界处因此寄存器可简单地以rp为基准的偏移来寻址。一个具体的调用示例给定以下 JavaScriptfunction x(a) {} function y(b, c) { return x(b c); } y(1, 2);在调用x期间栈布局如下caller prologue caller arguments callee prologue callee arguments ┌─────────────────┐ ┌─────────┐ ┌─────────────────┐ ┌──────┐ ▼ ▼ ▼ ▼ │ ▼ ▼ ▼ | 0: undefined | 1: y | 2: 1 | 3: 2 | 4: undefined | 5: x | 6: 3 | ▲ ▲ ▲ │ caller register pointer ────┤ │ │ │ callee register pointer │ callee frame pointer │ └───── caller frame pointer调用约定如下调用方先压入this和函数对象序言再压入实参调用方创建CallFrame并调用push_frame()push_frame()将rp设为当前栈顶并按register_count扩展栈空间所有新槽位初始化为undefined源码实现见 core/engine/src/vm/mod.rs#L588-L619其中frame.fp 当前栈长 - argument_count - FUNCTION_PROLOGUEFUNCTION_PROLOGUE 2函数返回时栈被截断回调用方的帧指针位置truncate_to_frame。调用帧CallFrameCallFrame保存一次函数调用的全部执行状态其结构源码见 core/engine/src/vm/call_frame/mod.rsstruct CallFrame { code_block: GcCodeBlock, // the functions compiled bytecode pc: u32, // program counter (offset into bytecode) rp: u32, // register pointer (start of registers in the stack) argument_count: u32, // how many arguments were passed env_fp: u32, // environment frame pointer (for cleanup on exception) environments: EnvironmentStack, // lexical environment chain realm: Realm, // the realm this function runs in iterators: ThinVecIteratorRecord, // open iterators (need closing on abrupt completion) binding_stack: VecBindingLocator, // bindings being updated loop_iteration_count: u64, // tracks loop iterations for runtime limits active_runnable: OptionActiveRunnable, // owning Script or Module flags: CallFrameFlags, // EXIT_EARLY, CONSTRUCT, etc. }借助rp与argument_countCallFrame可以推算出栈上一切元素的位置frame_pointer() rp - argument_count - 2序言起点this_index() rp - argument_count - 2function_index() rp - argument_count - 1arguments_range() (rp - argument_count)..rp值得关注的标志CallFrameFlagsbitflagsu8类型包括EXIT_EARLY— 从此帧返回时直接终止整个 VM 并返回 Rust 调用方而不是继续父帧的执行CONSTRUCT— 该帧经由[[Construct]]即new关键字创建REGISTERS_ALREADY_PUSHED— 用于恢复生成器寄存器区域已由上次执行填充完毕push_frame()跳过寄存器分配THIS_VALUE_CACHED—this值已被解析并缓存缓存值放在this槽位NATIVE_FRAME— 为原生函数调用压入的轻量帧仅用于在帧栈上持有函数的 realm。帧的压入与弹出调用push_frame()时当前帧被换出并压入frames向量新帧成为vm.framepop_frame()则执行逆操作把向量末尾的帧换回。frames向量索引 0 处始终保留一个 dummy 帧确保栈永不为空。对于 async/generator 函数前几个寄存器被保留常量定义见 core/engine/src/vm/call_frame/mod.rs#L114-L119寄存器 0固定undefined寄存器寄存器 1、2、3promise capabilitypromise 对象、resolve 函数、reject 函数寄存器 4async generator 对象仅异步生成器使用执行循环核心循环位于Context::run()core/engine/src/vm/mod.rs#L988-L1014是一个直接的取指-解码-执行循环fn run(mut self) - CompletionRecord { while let Some(byte) bytecode.get(frame.pc) { let opcode Opcode::decode(*byte); match self.execute_one(Self::execute_bytecode_instruction, opcode) { ControlFlow::Continue(()) {} ControlFlow::Break(value) return value, } } }分发使用静态 handler 表OPCODE_HANDLERS是 256 个函数指针组成的数组每个可能的操作码字节对应一个 handler。每个 handler 从字节码中解码操作数、将pc推进到操作数之后、执行对应操作并返回ControlFlow。预算式异步执行对于异步上下文如模块求值存在run_async_with_budget()。每个操作码都有COST预算在每条指令执行时递减当预算归零时函数通过yield_now().await让出给异步执行器从而防止长时间运行的脚本饿死其他任务。OPCODE_HANDLERS_BUDGET中的 handler 会在执行前先执行*budget budget.saturating_sub(u32::from($Variant::COST))。指令级追踪Tracing若以trace特性构建VM 可以打印每条执行的指令。可全局启用vm.trace true也可按函数启用code_block.set_traceable(true)对应源码中的CodeBlock::set_traceable()。输出形如Time Opcode Operands Top Of Stack 6μs PushOne dst:0 1 7μs PutLexicalValue src:0, binding_index:0 empty追踪逻辑实现在Context::trace_execute_instruction()core/engine/src/vm/mod.rs#L723-L766它使用Instant计时每条指令并调用Stack::display_trace()以紧凑的分组形式展示栈内容相同值的连续槽位合并为value (xN)帧边界用|帧号|标记超宽自动截断。异常处理异常处理器以Handler结构编译进CodeBlock源码 core/engine/src/vm/code_block.rs#L80-L97struct Handler { start: u32, // start of the protected range end: u32, // handler address (where to jump on catch) environment_count: u32, // environments to preserve when unwinding }对于如下的 try/catchtry { // bytecode at pc 10..50 riskyOperation(); } catch (e) { // handler at pc 50 handleError(e); }会得到Handler { start: 10, end: 50, environment_count: N }。contains(pc)判断一个pc是否落在[start, end)保护区。异常抛出时的处理流程VM 从影子栈shadow stack捕获回溯信息backtrace上限由RuntimeLimits::backtrace_limit决定默认 50检查错误是否可捕获。不可捕获的错误如超出运行时限制跳过所有 handler 逻辑立即解旋unwind一切对于可捕获错误find_handler(pc)反向最内层优先搜索 handler寻找[start, end)范围包含当前pc的那一个core/engine/src/vm/code_block.rs#L305-L311若在当前帧找到 handler设置pc handler.end将环境截断到env_fp handler.environment_count把异常存入vm.pending_exception并继续执行。字节码稍后可通过Exception操作码取回异常handle_exception_at实现见 core/engine/src/vm/mod.rs#L646-L663若当前帧无 handler检查该帧是否设置了EXIT_EARLY若是把错误返回给 Rust 调用方否则弹出该帧尝试父帧的 handler持续解旋直到找到 handler 或帧耗尽。pending_exception为None时表示生成器被调用了return()异常会穿过 handler 并执行finally代码对应ReThrow与Exception操作码的注释。生成器与异步函数生成器函数使用GeneratorResumeKind追踪恢复方式enum GeneratorResumeKind { Normal 0, // .next(value) Throw 1, // .throw(error) Return 2, // .return(value) }生成器通过GeneratorYield操作码让出yield时当前帧被弹出其栈部分被保存。再次调用.next()时保存的CallFrame带着REGISTERS_ALREADY_PUSHED标志被重新压入——由于寄存器区域已经来自上一次执行push_frame()会跳过分配步骤见 core/engine/src/vm/mod.rs#L595 的registers_already_pushed()检查执行直接从上次中断处继续。异步函数的工作方式类似但使用保留的寄存器槽位存放 promise 机制寄存器 1-3 分别持有 promise、resolve、reject。Await操作码像Yield一样挂起执行但恢复由 promise 的落定settlement驱动而非显式的.next()调用。异步生成器则结合两者寄存器 1-3 用于 promise寄存器 4 存放 async generator 对象。内联缓存Inline Caching属性访问可能很昂贵——VM 每次都需要沿形状链shape chain查找。为加速每个GetPropertyByName和SetPropertyByName指令都通过ic_index引用一个内联缓存条目CodeBlock.ic。缓存存储属性名与最近见过的对象形状若对象的形状与缓存匹配则属性槽位索引已知可跳过完整查找未命中时执行完整查找并更新缓存。实现见 core/engine/src/vm/inline_cache/mod.rsInlineCache内部维护多个形状→槽位的多态条目polymorphic cache对形状使用弱引用WeakShape避免缓存阻止形状的回收查找时还会顺带清理过期的弱引用。当访问点见过太多形状时该缓存条目标记为不再缓存避免缓存被频繁淘汰。运行时限制Runtime Limits嵌入方可通过RuntimeLimits约束 VM 行为。该结构定义于 core/engine/src/vm/runtime_limits.rs默认值如下限制项默认值说明递归深度recursion512最大函数递归层数栈大小stack_size1024 × 10 10240最大栈槽位数超过抛出错误循环迭代loop_iterationu64::MAX实际无限制设为具体值可启用限制回溯条数backtrace_limit50异常中最多保留的回溯帧数需要说明文档原文称stack size of 1024而当前源码默认值是1024 * 10且Vm::new()创建Stack::new(1024)时传入的 1024 只是栈的初始容量二者并非同一概念以源码 core/engine/src/vm/runtime_limits.rs#L17-L27 为准。这些限制在调用边界和循环内部被检查check_runtime_limits()见 core/engine/src/vm/mod.rs#L1017-L1033递归深度 帧数 − 1排除 dummy 帧host_call_depth。超出任何限制都会抛出不可捕获的RuntimeLimitError它绕过所有异常处理器直接向 Rust 调用方解旋。嵌入方可调用set_recursion_limit、set_stack_size_limit、set_loop_iteration_limit、set_backtrace_limit及disable_loop_iteration_limit定制这些阈值。理解 trace 输出搭建好环境后可以在测试文件中尝试简单的 JavaScript例如let a 1; let b 2;输出----------------------Compiled Output: main----------------------- Location Count Handler Opcode Operands 000000 0000 none PushOne 000001 0001 none PutLexicalValue 0000: a 000006 0002 none PushInt8 2 000008 0003 none PutLexicalValue 0001: b 000013 0004 none Return Literals: empty Bindings: 0000: a 0001: b Functions: empty Handlers: empty ----------------------------------------- Call Frame ----------------------------------------- Time Opcode Operands Top Of Stack 6μs PushOne 1 7μs PutLexicalValue 0000: a empty 0μs PushInt8 2 2 1μs PutLexicalValue 0001: b empty 0μs Return empty Stack: empty undefined各字段含义待执行函数的字节码及其属性Compiled Output段Compiled Output字节码本身。Location指令的位置指令大小不一。Count指令计数。Handler若该指令抛出异常由哪个 handler 负责以及跳转到何处表示 handler 的起点表示终点此格式由 core/engine/src/vm/code_block.rs 中CodeBlock的Display实现生成。Opcode操作码名称。Operands操作码的操作数instruction_operands()方法会结合CodeBlock中的内联缓存等上下文格式化。Literals字节码使用的字面量如字符串。Bindings字节码使用的绑定名。Functions字节码使用的函数名。Handlers字节码使用的异常处理器包含相对CallFrame帧指针而言的栈上/环境数量。正在执行的代码标记为Vm Start或Call FrameTime该条指令执行耗时。Opcode操作码名称。Operands该操作码的操作数。Top Of Stack指令执行之后的栈顶元素。Stack执行结束后栈的追踪结果。执行结果栈顶元素栈为空则返回undefined。对比其他引擎的字节码输出若想用同样的 JavaScript 对比其他引擎的字节码SpiderMonkey 的字节码输出是最佳参照。需要从源码构建 SpiderMonkey shell预构建二进制不包含所需的调试工具将二进制命名为js_shell因为js与 NodeJS 冲突。启动后使用js_shell -f tests/js/test.js运行起初没有输出需要在代码中调用dis()或dis([func])之后会得到类似如下输出loc op ----- -- 00000: GlobalOrEvalDeclInstantiation 0 # main: 00005: One # 1 00006: InitGLexical a # 1 00011: Pop # 00012: Int8 2 # 2 00014: InitGLexical b # 2 00019: Pop # 00020: GetGName dis # dis对比可见两套 VM 对同样源码的编译结果在语义上高度对应压入常量、初始化词法绑定、弹出、读取全局名但 Boa 的字节码布局更紧凑操作码后跟变长操作数且PutLexicalValue直接以绑定索引而非名字寻址。进一步阅读core/engine/src/vm/mod.rsVM 主体、值栈、帧管理、执行循环与运行时限制检查core/engine/src/vm/code_block.rsCodeBlock、Constant、Handler及 trace 输出格式化core/engine/src/vm/call_frame/mod.rsCallFrame、帧标志与保留寄存器core/engine/src/vm/opcode/mod.rsgenerate_opcodes!宏与Operationtraitcore/engine/src/vm/runtime_limits.rs运行时限制的默认值与设置接口core/engine/src/vm/inline_cache/mod.rs属性访问的多态内联缓存docs/bytecompiler.md字节码编译器ByteCompiler的配套文档tests/insta-bytecode基于 insta 的字节码编译快照测试可观察各种语法结构编译后的真实字节码形态赞分享编程语言编译器开发工具【免费下载链接】boaBoa is an embeddable Javascript engine written in Rust.项目地址https://gitcode.com/gh_mirrors/bo/boa点击查看免费下载相关推荐Boa引擎字节码生成与执行虚拟机工作原理深度解析Boa引擎字节码生成与执行虚拟机工作原理深度解析 Boa引擎是一个用Rust编写的实验性JavaScript引擎它通过字节码生成和虚拟机执行来实现高效运行。编程语言编译器开发工具SBOM工具支持的12种生态系统全解析从NPM到Maven的完整支持SBOM工具支持的12种生态系统全解析从NPM到Maven的完整支持 在当今软件供应链安全日益重要的时代SBOM软件物料清单工具成为了开发者和安全团队的供应链安全SBOMCLI开发工具告别枯燥背单词用Qwerty Learner重塑你的英语学习与打字技能告别枯燥背单词用Qwerty Learner重塑你的英语学习与打字技能 还在为记不住单词而烦恼吗还在因为打字速度慢而影响工作效率吗Qwerty Learn前端教育上一篇Deceive下载与安装教程3分钟快速上手下一篇PPT Master 完整指南三步把文档变成可编辑 PPT创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门