调试

跨平台 Emscripten 代码调试的主要优势之一是,相同的跨平台源代码可以在本地平台上调试,也可以使用 Web 浏览器的功能越来越强大的工具集进行调试,包括调试器、分析器和其他工具。

Emscripten 提供了许多功能和工具来帮助调试

  • 编译器调试信息标志,允许您在已编译的代码中保留调试信息,甚至创建源映射,以便您在浏览器中调试时可以单步执行本机 C++ 源代码。

  • 调试模式,它会发出调试日志并存储中间构建文件以供分析。

  • 编译器设置,以启用内存访问和常见分配错误的运行时检查。

  • 手动打印调试 Emscripten 生成的代码也受支持,在某种程度上甚至比本地平台更好。

  • AutoDebugger,它会自动为 LLVM 位代码添加代码以写入每个存储到内存中的数据。

本文介绍了 Emscripten 提供的主要工具和设置,用于调试,以及一个部分解释如何调试一些 Emscripten 特定的问题.

在浏览器中调试

Emcc 可以以两种格式输出调试信息,要么作为 DWARF 符号,要么作为源映射。两者都允许您在浏览器的调试器中查看和调试C/C++ 源代码。DWARF 提供了最精确和详细的调试体验,并且在 Chrome 88 中被支持为一项实验,使用一个扩展 <https://goo.gle/wasm-debugging-extension>。请查看这里 <https://developer.chrome.com/blog/wasm-debugging-2020/>了解详细的使用指南。源映射在 Firefox、Chrome 和 Safari 中得到了更广泛的支持,但与 DWARF 不同,它们不能用于检查变量,例如。

Emcc 默认情况下会从优化构建中剥离大部分调试信息。DWARF 可以使用 emcc -g 标志 生成,源映射可以使用 -gsource-map 选项生成。请注意,优化级别-O1 及更高级别会越来越多地删除 LLVM 调试信息,还会禁用运行时ASSERTIONS 检查。传递 -g 标志也会影响生成的 JavaScript 代码,并保留空白、函数名称和变量名称,

提示

即使对于中等规模的项目,DWARF 调试信息的大小也可能很大,并对页面性能产生负面影响,尤其是模块的编译和加载。调试信息也可以使用 -gseparate-dwarf 选项在旁边文件中生成!调试信息的大小也会影响链接时间,因为所有目标文件中的调试信息都需要链接。传递 -gsplit-dwarf 选项可以在这里有所帮助,它会导致 clang 将调试信息分散到目标文件中。然后,需要使用 emdwp 工具将调试信息链接到 DWARF 包文件 (.dwp) 中,但这可以在与已编译输出的链接并行进行!在链接后运行它,就像 emdwp -e foo.wasm -o foo.wasm.dwp 那样简单,或者在与 -gseparate-dwarf 结合使用时使用 emdwp -e foo.debug.wasm -o foo.debug.wasm.dwp(dwp 文件应该与主符号文件具有相同的名称,但扩展名为 .dwp)。

-g 标志也可以指定整数级别:-g0-g1-g2-gsource-map 的默认值)和 -g3-g 的默认值)。每个级别都建立在最后一个级别之上,以便在已编译的输出中提供越来越多的调试信息。

注意

因为 Binaryen 优化会进一步降低 DWARF 信息的质量,所以除非其他选项要求,否则 -O1 -g 将完全跳过运行 Binaryen 优化器 (wasm-opt)。如果要确保保留调试信息,您也可以添加 -sERROR_ON_WASM_CHANGES_AFTER_LINK 选项!请查看Skipping Binaryen了解更多详情。

注意

在与调试标志结合使用时,某些优化可能会被禁用,无论是在 Binaryen 优化器(即使它运行)还是 JavaScript 优化器中。例如,如果您使用 -O3 -g 编译,Binaryen 优化器将跳过一些不会生成有效 DWARF 信息的优化过程,并且一些正常的 JavaScript 优化也将被禁用,以便更好地提供所请求的调试信息。

