Debugger 接口

Mozilla 的 JavaScript 引擎 SpiderMonkey 提供了一个名为 Debugger 的调试接口,它允许 JavaScript 代码观察和操作其他 JavaScript 代码的执行。Firefox 内置的开发者工具和 Firebug 扩展都使用 Debugger 来实现它们的 JavaScript 调试器。但是,Debugger 非常通用,可以用于实现其他类型的工具,例如跟踪器、覆盖率分析、补丁和继续等等。

Debugger 具有三个基本特性

  • 它是一个源代码级接口:它根据 JavaScript 语言进行操作,而不是机器语言。它操作 JavaScript 对象、堆栈帧、环境和代码,并且无论被调试者是解释、编译还是优化,都提供一致的接口。如果你对 JavaScript 语言有很好的掌握,那么你应该具备使用 Debugger 所需的所有背景知识,即使你从未了解过该语言的实现。

  • 它供JavaScript 代码使用。JavaScript 既是被调试语言,也是工具实现语言,因此使 JavaScript 在 Web 上有效的特性可以用于为开发人员创建工具。正如预期的那样,Debugger 是一个健全的接口:使用(甚至滥用)Debugger 绝不应该导致 Gecko 崩溃。错误会抛出正确的 JavaScript 异常。

  • 它是一个线程内调试 API。被调试者和使用 Debugger 观察它的代码必须在同一个线程中运行。跨线程、跨进程和跨设备的工具必须使用 Debugger 从同一线程内观察被调试者,然后自行处理任何必要的通信。(Firefox 的内置工具为此目的定义了一个协议。)

在 Gecko 中,Debugger API 仅对 chrome 代码可用。根据设计,它不应该引入安全漏洞,因此原则上也可以提供给内容;但很难证明增加攻击面的安全风险是合理的。

Debugger API 目前无法观察自托管 JavaScript。这并非 API 设计固有的,而仅仅是因为自托管基础设施尚未准备好应对 Debugger API 可以执行的入侵。

调试器实例和影子对象

Debugger 将被调试者状态的每个方面都反映为 JavaScript 值——不仅是像对象和基本类型这样的实际 JavaScript 值,还包括堆栈帧、环境、脚本和编译单元,这些通常无法作为它们自身的对象访问。

这是一个正在运行计时器回调函数的 JavaScript 程序

A running JavaScript program and its Debugger shadows

此图显示了构成 Debugger API 的各种类型的影子对象(它们都遵循一些通用约定

  • Debugger.Object 表示一个被调试者对象,提供了一个面向反射的 API,可以保护调试器避免意外调用 getter、setter、代理陷阱等等。

  • Debugger.Script 表示一段 JavaScript 代码——函数体或顶级脚本。给定一个 Debugger.Script,可以设置断点,在源位置和字节码偏移量之间转换(偏离“源代码级”设计原则),以及查找代码的其他静态特性。

  • Debugger.Frame 表示一个正在运行的堆栈帧。可以使用它们遍历堆栈并查找每个帧的脚本和环境。还可以为帧设置 onSteponPop 处理程序。

  • Debugger.Environment 表示一个环境,将变量名与存储位置关联起来。环境可能属于正在运行的堆栈帧、被函数闭包捕获,或者将某个全局对象的属性反映为变量。

Debugger 实例本身并不是被调试者中任何内容的影子;相反,它维护着一组要被视为被调试者的全局对象。一个 Debugger 只观察在这些全局对象的范围内发生的执行。可以设置函数,以便在推送新的堆栈帧时调用;加载新代码时调用;等等。

此图中省略了 Debugger.Source 实例,它们表示 JavaScript 编译单元。一个 Debugger.Source 可以提供其源代码的完整副本,并解释代码是如何进入系统的,无论是通过调用 eval<script> 元素还是其他方式。一个 Debugger.Script 指向它派生的 Debugger.Source

同样省略的是 DebuggerDebugger.Memory 实例,它包含用于观察被调试者内存使用情况的方法和访问器。

所有这些类型都遵循一些通用约定,在深入研究任何特定类型的规范之前,你应该先浏览一下。

所有影子对象对于每个 Debugger 和每个引用都是唯一的。对于给定的 Debugger,只有一个 Debugger.Object 引用特定的被调试者对象;对于特定的堆栈帧,只有一个 Debugger.Frame;等等。因此,工具可以将引用的元数据存储为影子本身上的属性,并在再次遇到相同的引用时找到该元数据。并且由于影子是每个 Debugger 的,因此工具可以在不担心干扰使用其自身 Debugger 实例的其他工具的情况下这样做。

示例

以下是一些你可以自己尝试的事情,它们展示了 Debugger 的一些功能