<audio id="audio" title="18 | 如何进行 Wasm 应用的调试与分析？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ce/34/ce124b750f090c1ab790e8e1617f0f34.mp3"></audio>

你好，我是于航。

所有正在应用 Wasm 这门技术的开发者，都会被频繁问到这样一个问题 ——  “如何能够以最正确的方式来对一个 Wasm 模块（应用）进行调试？”

实际上，针对 Wasm 模块的调试方案与相应的工具链，暂时还没有统一的“事实标准”。而又由于 Wasm 本身可以被应用在诸如 Web 与 out-of-web 等多种场景中，这便使得对 Wasm 模块或应用的调试过程，变得十分复杂。

在本节课里我将为你总结现阶段所能够使用的一些，针对于独立 Wasm 模块或者说 Wasm 应用的调试方案。这些方案本身并没有优劣之分，你可以根据自己的实际情况和应用场景来挑选合适的 Wasm 调试方式。

这些方案将会基于不同的工具实现来展开，而关于工具本身的安装过程，你可以参考我在这节课里给出的相关链接。

总的来说，我们可以将这些方案划分为 Web 与 out-of-web 两种场景。前者对应于运行在 Web 浏览器中的 Wasm 应用，这些应用仅使用到了 Wasm 核心标准中的特性；而后者则对应于运行在如 Wasmtime 等 Wasm 运行时中的 Wasm 应用，这部分应用还将会使用到除 Wasm 核心标准之外的 WASI 抽象操作系统接口标准。

## 编译时调试

作为开发 Wasm 应用的一个必不可少的流程，“编译”是一个无论如何也要跨过去的“槛”。但总是有着各种各样的原因，导致你的应用在编译阶段会出现问题。所以我们先来看看如何调试应用在编译期发生的错误。

### Emscripten

Emscripten 作为构建可运行于 Web 浏览器上的 Wasm 应用的首选编译工具之一，它为我们提供了众多的调试选项，可以在编译过程中输出详细的调试信息以供排错之用。

#### EMCC_DEBUG

以我们上节课从零构建的 Wasm DIP 应用为例，在实际使用 emcc（Emscripten 提供的编译器）编译该项目时，我们可以通过为编译命令添加 “EMCC_DEBUG” 环境变量的方式，来让 emcc 以“调试模式”的方式来编译我们的项目，修改后的编译命令如下所示。

