- 编程语言
- 编译器
- 开发工具
【免费下载链接】boa
Boa is an embeddable Javascript engine written in Rust.
Boa 是一个用 Rust 编写的可嵌入式 JavaScript 引擎(当前仓库为gh_mirrors/bo/boa),本文围绕仓库文档 docs/debugging.md 的核心脉络,系统讲解如何对 Boa 进行端到端调试:从词法/语法阶段的 AST 转储,到字节码生成与逐指令追踪,再到可视化指令流图与 JavaScript 侧的$boa调试对象,最后覆盖编译器 panic 回溯与 LLDB/VS Code 断点调试。读完本文,你将掌握一套完整的 Boa 内部观测工具箱,既能排查引擎自身的 bug,也能深入理解源码到字节码再到 VM 执行的全链路行为。
快速上手:三种运行 JavaScript 的方式
调试 Boa 的第一步,是先让它运行你的 JavaScript。在仓库根目录(docs/debugging.md所在项目的根目录)下,有以下三种基本用法:
方式一:运行单个脚本文件
cargo run -- test.js在仓库根目录创建一个test.js文件,然后通过cargo run -- test.js让 Boa 执行它。
方式二:一次运行多个脚本文件
cargo run -- file1.js file2.jsCLI 会按参数顺序逐个编译并执行这些文件(evaluate_files循环遍历所有传入文件,见 cli/src/main.rs)。
方式三:交互式 REPL
cargo run不传任何参数时,Boa 会启动一个交互式 shell,你可以逐行输入 JavaScript 表达式并立即查看结果。REPL 内置了若干点命令(由 cli/src/main.rs 的 readline 循环实现):
.help:显示帮助信息;.exit:退出 REPL(或按Ctrl+D);.clear:清屏;.load <file>:加载并求值一个 JavaScript 文件;- 按
Ctrl+C可中止当前表达式的求值。
REPL 的命令历史会保存在仓库根目录下的.boa_history文件中,下次启动自动恢复。此外 CLI 还支持--strict(严格模式)、-e <expr>(执行表达式后退出)、-q(静默启动、不打印欢迎横幅)、-m(把输入文件当作模块处理)等开关,可配合调试使用,完整参数定义见 cli/src/main.rs。
理解调试切入点:源码的编译与执行管线
Boa 的调试手段是按"代码被读取的顺序"层层递进的,先了解整体管线有助于定位问题所在。根据 docs/vm.md 的架构说明,Boa 执行 JavaScript 的管线为:
Source Code → Parser → AST → ByteCompiler → CodeBlock → VM → Result- Parser(解析器):把源码切成 token,再解析为抽象语法树(AST),语法错误在这一阶段抛出;
- ByteCompiler(字节码编译器):把 AST 编译成字节码,存进
CodeBlock; - CodeBlock:每个函数(或脚本/模块)编译后的形态,包含指令字节、常量池、绑定信息、异常处理器与元数据;
- VM(虚拟机):逐条执行字节码并产出结果。
下面依次介绍对应各阶段的可观测工具。
第一阶段:转储 Tokens 与 AST(--dump-ast)
Boa 做的第一件事是从源代码生成 token,再把 token 解析为 AST。任何语法错误都会在 AST 生成阶段被抛出。要查看 AST,使用 CLI 标志--dump-ast(短选项-a,见 cli/src/main.rs),它支持三种输出格式:
| 格式 | 说明 |
|---|---|
Debug | 默认格式,即 Ruststd::fmt::Debug的美化输出 |
Json | 压缩的单行 JSON |
JsonPretty | 格式化缩进的 JSON |
转储文件的 AST:
cargo run -- test.js --dump-ast # AST dump 格式默认为 Debug在交互式 REPL 中同样可用:
cargo run -- --dump-ast # AST dump 格式默认为 Debug从实现上看,dump()函数会创建一个boa_parser::Parser,分别调用parse_script/parse_module完成解析,再按DumpFormat枚举(Debug/Json/JsonPretty)序列化输出,解析和 AST 生成耗时会在--time标志开启时被单独统计(见 cli/src/main.rs)。另外 AST 转储与--flowgraph互斥(conflicts_with = "graph"),两者不能同时使用。
第二阶段:字节码生成与执行追踪(--trace)
AST 生成后,Boa 会把它编译成字节码,再由 VM 执行。CLI 标志--trace可以同时打印编译产出的字节码和实际执行的每一条指令,这是观察引擎内部行为最直接的手段。例如,对下面的脚本:
let a = 1; let b = 2;运行cargo run -- test.js --trace,会得到类似如下的输出(示例取自 docs/vm.md 的"Understanding the trace output"一节):
----------------------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这段输出分四部分,含义如下(依据 docs/vm.md):
Compiled Output(编译产物):将被执行的函数的字节码与属性。Location:指令所在位置(注意指令不是等长的);Count:指令计数;Handler:异常处理器,若该指令抛异常,由哪个 handler 负责以及跳往何处;>表示 handler 起点,<表示终点;Opcode:操作码名称;Operands:操作码的操作数;Literals:字节码使用的字面量(如字符串);Bindings:字节码使用的绑定名;Functions:字节码引用的函数名;Handlers:字节码使用的异常处理器,记录相对CallFrame帧指针需要保留的栈值和环境数量。
Call Frame(调用帧):实际执行过程中的每条指令。Time:该指令执行耗时;Opcode:操作码名称;Operands:操作数;Top Of Stack:指令执行后栈顶元素。
Stack:执行结束后的栈内容追踪。- 执行结果:栈顶元素(栈为空则返回
undefined)。
关于 VM 与 trace 输出的更多细节,请阅读 docs/vm.md。需要说明的是,逐指令追踪在tracecargo feature 开启时生效,引擎内部通过vm.trace(全局)与code_block.set_traceable(true)(单函数)两个开关控制,--trace标志对应在 cli/src/main.rs 的context.set_trace(args.trace)调用。
第三阶段:指令流图(--flowgraph)
除了逐行文本追踪,你还可以获得 VM 指令的流图(flowgraph)——它是指令流的一种可视化表示,对理解控制流(跳转、分支、循环)尤其直观。
图形语义
- 图中的
Start(绿色)与End(红色)节点分别代表执行的起点与终点,它们不是指令,只是标记; - 条件指令呈菱形,
"YES"分支为绿色,"NO"分支为红色; - push/pop 环境配对使用相同颜色,并用虚线连接。
使用方式
--flowgraph默认输出 graphviz 格式(描述语言,需由dot工具渲染成图片),也可以加--flowgraph=mermaid输出 Mermaid 格式。渲染命令示例:
cargo run -- test.js --flowgraph | dot -Tpng > test.png把-Tpng换成-Tsvg即可生成 SVG 文件:
cargo run -- test.js --flowgraph | dot -Tsvg > test.svgdot来自graphviz软件包,绝大多数 Linux 发行版默认已安装;如果你不想安装,也可以使用支持 Graphviz 的在线编辑器来查看图内容。下面这张图是仓库自带的一个流图渲染示例(graphviz_flowgraph.svg,位于 docs/img/):
Mermaid 格式的图可以被 GitHub 原生渲染,无需第三方程序,只需把图内容放进一个mermaid代码块:
控制流向方向
通过--flowgraph-direction可以指定图的流向(对应 CLI 中的FlowgraphDirection枚举,见 cli/src/main.rs):
| 选项 | 含义 |
|---|---|
top-to-bottom | 从上到下(默认) |
bottom-to-top | 从下到上 |
left-to-right | 从左到右 |
right-to-left | 从右到左 |
例如:
cargo run -- test.js --flowgraph --flowgraph-direction=left-to-right该方向参数最终会映射到 VM 流图模块的Direction枚举(TopToBottom/BottomToTop/LeftToRight/RightToLeft),见 cli/src/main.rs 中的generate_flowgraph实现。
通过$boa调试对象在 JavaScript 中调试 JavaScript
有些调试动作在 JavaScript 层面很难甚至不可能完成——比如强制触发一次 GC 回收。为此 Boa 提供了$boa全局调试对象,它包含一系列实用工具,让你可以在 JavaScript 中调试 JavaScript。启用方式是在 CLI 加上--debug-object标志(见 cli/src/main.rs),该标志会调用init_boa_debug_object把$boa以全局变量的形式注入当前上下文,对象按模块划分:gc、function、object、shape、optimizer、realm、limits、string(各模块的注册代码见 cli/src/debug/mod.rs)。
例如强制触发一次 GC:
$boa.gc.collect()如果只想追踪某一个特定函数(而不用--trace那样追踪所有指令、被输出淹没),可以用$boa.function.trace(func, this, ...args)。
$boa对象的全部模块与功能完整文档见 docs/boa_object.md,下面按模块逐一展开。
模块$boa.gc
与垃圾回收器相关的函数,目前只有.collect():
$boa.gc.collect()该方法强制触发 GC 扫描堆并回收垃圾,实现上直接调用boa_gc::force_collect()(见 cli/src/debug/gc.rs)。
模块$boa.function
与函数执行和调试相关的工具函数(实现见 cli/src/debug/function.rs)。
$boa.function.bytecode(func)
返回某个函数编译后的字节码字符串。例如在 REPL 中:
>> function add(x, y) { return x + y } >> $boa.function.bytecode(add) " ------------------------Compiled Output: 'add'------------------------ Location Count Handler Opcode Operands 000000 0000 none CreateMappedArgumentsObject 000001 0001 none PutLexicalValue 2: 0 000004 0002 none GetArgument 0 000006 0003 none PutLexicalValue 2: 1 000009 0004 none GetArgument 1 000011 0005 none PutLexicalValue 2: 2 000014 0006 none PushDeclarativeEnvironment 2 000016 0007 none GetName 0000: 'x' 000018 0008 none GetName 0001: 'y' 000020 0009 none Add 000021 0010 none SetAccumulatorFromStack 000022 0011 none CheckReturn 000023 0012 none Return 000024 0013 none CheckReturn 000025 0014 none Return Constants: 0000: [ENVIRONMENT] index: 1, bindings: 1 0001: [ENVIRONMENT] index: 2, bindings: 3 0002: [ENVIRONMENT] index: 3, bindings: 0 Bindings: 0000: x 0001: y Handlers: <empty> "$boa.function.trace(func, this, ...args)
只追踪指定的函数;如果该函数调用了其他函数,被调用者的指令不会被追踪。this值以及传入的参数都可以自由指定:
>> const add = (a, b) => a + b >> $boa.function.trace(add, undefined, 1, 2) 5μs DefInitArg 0000: 'a' 2 4μs DefInitArg 0001: 'b' <empty> 0μs RestParameterPop <empty> 3μs GetName 0000: 'a' 1 1μs GetName 0001: 'b' 2 2μs Add 3 1μs Return 3 3 >>从实现看,trace会先给目标函数的CodeBlock设置 traceable 标志,调用完成后再清除(见 cli/src/debug/function.rs)。
$boa.function.traceable(func, mode)
把某个函数标记为"可追踪",使其在以后所有执行中都被追踪。它既适合一次标记多个函数,也适合追踪会挂起执行的函数(async 函数、生成器、async 生成器)。例如:
function* g() { yield 1; yield 2; yield 3; } $boa.function.traceable(g, true); var iter = g(); iter.next(); iter.next(); iter.next();输出(注意生成器挂起/恢复时的指令序列):
1μs RestParameterPop <empty> 1μs PushUndefined undefined 2μs Yield undefined 4μs GetName 0000: 'a' 1 0μs Yield 1 1μs GeneratorNext undefined 1μs Pop <empty> 15μs GetName 0001: 'b' 2 1μs Yield 2 1μs GeneratorNext undefined 1μs Pop <empty> 4μs GetName 0002: 'c' 3 1μs Yield 3$boa.function.flowgraph(func, options)
在函数级别获取指令流图,效果等同命令行--flowgraph,但不需要退出 Boa shell 再加参数。第二个参数可以是字符串(表示流图格式),也可以是对象(见 cli/src/debug/function.rs 的参数解析逻辑):
// 如果不指定,以下为默认值: { format: 'mermaid' direction: 'LeftRight' // 或 'LR' 简写 }示例:
$boa.function.flowgraph(func, 'graphviz') $boa.function.flowgraph(func, { format: 'mermaid', direction: 'TopBottom' })方向取值支持LeftRight/LR、RightLeft/RL、TopBottom/TB、BottomTop/BT及其小写形式。
模块$boa.object
提供获取对象内部信息的工具(实现见 cli/src/debug/object.rs)。
$boa.object.id(object)
返回给定对象在内存中的地址(十六进制字符串):
let o = { x: 10, y: 20 } $boa.object.id(o) // '0x7F5B3251B718' // 获取 $boa 对象自身在内存中的地址 $boa.object.id($boa) // '0x7F5B3251B5D8'实现上取object.as_ref()的裸指针并格式化为0x{:X}(见 cli/src/debug/object.rs)。
$boa.object.indexedStorageType(object)
返回对象的索引存储类型,可用于观察数组等对象在元素增删、打洞、改变属性描述符时内部存储形态的迁移:
let a = [1, 2]; $boa.object.indexedStorageType(a); // 'DenseI32' a.push(0xdeadbeef); $boa.object.indexedStorageType(a); // 'DenseI32' a.push(0.5); $boa.object.indexedStorageType(a); // 'DenseF64' a.push("Hello"); $boa.object.indexedStorageType(a); // 'DenseElement' a[100] = 100; // Make a hole $boa.object.indexedStorageType(a); // 'SparseElement' // Non-simple property descriptor (e.g., non-writable) Object.defineProperty(a, 2, { value: 10, writable: false }); $boa.object.indexedStorageType(a); // 'SparseProperty'返回值与引擎内部IndexProperties枚举一一对应:DenseI32、DenseF64、DenseElement、SparseElement、SparseProperty(见 cli/src/debug/object.rs)。
模块$boa.optimizer
包含启用/禁用优化的 getter 与 setter。
$boa.optimizer.constantFolding
访问器属性,getter 返回true(已启用)或false;setter 用于启用/禁用常量折叠优化:
$boa.optimizer.constantFolding = true $boa.optimizer.constantFolding // true$boa.optimizer.statistics
访问器属性,getter 返回优化统计是否开启;setter 控制是否把优化统计打印到stdout:
>> $boa.optimizer.constantFolding = true >> $boa.optimizer.statistics = true >> 1 + 1 Optimizer { constant folding: 1 run(s), 2 pass(es) (1 mutating, 1 checking) } 2 >>模块$boa.realm
包含用于测试跨 realm 行为的工具。
$boa.realm.create
创建一个带全新内建对象的新 realm,并返回其全局对象:
let global = $boa.realm.create(); Object != global.Object; // true模块$boa.shape
提供查询对象 shape(隐藏类)信息的函数。
$boa.shape.id(object)
返回对象 shape 在内存中的指针(十六进制字符串):
$boa.shape.id(Number) // '0x7FC35A073868' $boa.shape.id({}) // '0x7FC35A046258'$boa.shape.type(object)
返回对象的 shape 类型:
$boa.shape.type({x: 3}) // 'shared' $boa.shape.type(Number) // 'unique'$boa.shape.same(o1, o2)
若两个对象拥有相同 shape 则返回true(注意属性值不影响比较):
// 属性的值不重要! let o1 = { x: 10 } let o2 = {} $boa.shape.same(o1, o2) // false o2.x = 20 $boa.shape.same(o1, o2) // true o2.y = 200 $boa.shape.same(o1, o2) // falseshape 机制是 Boa 对象模型性能优化的核心,想深入了解可阅读 docs/shapes.md。
模块$boa.limits
提供修改运行时限制的工具(实现见 cli/src/debug/limits.rs,底层对应RuntimeLimits,默认值为递归 512 层、栈大小 1024、循环次数几乎不限,见 docs/vm.md 的 "Runtime Limits" 一节)。
$boa.limits.loop
getter 返回抛出错误前的循环迭代上限,setter 设置循环迭代上限:
$boa.limits.loop = 10; while (true) {} // RuntimeLimit: Maximum loop iteration limit 10 exceeded$boa.limits.stack
getter 返回抛出错误前的值栈大小上限,setter 设置该上限:
$boa.limits.stack = 10; function x() { return; } x(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // RuntimeLimit: exceeded maximum call stack length$boa.limits.recursion
getter 返回抛出错误前的递归深度上限,setter 设置递归上限:
$boa.limits.recursion = 100; function x() { return x(); } x(); // RuntimeLimit: Maximum recursion limit 100 exceeded$boa.limits.backtrace
getter 返回抛出错误时的回溯(backtrace)上限,setter 设置该上限:
$boa.limits.backtrace = 100; function x() { function y() { function z() { throw "Hello"; } z(); } y(); } x(); // Uncaught "Hello" // at z (test.js:6:13) // at y (test.js:8:6) // at x (test.js:10:4) // at <main> (test.js:12:2)模块$boa.string
提供查询字符串内部存储信息的函数。
$boa.string.storage(str)
返回字符串的内部存储类型:如果是 Boa 中已知的、存放在STATIC_STRINGS数组中的字符串则返回"static",否则返回"heap":
$boa.string.storage("push") // "static" $boa.string.storage("specialFunction") // "heap"$boa.string.encoding(str)
返回字符串的内部编码:
$boa.string.encoding("Greeting") // "latin1" $boa.string.encoding("挨拶") // "utf16"$boa.string.summary(str)
返回包含字符串简短摘要的对象:
$boa.string.summary("Greeting") // { storage: "heap", encoding: "latin1" }编译器 panic:获取完整回溯
如果 Boa 编译器本身发生了 panic(而不是 JavaScript 运行时错误),默认只会打印 panic 位置。要拿到完整的调用栈回溯,需要设置环境变量:
RUST_BACKTRACE=1在设置该变量后重新运行触发 panic 的命令,Rust 会打印完整的回溯栈,帮助定位 panic 发生在哪个编译/执行阶段。
使用调试器下断点
当文本级观测工具不够用时,可以直接在 Rust 侧下断点。
VS Code Debugger(推荐)
最快的入门方式是使用 VS Code 的CodeLLDB 插件(vadimcn.vscode-lldb),安装后在 Rust 源码(例如 cli/src/main.rs 或 core/engine/src/vm/mod.rs)的任意行打上断点,用调试配置启动boa,即可单步观察解析、编译与 VM 执行过程。
LLDB 手动调试
也可以使用rust-lldb直接启动编译产物。先构建 debug 版二进制(cargo build会生成target/debug/boa),然后:
rust-lldb ./target/debug/boa [arguments]例如:
rust-lldb ./target/debug/boa test.js --trace在rust-lldb会话中可以对任意 Rust 源码行设置断点(breakpoint set --file xxx.rs --line N)、单步执行(next/step)、打印变量(frame variable)等,从而在 Rust 层面观察引擎内部状态。
小结:按调试需求选择工具
| 想观察什么 | 使用什么工具 | 关键参数/入口 |
|---|---|---|
| 语法阶段产物 | AST 转储 | --dump-ast(Debug/Json/JsonPretty) |
| 编译与执行阶段 | 字节码 + 逐指令追踪 | --trace |
| 控制流可视化 | 指令流图 | --flowgraph/--flowgraph=mermaid、--flowgraph-direction |
| JS 内触发 GC、追踪单函数、改限制等 | $boa调试对象 | --debug-object,完整 API 见 docs/boa_object.md |
| 编译器 panic 定位 | Rust 回溯 | RUST_BACKTRACE=1 |
| 断点级调试 | CodeLLDB 插件 /rust-lldb | rust-lldb ./target/debug/boa [arguments] |
以上所有调试手段都建立在 docs/debugging.md 描述的核心工作流之上:先用cargo run跑起脚本或 REPL,再沿着"AST → 字节码 → 执行 → 流图 → JS 侧调试对象 → 断点"的链路逐层深入。无论你是想修 Boa 本身的 bug、验证新特性,还是单纯想理解一个 Rust 实现的 JS 引擎内部如何运转,这套工具箱都值得常备。
- 编程语言
- 编译器
- 开发工具
【免费下载链接】boa
Boa is an embeddable Javascript engine written in Rust.
相关推荐
如何快速上手SharpShooter:5分钟创建你的第一个恶意Payload
如何快速上手SharpShooter:5分钟创建你的第一个恶意Payload SharpShooter 是一个功能强大的Payload生成框架,专门用于创建和执
Boa 引擎的 `$boa` 调试对象:在 JavaScript 中直接调试字节码、Shape 与运行时限制的完整指南
Boa 引擎的 $boa 调试对象:在 JavaScript 中直接调试字节码、Shape 与运行时限制的完整指南 $boa 是 Boa JavaScript
编程语言编译器开发工具Boa 字节码快照测试指南:用 insta 追踪 JavaScript 引擎编译输出
Boa 字节码快照测试指南:用 insta 追踪 JavaScript 引擎编译输出 导读 本文介绍 Boa(用 Rust 编写的可嵌入 JavaScript
编程语言编译器开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考