☰
Boa 引擎调试实战指南:AST 转储、字节码追踪、指令流图与 $boa 调试对象全解析
2026/9/28 2:44:25 网站建设 项目流程
  • 编程语言
  • 编译器
  • 开发工具

【免费下载链接】boa

Boa is an embeddable Javascript engine written in Rust.

项目地址:https://gitcode.com/gh_mirrors/bo/boa
点击查看免费下载

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.js

CLI 会按参数顺序逐个编译并执行这些文件(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):

  1. Compiled Output(编译产物):将被执行的函数的字节码与属性。
    • Location:指令所在位置(注意指令不是等长的);
    • Count:指令计数;
    • Handler:异常处理器,若该指令抛异常,由哪个 handler 负责以及跳往何处;>表示 handler 起点,<表示终点;
    • Opcode:操作码名称;
    • Operands:操作码的操作数;
    • Literals:字节码使用的字面量(如字符串);
    • Bindings:字节码使用的绑定名;
    • Functions:字节码引用的函数名;
    • Handlers:字节码使用的异常处理器,记录相对CallFrame帧指针需要保留的栈值和环境数量。
  2. Call Frame(调用帧):实际执行过程中的每条指令。
    • Time:该指令执行耗时;
    • Opcode:操作码名称;
    • Operands:操作数;
    • Top Of Stack:指令执行后栈顶元素。
  3. Stack:执行结束后的栈内容追踪。
  4. 执行结果:栈顶元素(栈为空则返回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.svg

dot来自graphviz软件包,绝大多数 Linux 发行版默认已安装;如果你不想安装,也可以使用支持 Graphviz 的在线编辑器来查看图内容。下面这张图是仓库自带的一个流图渲染示例(graphviz_flowgraph.svg,位于 docs/img/):

Mermaid 格式的图可以被 GitHub 原生渲染,无需第三方程序,只需把图内容放进一个mermaid代码块:

![mermaid](https://web-api.gitcode.com/mermaid/svg/eNoBGwDk_y8vIOWbvuWGheWuueWGmeWcqOi_memHjC4uLuE3D-w)

控制流向方向

通过--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) // false

shape 机制是 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-lldbrust-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.

项目地址:https://gitcode.com/gh_mirrors/bo/boa
点击查看免费下载
上一篇:use-gesture国际化支持:多语言错误提示
下一篇:Resque安全加固:权限控制与敏感数据处理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询