注：此命令行形式仅适用于 Linux / MacOS 系统，对于 Windows 则会有所区别。你可以参考 [Emscripten 官方文档](https://emscripten.org/docs/index.html)来查看相关细节。

```
EMCC_DEBUG=1 emcc dip.cc 
  -s WASM=1 
  -O3 
  --no-entry 
  -o dip.wasm

```

这里命令行中设置的环境变量 “EMCC_DEBUG” 支持三个值：0、1 与 2。其中 “0” 表示关闭调试模式，也就是默认不加该环境变量时的情况；“1” 表示输出编译时的调试性信息，同时生成包含有编译器各个阶段运行信息的中间文件。这些输出信息和文件可用于对整个 emcc 编译流程的各个步骤进行调试。以下为 emcc 的编译输出信息及所生成中间文件的截图。

<img src="https://static001.geekbang.org/resource/image/c6/c1/c69785b478c75e4e315b08d920e572c1.png" alt="" title="编译时输出的调试性信息">

在编译时输出的调试性信息中，包含有 emcc 在实际编译源代码时其各个编译阶段所实际调用的命令行信息（命令+参数）。比如在编译阶段调用的 clang++、链接阶段调用的 wasm-ld，甚至在优化阶段还会调用的 node 等等。

通过这些输出的详细命令行参数，你就能够知道 emcc 在对源代码的实际编译过程中，使用了哪些编译器参数，以及哪些缺少或错误添加的参数会影响源代码的编译流程。通过这种方式，能够在一定程度上辅助你找到项目编译失败的“根源”。

<img src="https://static001.geekbang.org/resource/image/f4/db/f4960bc276376619d24dd67d0dfa95db.png" alt="" title="编译时输出的中间调试文件">

而当为 “EMCC_DEBUG” 设置的值为 “2” 时，emcc 会生成更多的包含有中间调试性信息的文件，在这些文件中将包含有与 JavaScript 优化器相关的编译时信息。

#### -s [DEBUGGER_FLAG=VALUE]

除了我们上述介绍的 “EMCC_DEBUG” 之外，emcc 还有很多针对特定场景的编译器调试选项可以使用。而这些选项都需要以 “emcc -s [DEBUGGER_FLAG=VALUE]” 的方式，来将其应用到编译命令行中。

比如 “ASSERTIONS” 选项。该选项可用于启用 emcc 对常见内存分配错误的运行时断言检查。其值可以被设置为 “0”，“1” 或 “2”。其中，“0” 表示禁用该选项，另外的 “1” 和 “2” 随着数字的逐渐增大，表示所启用相关测试集的增多。

类似的，还有其他如 “SAFE_HEAP” 等编译器调试选项可以使用。而关于这些可以在 emcc 中使用的调试器选项信息，你可以参考[这里](https://github.com/emscripten-core/emscripten/blob/master/src/settings.js)进行了解。

## 运行时调试

相较于“编译时调试”，“运行时调试”意味着我们已经成功地编译了 Wasm 应用，但是却在实际运行时发生了错误。那本节我们来看看，如何调试那些在运行时发生异常的 Wasm 应用。

### Emscripten

为了能够调试运行在 Web 浏览器中的 Wasm 应用，我们需要在通过 Emscripten 编译应用时，为编译命令指定特殊的“调试参数”，以保留这些与调试相关的信息。而这个参数就是 “-g”。

“-g” 参数控制了 emcc 的编译调试等级，每一个调试等级都会保留更多的相应调试性信息。整个等级体系被分为 0-4 五个级别。在其中 “-g4” 级别会保留最多的调试性信息。

不仅如此，在 “-g4” 这个级别下，emcc 还会为我们生成可用于在 Web 浏览器中进行“源码级”调试的特殊 DWARF 信息。通过这些特殊格式的信息，我们便可以直接在 Web 浏览器中对 Wasm 模块编译之前的源代码进行诸如“设置断点”、“单步跟踪”等调试手段。如下所示，假设此时我们使用该参数重新编译上一节课中的 DIP Web 应用。

```
emcc dip.cc 
  -g4 
  -s WASM=1 
  -O3 
  --no-entry  
  -o dip.wasm

```

可以看到，这里在命令行中，我们使用了参数 “-g4”。编译完成后，我们便可以使用浏览器来加载这个 Web 应用。在此同时并打开“开发者面板”，来尝试直接通过“操作” C/C++ 源代码的方式，来为应用所使用的 Wasm 模块设置断点。具体你可以参考下面这张图（这里我们使用的是 Chrome 浏览器）。

<img src="https://static001.geekbang.org/resource/image/2d/45/2dd21169d170a870582c405990473745.png" alt="">

通过这种方式，我们可以方便地在 Wasm Web 应用的实际运行过程中，来调试那些发生在 Wasm 模块内部（C/C++）的“源码级”错误。

但目前这项调试功能还不是十分完善。我们仅能够在 Web 浏览器中为 C/C++ 等源代码设置断点、进行单步跟踪调试，或者是查看当前的调用栈信息。而比如“查看源代码中的变量值和类型信息”、“跟踪观察变量或表达式的值变化”等更加实用的功能，暂时还无法使用。

对于使用 Rust 语言编写的 Wasm 模块来说，我们可以通过类似地为 rustc 添加 “-g” 参数的方式，来让编译器将 DWARF 调试信息加入到生成的 Wasm 模块中。而对于直接使用 cargo 编译的 Wasm 项目来说，调试信息将会自动被默认加入到生成的模块中。

### Wasmtime

对于 out-of-web 领域中的 Wasm 应用，根据第 [07](https://time.geekbang.org/column/article/287138) 讲中的介绍，我们知道借助于 WASI，这些应用可以在保证良好可移植性的情况下，进一步与各类操作系统资源打交道。而为了能够在 Web 浏览器之外的环境中执行 Wasm 模块中的字节码，则需要诸如 Wasmtime、Lucet 等各类 Wasm 运行时的支持。

对比于在 Native 环境中直接编译而成的可执行文件来说，这些基于 WASI 构建的 Wasm 模块可以具有与这些原生可执行程序同等的能力，前提是只要 WASI 标准中支持相应的操作系统调用抽象接口即可。

能力虽然可以相同，但两者的运行时环境却完全不同。对于原生可执行程序来说，它们的实际执行过程会交由操作系统来统一负责。而对于 Wasm 模块来说，无论是运行在 Web 平台之上，还是应用于 out-of-web 领域中的 Wasm 字节码，它们都需要通过 Wasm 运行时（引擎）来提供字节码的实际执行能力。这也就造成了两者在调试过程和方法上的区别。

为了能够尽量使两者的调试方式保持一致，Wasmtime（一个 Wasm 运行时）便为我们提供了这样的一种能力，让我们可以使用诸如 LLDB 与 GDB 等专用于原生可执行程序的调试工具，来直接调试 Wasm 的二进制模块文件。不过需要注意的是，为了能够确保这个特性具有最大的可用性，我们需要使用最新版的 LLDB、GDB 以及 Wasmtime。

在此基础之上，我们便可以仅通过如下这行命令，来在 LLDB 中调试我们的 Wasm 字节码（假设这里我们要调试的 Wasm 模块文件名为 “app.wasm”）。

```
lldb -- wasmtime -g app.wasm

```

当然，现实的情况是，如果想要使用这种针对 Wasm 字节码的 out-of-web 调试方式，你需要重新编译整个 LLDB 或 GDB 调试工具链，并确保本机的 Wasmtime 已经被更新到最近的版本。其中，前者要花费不少的精力，而后者还没有发布正式的版本。因此这种调试方式所能够支持的调试功能仍有着一定的限制。更多的信息你可以参考这里的[链接](https://hacks.mozilla.org/2019/09/debugging-webassembly-outside-of-the-browser/)。

<img src="https://static001.geekbang.org/resource/image/b8/6c/b8f8bea5f284d8de518151e26a19f66c.png" alt="">

## 其他调试工具

对于其他的 Wasm 相关调试工具，这里主要推荐你使用 “[WABT](https://github.com/WebAssembly/wabt)” 。关于这个工具集，我在文章 [06](https://time.geekbang.org/column/article/286276) 中曾提到过。WABT 内置了众多可以直接对 Wasm 字节码或者 WAT 可读文本代码进行转换和分析的工具。比如用于将 WAT 格式转换为 Wasm 字节码的工具 “wat2wasm”、用于将 WAT 转换为 Flat-WAT 的工具 “wat-desugar” 等等。

除此之外，还有一些可以针对 Wasm 字节码进行“反编译”的工具，比如 “wasm-decompile” 工具可以将 Wasm 字节码反向转换为“类 C 语法格式” 的可读代码。其可读性相较于 WAT 来说可以说是又更近了一步。

<img src="https://static001.geekbang.org/resource/image/21/22/21c21fb351247ebf71edd12d347bdc22.png" alt="">

## 总结

好了，讲到这，今天的内容也就基本结束了。最后我来给你总结一下。

在今天这节课中，我们主要讲解了有关 Wasm 应用调试的一些现阶段可用的方案。Wasm 应用的构建和使用主要被分为“编译”与“运行”两个阶段，而相应的调试方案便也需要针对这两个阶段分别进行处理。

对于“编译”阶段，我们主要介绍了在通过 Emscripten 构建 Wasm 应用时，可以在编译命令行中使用的一些调试参数。其中，“EMCC_DEBUG” 参数可以让 emcc 在编译过程中输出更多的信息以用于支持应用的编译时调试。相应地，通过 “-s” 参数，我们也可以为 emcc 指定一些针对某些特定场景的调试参数，这些参数可以让 emcc 在编译过程中检查某些特定问题。

对于“运行”阶段，我们首先介绍了如何通过为 Emscripten 的编译命令添加 “-g” 参数，来让我们可以直接在 Web 浏览器中针对 Wasm 模块的编译前源代码进行调试。但就目前而言，我们能够在 Web 浏览器中获得的调试性信息，还不足以让我们可以直接高效地在浏览器中解决相应的运行时问题。

最后，我们还介绍了如何在 out-of-web 环境中调试 Wasm 字节码。这里我们依赖于 Wasmtime 所提供的支持，使得我们可以直接在诸如 LLDB、GDB 等传统调试器中调试 Wasm 字节码对应的编译前源代码。但一个重要的前提是，你需要事先安装这些调试器与 Wasmtime 的最新版本，这在某种程度上来说，可能对你也是一种负担。

总的来说，就现阶段的 Wasm 应用调试而言，无论是在 Web 平台上，还是 out-of-web 环境中，都没有一个成熟、稳定、可靠的“一站式”调试解决方案。但好在  Wasm CG 旗下的 “[WebAssembly Debugging Subgroup](https://github.com/WebAssembly/debugging)” 正在努力解决这个问题。相信在不久的将来，针对 Wasm 应用的调试不会再成为一个令开发者“望而生畏”的难题。

## 更新（2020-12-11）：

在今年的 Chrome 2020 开发者峰会上，Chrome 团队推出了一款新的 Chrome 扩展，可以帮助我们增强浏览器上的 Wasm 应用调试体验。相较于之前在浏览器中直接调试 Wasm 模块对应 C/C++ 源代码的体验，在这款扩展的帮助下，我们还可以做到诸如：查看原始 C/C++ 源代码中变量的值、对 C/C++ 源代码进行单步跟踪，甚至直接跟踪观察某个变量值的变化等等。下图所示为在借助这款插件后，对同一个项目的调试体验差异。

<img src="https://static001.geekbang.org/resource/image/33/df/33b556af454434a45c761cb5eed4fedf.png" alt="">

关于这款插件的具体使用方式和更多信息，你可以点击“[这里](https://developers.google.com/web/updates/2020/12/webassembly)”进行查看。

## **课后思考**

最后，我们来做一个思考题吧。

现阶段针对 Wasm 的调试过程虽然没有成熟的“一站式”解决方案，但各种小的调试技巧和方法却层出不穷。那么你在日常的开发工作中是怎样调试这些 Wasm 应用的呢？

今天的课程就结束了，希望可以帮助到你，也希望你在下方的留言区和我参与讨论，同时欢迎你把这节课分享给你的朋友或者同事，一起交流一下。