调试模式 (EMCC_DEBUG)

可以将 EMCC_DEBUG 环境变量设置为启用 Emscripten 的调试模式

# Linux or macOS
EMCC_DEBUG=1 emcc test/hello_world.cpp -o hello.html

# Windows
set EMCC_DEBUG=1
emcc test/hello_world.cpp -o hello.html
set EMCC_DEBUG=0

在设置了 EMCC_DEBUG=1 之后,emcc 会发出调试输出并为编译器的各个阶段生成中间文件。EMCC_DEBUG=2 还会为每个 JavaScript 优化过程生成中间文件。

调试日志和中间文件输出到 TEMP_DIR/emscripten_temp,其中 TEMP_DIR 是 OS 默认的临时目录(例如,UNIX 上的 /tmp)。

调试日志可以分析,以分析和查看每个步骤所做的更改。

注意

更少的调试信息也可以通过指定 详细输出 编译器标志 (emcc -v) 来启用。

编译器设置

Emscripten 有许多编译器设置,可以用于调试。这些设置使用 emcc -s 选项设置,并将覆盖任何优化标志。例如

emcc -O1 -sASSERTIONS test/hello_world

一些重要的设置是

  • ASSERTIONS=1 用于启用对常见内存分配错误(例如,写入超过分配的内存)的运行时检查。它还定义了 Emscripten 如何处理程序流中的错误。可以将该值设置为 ASSERTIONS=2 以运行更多测试。

    ASSERTIONS=1 默认情况下是启用的。对于优化代码 (-O1 及更高级别),断言将被关闭。

  • SAFE_HEAP=1 添加了额外的内存访问检查,并将对诸如取消引用 0 和内存对齐问题等问题给出清晰的错误。

    您还可以设置 SAFE_HEAP_LOG 来记录 SAFE_HEAP 操作。

  • 通过添加 STACK_OVERFLOW_CHECK=1 链接器标志,会在栈的末尾添加一个运行时魔数标记值,该值会在特定位置进行检查,以验证用户代码是否意外地写过了栈的末尾。虽然在 Emscripten 栈上溢出对 JavaScript(不受影响)来说不是安全问题,但写过栈会导致 Emscripten HEAP 中全局数据和动态分配的内存部分发生内存损坏,这会导致应用程序以意外的方式失败。值 STACK_OVERFLOW_CHECK=2 会启用更详细的栈保护检查,这可以提供更精确的调用栈,但会牺牲一些性能。如果设置了 ASSERTIONS=1,则默认值为 1,否则禁用。

src/settings.js 中定义了许多其他有用的调试设置。有关更多信息,请在该文件中搜索关键词“check”和“debug”。

Sanitizers

Emscripten 还支持 Clang 的一些 sanitizers,例如 Undefined Behaviour SanitizerAddress Sanitizer

emcc 详细输出

使用 emcc -v 编译会导致 Emscripten 输出它运行的子命令以及将 -v 传递给 Clang。

手动打印调试

您也可以使用 printf() 语句手动对源代码进行检测,然后编译并运行代码以调查问题。请注意,printf() 是行缓冲的,请确保添加 \n 才能在控制台中看到输出。

如果您对问题行有很好的了解,可以在 JavaScript 中添加 print(new Error().stack) 以在该点获取堆栈跟踪。

调试打印甚至可以执行任意 JavaScript。例如

function _addAndPrint($left, $right) {
  $left = $left | 0;
  $right = $right | 0;
  //---
  if ($left < $right) console.log('l<r at ' + stackTrace());
  //---
  _printAnInteger($left + $right | 0);
}

使用 Chrome Devtools 进行调试

Chrome Devtools 支持对带有 DWARF 信息的 WebAssembly 文件进行源代码级调试。要使用它,您需要以下 Wasm 调试扩展插件:https://goo.gle/wasm-debugging-extension

