在 Chrome DevTools 中调试 WASM
摘要
关于使用 Chrome DevTools 调试 WebAssembly 代码的指南,包括设置断点和捕获异常。
<p>当我在开发 Scheme 编译器的 <a class="reference external" href="https://eli.thegreenplace.net/2026/compiling-scheme-to-webassembly/">WASM 后端</a> 时,遇到了几个调试生成的 WASM 代码的棘手情况。结果发现 Chrome 的 DevTools 中有一个功能非常强大的 WASM 调试器,所以在这篇简短的文章中,我想分享一下如何使用它。</p>
<div class="section" id="the-setup-and-harness">
<h2>设置与工具</h2>
<p>本文我将使用 <a class="reference external" href="https://github.com/eliben/wasm-wat-samples">wasm-wat-samples 项目</a> 中的一个示例。实际上,所有内容都在 <a class="reference external" href="https://github.com/eliben/wasm-wat-samples/tree/main/gc-print-scheme-pairs">gc-print-scheme-pairs</a> 示例中准备好了。该示例展示了如何在 WASM 中使用 gc 引用构造类似 Scheme 的 s-表达式,并进行递归打印。该示例支持整数、布尔值和符号的嵌套对。</p>
<p>要实际查看效果,我们需要先将 WAT 文件编译为 WASM,例如使用 <a class="reference external" href="https://github.com/eliben/watgo">watgo</a>:</p>
<div class="highlight"><pre><span></span>$ cd gc-print-scheme-pairs
$ watgo parse gc-print-scheme-pairs.wat -o gc-print-scheme-pairs.wasm
</pre></div>
<p>该目录中的 <tt class="docutils literal"><span class="pre">browser-loader.html</span></tt> 文件已经期望加载 <tt class="docutils literal"><span class="pre">gc-print-scheme-pairs.wasm</span></tt>。但我们不能直接从文件系统打开它;由于它加载 WASM,这个文件需要通过本地 HTTP 服务器提供服务。我个人使用 <a class="reference external" href="https://github.com/eliben/static-server/">static-server</a>,但你也可以使用其他工具——比如 Python 内置的 <tt class="docutils literal">http.server</tt>:</p>
<div class="highlight"><pre><span></span>$ static-server
2026/04/10 08:55:20.244096 Serving directory "." on http://127.0.0.1:8080
...
</pre></div>
<p>现在,可以通过点击打印的链接并选择 <tt class="docutils literal"><span class="pre">browser-loader.html</span></tt> 文件在浏览器中打开它。</p>
</div>
<div class="section" id="the-debugging-process">
<h2>调试过程</h2>
<p>打开 Chrome DevTools,在 <em>Sources</em> 面板中,打开左侧的 <em>Page</em> 视图。它应该在 <em>wasm</em> 下有一个条目,显示我们模块的反编译 WAT 代码。注意:这段代码是从 WASM 二进制文件中反汇编出来的,因此会丢失一些 WAT 语法糖(比如折叠指令):</p>
<img alt="显示 DevTools 中 WASM 源代码位置的截图" class="align-center" src="https://eli.thegreenplace.net/images/2026/wasm-debug-screenshot1.png" />
<p>你可以通过点击代码左侧的地址列来设置断点,然后刷新页面。DevTools 调试器会重新运行程序并在断点处停止:</p>
<img alt="显示调试器在断点行停止的截图" class="align-center" src="https://eli.thegreenplace.net/images/2026/wasm-debug-screenshot2.png" />
<p>这里你可以逐过程、逐语句执行,查看局部变量和调用栈等——一个真正的调试器!</p>
</div>
<div class="section" id="debugging-unexpected-exceptions">
<h2>调试意外异常</h2>
<p>在开发编译器时,对我来说最重要的用例是调试意外异常(来自诸如 <tt class="docutils literal">ref.cast</tt> 这样的指令)。注意上一张截图右侧的复选框,上面写着 "Pause on ... exceptions"。选中这些后,DevTools 调试器会自动停在异常处并显示异常来源。让我们修改 <tt class="docutils literal"><span class="pre">gc-print-scheme-pairs.wat</span></tt> 示例来实际看看。<tt class="docutils literal">$emit_value</tt> 函数在转换之前会执行一组 <tt class="docutils literal">ref.test</tt> 检查,以确定它正在处理哪种引用;让我们在最开始添加这一行:</p>
<div class="highlight"><pre><span></span>(call $emit_bool (ref.cast (ref $Bool) (local.get $v)))
</pre></div>
<p>在不先测试的情况下假设 <tt class="docutils literal">$v</tt> 是一个布尔引用显然是错误的;这仅用于演示目的。</p>
<p>不设置任何断点,使用 <tt class="docutils literal">watgo</tt> 重新编译此代码并重新加载页面,我们会得到:</p>
<img alt="显示调试器在异常处停止的截图" class="align-center" src="https://eli.thegreenplace.net/images/2026/wasm-debug-screenshot3.png" />
<p>调试器停在引起异常的指令处;此外,在右侧的 <em>Scope</em> 面板中,我们可以看到 <tt class="docutils literal">$v</tt> 的实际类型是 <tt class="docutils literal">(ref $Pair)</tt>,所以马上就能明白发生了什么。</p>
<p>我发现这个功能在编写(或从编译器生成)使用 gc 类型和指令的非平凡 WASM 代码块时非常宝贵。</p>
</div>
<div class="section" id="debugger-vs-printfs-in-wasm">
<h2>WASM 中的调试器与 printfs</h2>
<p>"我应该使用调试器还是仅使用 printfs" 是程序员之间常见的争论话题。虽然我通常属于 "printf 调试" 派,但我并不教条,当情况需要时,我肯定会使用调试器。</p>
<p>具体来说,在调查 WASM 中的引用异常时,有两个强有力的因素使得使用调试器成为更优选择:</p>
<ol class="arabic">
<li><p class="first">一般来说,WASM 的 printf 能力并不强。我们可以从宿主导入类似 print 的函数(实际上,我们的示例正是这样做的),但它们不够灵活,而且在 WASM 中处理字符串通常很痛苦。当使用 gc 类型时,这个问题更加严重,因为这些类型甚至对宿主不可见(它们是透明引用)。如果我们想对 gc 值进行 printf 调试,我们必须首先构建大量的辅助代码。</p>
</li>
<li><p class="first">异常调试——一般来说——在有支持的调试器的情况下要容易得多。上面例子中的 <tt class="docutils literal">ref.cast</tt> 异常可能发生在代码中的任何地方。想象一下,需要调试一个非常大的 WASM 程序(由编译器生成)以找到失败的 <tt class="docutils literal">ref.cast</tt> 的来源;调试器会直接将你带到那个位置!</p>
<p>事实上,即使在 C 编程中,我也一直发现 <tt class="docutils literal">gdb</tt> 对于定位段错误和类似崩溃的根源非常有用。</p>
</li>
</ol>
</div>
查看缓存全文
缓存时间: 2026/05/16 03:36
# 在 Chrome DevTools 中调试 WASM
来源:https://eli.thegreenplace.net/2026/debugging-wasm-in-chrome-devtools
在我为 Scheme 编译器开发 WASM 后端(https://eli.thegreenplace.net/2026/compiling-scheme-to-webassembly/)时,遇到了几个调试生成的 WASM 代码的棘手情况。事实证明,Chrome 的 DevTools 内置了功能强大的 WASM 调试器,因此在这篇简短的文章中,我想分享如何使用它。
## 设置与测试框架
本文将以我的 wasm-wat-samples 项目(https://github.com/eliben/wasm-wat-samples)中的一个示例为基础。实际上,所有内容都已准备就绪,位于 gc-print-scheme-pairs(https://github.com/eliben/wasm-wat-samples/tree/main/gc-print-scheme-pairs)示例中。该示例展示了如何在 WASM 中使用 gc 引用构建类似 Scheme 的 s 表达式,并递归打印它们。该示例支持嵌套的整数对、布尔值和符号。
要查看实际效果,我们首先需要将 WAT 文件编译为 WASM,例如使用 watgo(https://github.com/eliben/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(https://github.com/eliben/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 中 WASM 源码位置的截图
你可以点击代码左侧的地址列来设置断点,然后刷新页面。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 中的 printf
“应该使用调试器还是只用 printf”是程序员之间常见的争论话题。虽然我通常站在“printf 调试”阵营,但我并不教条,当情况需要时,我肯定会使用调试器。
具体到 WASM 中调查引用异常时,有两个强因素促使我选择使用调试器:
1. 总体而言,WASM 的 printf 能力并不强。我们可以从宿主环境导入类似 print 的函数(事实上,我们的示例就是这样做的),但它们不够灵活,并且在 WASM 中处理字符串通常很痛苦。当处理 gc 类型时,这一点更加明显,因为这些类型甚至对宿主环境不可见(它们是不透明引用)。如果我们想对 gc 值进行 printf 调试,首先需要构建大量的基础框架。
2. 异常调试——通常——在有一个好的调试器支持下要容易得多。上面例子中的 ref.cast 异常可能发生在代码中的任何位置。想象一下,必须调试一个非常大的 WASM 程序(由编译器生成)来找到失败的 ref.cast 的源头;调试器直接带你到那个位置!事实上,即使对于 C 编程,我一直觉得 gdb 最有用的是定位段错误和类似崩溃的根源。
相似文章
将我的C游戏移植到WASM,这是我遇到的所有Bug
一位开发者分享了将C游戏移植到WebAssembly的经验,详细介绍了因32位与64位差异遇到的Bug,并提供了调试技巧。
weblings:在 WASM 内部将 Rust 编译为 WASM
Weblings 是一个编译为 WebAssembly 的 Rust 编译器工具链,通过带有 playground 和 Rustlings 练习的网页 UI,支持在浏览器中直接编译和执行 Rust 代码。
Wasmtime 中的垃圾回收与异常处理
Wasmtime 47 默认启用 Wasm 垃圾回收与异常提案,使具有垃圾回收和异常处理机制的高级语言能够更高效地编译到 WebAssembly。
从零编写调试器
本文开启了一个使用Rust从零构建调试器的系列,首先介绍如何使用操作系统调试API附加到Windows进程。
Debug web apps with browser use in Codex
Codex 的 Browser Use 功能新增了 Chrome DevTools Protocol 支持,使开发者能够通过检查网络流量、性能分析和控制台日志等高级功能来深入调试 Web 应用。