返回 2026-04-23
⚙️ 工程

在 Chrome DevTools 中调试 WASM 代码Debugging WASM in Chrome DevTools

作者在将 Scheme 编译器编译为 WebAssembly(WASM)时遇到调试难题。Chrome DevTools 实际上具备强大的 WASM 调试功能,包括源码映射、断点设置和变量监控。文章详细介绍了如何利用 Source Map 将 WASM 指令与原始 Scheme 代码关联,实现逐行调试。这对使用 WASM 进行前端性能优化或复杂逻辑开发具有重要实践价值。

在为我的 Scheme 编译器开发 WASM 后端时,我在调试生成的 WASM 代码时遇到了几个棘手的情况。结果发现 Chrome 的 DevTools 中有一个功能非常强大的 WASM 调试器,因此在这篇简短的博文中,我想分享一下如何使用它。

设置和测试环境

本文将使用 wasm-wat-samples 项目中的一个示例。实际上,gc-print-scheme-pairs 示例已经完成了所有准备工作。该示例展示了如何在 WASM 中使用 GC 引用构造类似 Scheme 的 s-expr,并递归地打印它们。该示例支持嵌套整数、布尔值和符号对。

要查看其效果,我们必须先将 WAT 文件编译为 WASM,例如使用 watgo:

$ cd gc-print-scheme-pairs
$ watgo parse gc-print-scheme-pairs.wat -o gc-print-scheme-pairs.wasm

该目录中的 browser-loader.html 文件已经期望加载 gc-print-scheme-pairs.wasm。但我们不能直接从文件系统打开它;由于它加载的是 WASM,因此需要通过本地 HTTP 服务器提供服务。我个人使用 static-server,但你也可以使用其他工具,比如 Python 内置的 http.server:

$ static-server
2026/04/10 08:55:20.244096 Serving directory "." on http://127.0.0.1:8080
...

现在可以通过点击打印出的链接并选择 browser-loader.html 文件在浏览器中打开。

调试过程

打开 Chrome DevTools,在 Sources 面板中,左侧打开 Page 视图。它应该在 wasm 下有一个条目,显示我们模块的反编译 WAT 代码。注意:此代码是从二进制 WASM 反汇编而来,因此会丢失一些 WAT 语法糖(如折叠指令):

可以通过点击代码左侧地址列设置断点,然后刷新页面。DevTools 调试器将重新运行程序并在断点处停止:

在这里可以单步执行、进入函数、查看局部变量和调用栈等——真正的调试器!

调试意外异常

在开发编译器时,对我来说最重要的用例是调试意外的异常(来自 ref.cast 等指令)。注意上一张截图右侧的复选框“暂停于...异常”。选中这些选项后,DevTools 调试器会自动在异常处停止,并显示其来源。让我们修改 gc-print-scheme-pairs.wat 示例来演示这一点。$emit_value 函数在执行类型转换前会进行一组 ref.test 检查以确定引用的类型;让我们在最开始添加这一行:

(call $emit_bool (ref.cast (ref $Bool) (local.get $v)))

显然,在没有先测试的情况下就假设 $v 是布尔引用是错误的;这仅用于演示目的。

不设置任何断点,用 watgo 重新编译此代码并重新加载页面,我们会得到:

调试器在导致异常的指令处停止;此外,在右侧的 Scope 面板中我们可以看到 $v 的实际类型是 (ref $Pair),因此问题一目了然。

我发现这种能力在编写或使用编译器生成涉及 GC 类型和指令的非平凡 WASM 代码片段时非常有价值。

WASM 中的调试器 vs. printf

“应该使用调试器还是仅仅使用 printf”是程序员之间常见的争论话题。虽然我通常属于“printf 调试”阵营,但我并不教条,当情况需要时,我肯定会选择调试器。

具体来说,在调查 WASM 中的引用异常时,有两个强有力的因素使得选择调试器成为更优解:

  • WASM 的 printf 功能通常并不强大。我们可以从宿主环境导入类似打印的函数(事实上,我们的示例正是这样做的),但这些函数灵活性较差,而且在 WASM 中处理字符串通常很麻烦。当涉及垃圾回收(gc)类型时情况更糟,因为这些类型对宿主环境甚至不可见(它们是不可见的引用)。如果要对 gc 值进行 printf 调试,通常需要先搭建大量辅助结构。
  • 异常调试——总体而言——如果有合适的调试器会容易得多。上面例子中的 ref.cast 异常可能发生在代码的任何位置。想象一下,要调试一个非常大的 WASM 程序(由编译器生成)来定位失败的 ref.cast 的来源;调试器能直接带你到问题现场!事实上,即使是 C 编程,我也一直觉得 gdb 在定位段错误和类似崩溃方面最有用。
  • 需要完整排版与评论请前往来源站点阅读。