有关详细信息,请参阅 Debugging WebAssembly with modern tools

从 JavaScript 处理 C++ 异常

请参阅 Handling C++ Exceptions from JavaScript

Emscripten 特定问题

内存对齐问题

Emscripten 内存表示 与 C 和 C++ 兼容。但是,当涉及未定义的行为时,您可能会看到与原生架构的差异,以及 Emscripten 对 asm.js 和 WebAssembly 的输出之间的差异

  • 在 asm.js 中,加载和存储必须对齐,并且对未对齐地址执行正常加载或存储可能会静默失败(访问错误的地址)。如果编译器知道加载或存储未对齐,它可以以有效但缓慢的方式模拟它。

  • 在 WebAssembly 中,未对齐的加载和存储将起作用。每个都用其预期对齐方式进行注释。如果实际对齐方式不匹配,它仍然可以工作,但在某些 CPU 架构上可能会很慢。

提示

SAFE_HEAP 可用于揭示内存对齐问题。

通常最好避免未对齐的读写——通常它们是由于上面提到的未定义行为造成的。但是,在某些情况下,它们是不可避免的——例如,如果要移植的代码从某些预先存在的数据格式的打包结构中读取一个 int。在这种情况下,为了使 asm.js 中的事物正常工作,并在 WebAssembly 中速度很快,您必须确保编译器知道加载或存储未对齐。为此,您可以

  • 手动读取单个字节并重建完整的值

  • 使用 emscripten_align* 类型定义,这些类型定义了基本类型的未对齐版本(shortintfloatdouble)。对这些类型的所有操作都没有完全对齐(在大多数情况下使用 1 变体,这意味着根本没有对齐)。

函数指针问题

如果您从函数指针调用到 nullFuncb0b1(可能伴随着错误消息“错误的函数指针”)收到一个 abort(),问题在于该函数指针在调用时在预期的函数指针表中未找到。

注意

nullFunc 是用于填充函数指针表中空索引条目的函数(b0b1 是在更优化的构建中用于 nullFunc 的较短名称)。指向无效索引的函数指针将调用此函数,该函数仅调用 abort()

有几个可能的原因

  • 您的代码正在调用一个从另一个类型转换的函数指针(这是未定义的行为,但它确实发生在实际代码中)。在优化的 Emscripten 输出中,每个函数指针类型都存储在一个基于其原始签名的单独表中,因此您必须使用相同的签名调用函数指针才能获得正确的行为(请参阅代码可移植性部分中的 函数指针问题以获取更多信息)。

  • 您的代码正在对 NULL 指针调用方法或解除引用 0。这种类型的错误可能是由任何类型的编码错误引起的,但表现为函数指针错误,因为该函数在运行时无法在预期的表中找到。

为了调试这类问题

  • 使用 -Werror 编译。这将警告转换为错误,这可能有用,因为一些未定义行为的情况否则将显示警告。

  • 使用 -sASSERTIONS=2 以获取有关正在调用的函数指针及其类型的有用信息。

  • 查看浏览器堆栈跟踪以查看错误发生的位置以及应该调用哪个函数。

  • 使用 -Wcast-function-type 在危险的函数指针转换上启用 clang 警告。

  • 使用 SAFE_HEAP=1 构建。

  • 使用 Sanitizers 进行调试 在这里可以提供帮助,特别是 UBSan。

另一个函数指针问题是调用了错误的函数。 SAFE_HEAP=1 可以帮助解决这个问题,因为它检测到函数表访问中的一些可能错误。

无限循环

无限循环会导致页面挂起。经过一段时间后,浏览器会通知用户页面卡住了,并提供停止或关闭页面的选项。

如果您的代码遇到无限循环,一种简单的方法是使用JavaScript 分析器查找问题代码。在 Firefox 分析器中,如果代码进入无限循环,您将在配置文件的末尾看到重复执行同一操作的代码块。

