ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Boa 引擎的 `$boa` 调试对象:在 JavaScript 中直接调试字节码、Shape 与运行时限制的完整指南

Boa 引擎的 `$boa` 调试对象:在 JavaScript 中直接调试字节码、Shape 与运行时限制的完整指南 编程语言编译器开发工具【免费下载链接】boaBoa is an embeddable Javascript engine written in Rust.项目地址https://gitcode.com/gh_mirrors/bo/boa点击查看免费下载$boa是 Boa JavaScript 引擎Rust 编写的可嵌入式 JS 引擎通过 CLI 注入到运行上下文中的一个全局调试对象它把字节码查看、函数指令跟踪、指令流图生成、GC 强制回收、对象 Shape 检查、运行时限制调节等调试能力全部以 JavaScript API 的形式暴露出来。本文以 docs/boa_object.md 为骨架结合 cli/src/debug 与 core/engine/src/vm/runtime_limits.rs 的源码实现完整讲解$boa的 8 个功能模块及其每一个 API 的用法、输出格式与底层原理读者读完可以熟练地在 Boa REPL 中做引擎级的函数与对象内省。启用方式--debug-object命令行标志$boa默认不会出现在全局环境中需要通过命令行标志显式注入。在 Boa CLIcli/src/main.rs中debug_object字段对应的启动参数定义如下/// Inject debugging object $boa. #[arg(long)] debug_object: bool,因此启动 REPL 或执行脚本时带上该标志即可boa --debug-object在 cli/src/main.rs 的初始化流程中当检测到args.debug_object为真时会调用debug::init_boa_debug_object(context)把$boa注册为全局属性。查看 cli/src/debug/mod.rs 的init_boa_debug_object实现可以发现它本质上是执行一次context.register_global_property(js_string!($boa), boa_object, ...)将整个调试对象以普通全局变量的形式挂到全局对象上属性特性为WRITABLE | NON_ENUMERABLE | CONFIGURABLE可写、不可枚举、可配置因此它不会出现在for...in或Object.keys()中但可以被读取和覆盖。从create_boa_object的源码结构可以看到$boa被划分为 8 个按职责分离的子模块每个模块由独立的 Rust 文件构建模块对应源码职责$boa.functioncli/src/debug/function.rs字节码、指令跟踪、指令流图$boa.objectcli/src/debug/object.rs对象内存地址与索引存储类型$boa.shapecli/src/debug/shape.rs对象 Shape 的地址、类型与比较$boa.optimizercli/src/debug/optimizer.rs优化器开关与统计$boa.gccli/src/debug/gc.rs强制触发垃圾回收$boa.realmcli/src/debug/realm.rs跨 Realm 行为测试$boa.limitscli/src/debug/limits.rs运行时限制循环、栈、递归、回溯$boa.stringcli/src/debug/string.rs字符串内部存储与编码内省下面按模块逐一讲解。模块$boa.gc强制触发垃圾回收该模块目前只提供一个方法$boa.gc.collect()它强制触发 GC 对堆进行扫描并回收垃圾。其底层实现非常直接——cli/src/debug/gc.rs 中的collect函数只有一行核心逻辑boa_gc::force_collect();也就是说它调用的是 Boa 自研垃圾回收器boa_gc的强制回收入口对应 core/gc crate执行一次完整的堆扫描与回收然后返回undefined。这在排查内存泄漏、验证对象是否被正确回收时非常有用也是手动控制 GC 时机而非等待自动回收的唯一调试手段。模块$boa.function函数级字节码与指令跟踪这是$boa中功能最丰富的模块包含 4 个函数全部围绕 Boa 的字节码 VMcore/engine/src/vm展开。$boa.function.bytecode(func)导出函数的编译字节码传入一个函数返回其编译后字节码的字符串形式。以普通函数为例 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 输出中的每一行指令都对应一条 VM 操作码Location是字节码偏移Count是指令序号Opcode是操作码名Operands是对应的参数如词法绑定下标、环境引用、GetName的常量索引等。尾部还会附带Constants常量表、Bindings变量名绑定与Handlers异常处理器三个区段相当于一份完整的函数编译产物清单。从源码看cli/src/debug/function.rs 的bytecode实现会先把参数downcast_ref::OrdinaryFunction()转为普通函数对象取到其codeblock()后直接println!({code})打印到终端并返回undefined。这也意味着该函数要求传入的是普通函数对象对非对象参数会抛出TypeError: expected object类错误对非普通函数对象则报TypeError: expected an ordinary function object。$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 0000: b 2 2μs Add 3 1μs Return 3 3 输出的每一行左侧是该指令执行所耗费的微秒数如5μs随后是指令操作码与操作数最后一行3是函数调用的返回值。this值可以被替换例如传入某个对象参数列表也可以自由增减。其实现cli/src/debug/function.rs 的trace函数体现了临时标记的设计先把目标函数标记为可跟踪set_trace_flag_in_function_object(callable, true)随后用给定的this和参数真实调用它callable.call(this, arguments, context)调用结束后立刻取消标记。因此跟踪只发生在这一次调用过程中不会污染后续调用。$boa.function.traceable(func, mode)持久标记可跟踪函数将单个函数标记为在未来的所有执行中都可跟踪第二个参数传true启用、false关闭。它的价值在于两点一是可以同时把多个函数标记为可跟踪观察跨函数调用链二是可以跟踪会挂起执行并恢复的函数——异步函数、生成器generator、异步生成器。普通trace只能覆盖一次同步调用无法覆盖生成器多次next()之间的恢复执行而traceable可以。以下是一个生成器示例输入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 0000: b 2 1μs Yield 2 1μs GeneratorNext undefined 1μs Pop empty 4μs GetName 0000: c 3 1μs Yield 3可以看到每次iter.next()都会触发一次从恢复点继续执行并遇到Yield指令的过程GeneratorNext指令负责把控制权交回生成器内部。这正是traceable相比trace的核心优势场景。$boa.function.flowgraph(func, options)生成指令流图该函数用于获取函数的指令流图instruction flowgraph等价于 CLI 的--flowgraph标志但作用在函数级别无需退出 Boa shell 再附加命令行标志非常适合在 REPL 中交互式地对任意函数出图。它接受两个参数第一个是目标函数第二个可以是字符串表示流图格式也可以是一个对象。若为对象支持以下字段以下为未指定时的默认值{ format: mermaid, direction: LeftRight // 或 LR 简写 }用法示例$boa.function.flowgraph(func, graphviz) $boa.function.flowgraph(func, { format: mermaid, direction: TopBottom })format支持mermaid与graphviz两种格式分别输出 Mermaid 语法与 Graphviz DOT 语法可进一步渲染为可视化图。direction支持四向LeftRight/LR、RightLeft/RL、TopBottom/TB、BottomTop/BT大小写不敏感字符串会被转小写后匹配。从源码 cli/src/debug/function.rs 可以看到默认值逻辑format 未指定时默认Mermaiddirection 未指定时默认LeftToRight若传入对象则从对象的format/direction属性中读取若传入字符串则只解析 format未知的格式或方向字符串会抛出TypeError如Unknown format type xxx。流图数据来自code.to_graph(...)与Graph类型位于 core/engine/src/vm/flowgraph最终通过to_graphviz_format()或to_mermaid_format()序列化后以字符串返回。补充一点CLI 级的--flowgraph标志cli/src/main.rs默认格式是Graphviz并提供独立的--flowgraph-direction参数指定方向而$boa.function.flowgraph的默认格式是Mermaid二者默认值不同使用时可留意。模块$boa.object对象内部信息内省该模块提供两个用于获取对象内部信息的函数。$boa.object.id(object)对象内存地址返回给定对象在内存中的地址以字符串形式给出let o { x: 10, y: 20 } $boa.object.id(o) // 0x7F5B3251B718 // 获取 $boa 对象自身的内存地址 $boa.object.id($boa) // 0x7F5B3251B5D8源码实现cli/src/debug/object.rs 的id是取object.as_ref()的原始指针并格式化为0x{:X}大写十六进制字符串。由于 GC 对象是移动不动的non-moving heap该地址在对象存活期内保持稳定可以用来判断两个变量是否指向同一个对象。$boa.object.indexedStorageType(object)索引存储类型返回对象当前使用的索引属性indexed properties存储类型。这是观察 Boa 对象内部存储策略随数据变化而升级的绝佳工具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; // 制造一个空洞 $boa.object.indexedStorageType(a); // SparseElement // 非简单属性描述符例如不可写 Object.defineProperty(a, 2, { value: 10, writable: false }); $boa.object.indexedStorageType(a); // SparseProperty从源码看该函数读取object.borrow().properties().index_properties()并匹配IndexProperties枚举定义于 core/engine/src/object 的 property 存储体系共有五种取值含义如下返回值含义DenseI32密集存储元素全部为 32 位整数DenseF64密集存储元素包含浮点数DenseElement密集存储元素包含任意值如字符串SparseElement稀疏存储索引出现空洞元素为普通值SparseProperty稀疏存储且属性带有非简单描述符如writable: false这个 API 能让开发者直观地看到 Boa 对数组的存储优化策略整数数组保持紧凑的DenseI32一旦混入浮点数自动升级为DenseF64混入字符串升级为DenseElement索引出现空洞则退化为SparseElement出现非普通属性描述符则进一步退化为SparseProperty。模块$boa.optimizer优化器开关与统计该模块包含启用/禁用优化的 getter 与 setter 访问器属性。$boa.optimizer.constantFolding这是一个访问器属性getter 在常量折叠优化启用时返回true否则返回falsesetter 用于启用/禁用该优化$boa.optimizer.constantFolding true $boa.optimizer.constantFolding // true源码cli/src/debug/optimizer.rs中 getter 检查context.optimizer_options().contains(OptimizerOptions::CONSTANT_FOLDING)setter 则通过options.set(OptimizerOptions::CONSTANT_FOLDING, value)修改后再写回 context。也就是说它直接操作的是引擎级OptimizerOptions位标志位于 core/engine/src/optimizer对后续所有编译的代码生效。$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 可以看到输入1 1后优化器先输出一段统计摘要常量折叠运行次数、pass 次数、其中变更型与检查型 pass 各多少随后才打印求值结果2。这印证了统计信息的打印发生在优化流水线执行完毕之后且与表达式求值结果相互独立。模块$boa.realm跨 Realm 行为测试Realm 是 ECMAScript 规范中的抽象包含全局对象、内置对象集合与加载代码的机制。$boa.realm提供跨 Realm 行为的测试能力目前只有一个函数$boa.realm.create创建一个带有全新内置对象集合的新 Realm并返回它的全局对象let global $boa.realm.create(); Object ! global.Object; // true新 Realm 的Object构造器与当前 Realm 的不相同因此Object ! global.Object为true。从源码cli/src/debug/realm.rs看create的实现是直接Context::default()新建一个独立的引擎上下文并返回其global_object()。由于每个Context都拥有独立的全局对象与内置对象集合这天然就是一个新 Realm。这个 API 常用于验证跨 Realm 对象比较、原型隔离等规范行为。模块$boa.shape对象 Shape 内省在 Boa 中Shape对象形状描述了对象的属性结构属性名与顺序相同结构的对象共享同一 Shape这是属性访问优化的基础详见 docs/shapes.md。$boa.shape提供三个函数用于获取对象的 Shape 信息。$boa.shape.id(object)返回对象 Shape 在内存中的指针以十六进制字符串表示$boa.shape.id(Number) // 0x7FC35A073868 $boa.shape.id({}) // 0x7FC35A046258源码实现cli/src/debug/shape.rs通过object.borrow().shape()取到 Shape 后调用shape.to_addr_usize()得到地址并格式化。注意内置对象如Number的 Shape 地址与普通空对象的 Shape 地址不同——它们分属不同的 Shape 实例。$boa.shape.type(object)返回对象的 Shape 类型目前只有两种取值$boa.shape.type({x: 3}) // shared $boa.shape.type(Number) // uniqueshared共享 Shape普通对象字面量创建的 Shape 是可共享的多个结构相同的对象共用同一 Shape。unique唯一 Shape每个实例持有专属的 Shape典型如内置构造器对象。源码中通过shape.is_shared()判断后返回对应的字符串。$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) // falseo1 {x: 10}与空的o2结构不同比较为falseo2补上x属性后两者结构一致属性名x、顺序一致即使值不同10 与 20也返回true再添加y属性后结构再次分叉返回false。源码实现是对比两个对象 Shape 的地址shape.to_addr_usize()是否相等这从实现层面再次确认了同构即同 Shape、Shape 即结构的语义。模块$boa.limits运行时限制的动态调节该模块提供 4 组 getter/setter 访问器用于在运行时读取和修改引擎的执行限制是研究 Boa 运行时保护机制的入口。这些限制的默认值定义在 core/engine/src/vm/runtime_limits.rs 的RuntimeLimits中impl Default for RuntimeLimits { fn default() - Self { Self { loop_iteration: u64::MAX, // 循环迭代上限u64::MAX 表示无限制 recursion: 512, // 递归深度上限 backtrace_limit: 50, // 异常回溯栈帧上限 stack_size: 1024 * 10, // 值栈大小上限 } } }$boa.limits.loopgetter 返回抛出错误前的循环迭代上限setter 用于设置该上限$boa.limits.loop 10; while (true) {} // RuntimeLimit: Maximum loop iteration limit 10 exceeded当循环迭代次数超过上限时VM 抛出RuntimeLimit错误错误消息中会带上具体上限值。RuntimeLimits源码注释明确说明值为u64::MAX表示不设限制且提供了disable_loop_iteration_limit()专门用于关闭该限制。这在测试 core/engine/src/vm/tests.rs 中也有大量印证例如context.runtime_limits_mut().set_loop_iteration_limit(10)后配合循环用例验证报错行为。$boa.limits.stackgetter 返回值栈大小上限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注意这里的stack对应RuntimeLimits的stack_size默认1024 * 10限制的是 VM 值栈的大小栈空间被耗尽时抛出exceeded maximum call stack length。setter 在 cli/src/debug/limits.rs 中会把参数先to_length转换再try_into到usize若参数超过usize::MAX会抛出RangeError: Argument {value} greater than usize::MAX。$boa.limits.recursiongetter 返回递归深度上限setter 用于设置递归限制$boa.limits.recursion 100; function x() { return x(); } x(); // RuntimeLimit: Maximum recursion limit 100 exceeded默认递归上限为 512见上面RuntimeLimits默认值设置后无终止条件的递归会在超过深度时被截断并报错。同样的模式在 core/engine/src/vm/tests.rs 中通过set_recursion_limit(10)等用例验证。$boa.limits.backtracegetter 返回抛出错误时回溯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)默认回溯上限为 50 帧RuntimeLimits::default()中backtrace_limit: 50。当抛出错误时VM 会收集调用栈信息并生成带文件名与行列号的回溯at z (test.js:6:13)该限制决定回溯最多保留多少帧。这 4 组访问器在 cli/src/debug/limits.rs 中统一由FunctionObjectBuilder构造 getter/setter 函数对再通过ObjectInitializer::accessor注册为访问器属性底层分别读写context.runtime_limits()/context.runtime_limits_mut()的对应字段。也就是说$boa.limits的每一次读写都实时作用于当前Context的RuntimeLimits结构。模块$boa.string字符串内部表示内省该模块提供三个函数用于查看字符串的内部存储与编码信息帮助你理解 Boa 对字符串的两种存储位置和两种编码方案。$boa.string.storage(str)返回字符串的内部存储类型如果该字符串是 Boa 中众所周知的、存储在STATIC_STRINGS数组中的字符串则返回static否则返回heap$boa.string.storage(push) // static $boa.string.storage(specialFunction) // heap源码cli/src/debug/string.rs通过string.is_static()判断。STATIC_STRINGS是 Boa 内部预注册的静态字符串表字符串驻留池属性名等高频字符串会被驻留以加速查找而其他字符串则在堆上分配。这也解释了为什么push数组方法名属高频字符串返回static而任意的specialFunction返回heap。$boa.string.encoding(str)返回字符串的内部编码取值只有两种$boa.string.encoding(Greeting) // latin1 $boa.string.encoding(挨拶) // utf16纯 ASCII/单字节字符范围内的字符串采用latin1单字节紧凑编码而包含非 Latin-1 字符如日语挨拶的字符串采用utf16。源码中通过str.variant()匹配JsStrVariant枚举定义于 core/string crate的Latin1与Utf16两个变体这体现了 Boa 字符串类型docs/string.md为节省内存而做的双编码设计。$boa.string.summary(str)返回一个包含字符串存储与编码概要的对象$boa.string.summary(Greeting) // { storage: heap, encoding: latin1 }注意与storage的差异Greeting并非驻留字符串因此storage为heap而编码为latin1。从源码看summary会新建一个普通对象把storage与encoding两个字段以Attribute::all()完全属性写入并返回。这个 API 适合一次性获取字符串的两个核心内部属性避免两次调用。实战组合一个完整的引擎调试工作流将上述模块组合起来可以在 Boa REPL 中完成一套函数级 对象级 引擎级的调试流程用--debug-object启动 REPL获得$boa用$boa.function.bytecode(add)查看add的编译产物确认操作码序列是否符合预期用$boa.function.trace(add, undefined, 3, 4)观察单次执行的指令耗时分布对生成器等挂起函数改用$boa.function.traceable(g, true)做持续跟踪用$boa.function.flowgraph(add, graphviz)导出指令流图并渲染成可视化的控制流图用$boa.object.indexedStorageType(arr)验证数组在不同数据形态下的存储升级路径用$boa.shape.same(o1, o2)验证 Shape 共享与分叉逻辑用$boa.limits.loop/$boa.limits.recursion等设置较小的限制快速验证RuntimeLimit错误路径用$boa.gc.collect()手动触发回收配合$boa.object.id观察对象是否被回收。这套流程全部发生在 JavaScript 层无需退出 REPL、无需重新附加 CLI 标志也不要求直接阅读 Rust 内部结构是研究与调试 Boa 引擎行为尤其是字节码编译器、VM 执行、GC、Shape 系统与运行时限制机制的便捷入口。若想进一步理解这些 API 背后依赖的 VM 指令与执行模型可继续阅读 docs/vm.md 与 docs/bytecompiler.md要深入掌握$boa.shape依赖的 Shape 系统可参考 docs/shapes.md。赞分享编程语言编译器开发工具【免费下载链接】boaBoa is an embeddable Javascript engine written in Rust.项目地址https://gitcode.com/gh_mirrors/bo/boa点击查看免费下载相关推荐Authelia debug expression 命令实战对任意用户离线验证 CEL 用户属性表达式Authelia debug expression 命令实战对任意用户离线验证 CEL 用户属性表达式 本篇介绍 Authelia CLI 中 autheli编程语言编译器开发工具如何快速上手SharpShooter5分钟创建你的第一个恶意Payload如何快速上手SharpShooter5分钟创建你的第一个恶意Payload SharpShooter 是一个功能强大的Payload生成框架专门用于创建和执Boa引擎WASM支持在WebAssembly环境中运行JavaScript的完整指南Boa引擎WASM支持在WebAssembly环境中运行JavaScript的完整指南 Boa是一个用Rust编写的实验性JavaScript引擎支持Web编程语言编译器开发工具上一篇终极指南使用shadcn/ui快速美化Claude应用界面下一篇Uncle Novel书架管理完全教程分组更新与本地TXT导入技巧创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表