注意

如果您的应用程序使用无限主循环,可能需要重新编码 浏览器主循环

分析

速度

要分析代码的速度,请使用 分析信息 进行构建,然后在浏览器的开发者工具分析器中运行代码。然后您应该能够看到大多数时间是在哪些函数中花费的。

内存

浏览器的内存分析工具通常只了解 JavaScript 级别的分配。从这个角度来看,emscripten 编译的应用程序使用的整个线性内存是一个大的分配(一个 WebAssembly.Memory)。devtools 不会显示该对象内部使用情况的信息,因此您需要其他工具来完成此操作,我们将在此处进行描述。

Emscripten 支持 mallinfo(),它允许您从 dlmalloc 获取有关当前分配的信息。有关示例用法,请参阅 测试

Emscripten 还具有一个 --memoryprofiler 选项,它以视觉方式显示内存使用情况,让您看到内存碎片化程度等等。要使用它,您可以执行以下操作

emcc test/hello_world.c --memoryprofiler -o page.html

请注意,您需要像该示例中那样发出 HTML,因为内存分析器输出将呈现到页面上。要查看它,请在浏览器中加载 page.html(请记住使用 本地 Web 服务器)。显示会自动更新,因此您可以打开开发者工具控制台并运行类似 _malloc(1024 * 1024) 的命令。这将分配 1MB 的内存,然后将显示在内存分析器显示上。

AutoDebugger

AutoDebugger 是调试 Emscripten 代码的“核武器”。

警告

此选项主要面向 Emscripten 核心开发者。

AutoDebugger 将重写输出,以便它打印出对内存的每次存储操作。这很有用,因为您可以比较不同编译器设置的输出,以检测回归问题。

AutoDebugger 能够找到生成代码中的 **任何** 问题,因此它严格来说比 CHECK_* 设置和 SAFE_HEAP 更强大。AutoDebugger 的一个用途是快速输出大量日志记录,然后可以检查这些日志记录中的异常行为。AutoDebugger调试回归问题 也特别有用。

AutoDebugger 有一些局限性

  • 它会生成大量输出。使用 diff 工具可以非常有效地识别更改。

  • 它会打印出简单的数值而不是指针地址(因为指针地址在每次运行之间都会发生变化,因此无法进行比较)。这是一种限制,因为有时检查地址可以显示指针地址为 0 或过大的错误。可以在 tools/autodebugger.py 中修改该工具以将地址作为整数打印出来。

要运行 AutoDebugger,请在设置了环境变量 EMCC_AUTODEBUG=1 的情况下进行编译。例如

# Linux or macOS
EMCC_AUTODEBUG=1 emcc test/hello_world.cpp -o hello.html

# Windows
set EMCC_AUTODEBUG=1
emcc test/hello_world.cpp -o hello.html
set EMCC_AUTODEBUG=0

AutoDebugger 回归问题工作流程

使用以下工作流程来查找 AutoDebugger 中的回归问题。

  • 在环境中设置了 EMCC_AUTODEBUG=1 的情况下编译工作代码。

  • 在环境中再次使用 EMCC_AUTODEBUG=1 编译代码,但这次使用导致回归问题的设置。完成此步骤后,我们便拥有一个回归问题之前的构建和一个回归问题之后的构建。

  • 运行两个版本的编译代码并保存其输出。

  • 使用 diff 工具比较输出。

输出之间的任何差异都可能是由错误造成的。

注意

您可能希望使用 -sDETERMINISTIC,这将确保计时和其他问题不会导致误报。

需要帮助?

Emscripten 测试套件 包含 Emscripten 提供的几乎所有功能的良好示例。如果您遇到问题,建议您搜索该套件,以确定是否有具有类似行为的测试代码能够运行。

如果您已尝试过这里介绍的想法,但仍需更多帮助,请 联系我们