Emscripten教程之如何调试代码(六)

翻译:云荒杯倾

调试Emscripten代码的主要优点之一是,源代码既可以在本地平台上进行调试,也可以使用web浏览器日益强大的工具集——包括调试器、分析器和其他工具。

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

  • 编译器调试信息flags,允许您在已编译的代码中保存调试信息,甚至创建源映射,以便在浏览器中调试时可以单步调试c++源代码。
  • 调试模式,它产生调试日志和存储 编译时产生的中间文件 进行分析。
  • 编译器设置,使运行时检查内存访问和公共分配错误。
  • 还支持手动打印调试emscripten生成代码,这在某些方面甚至比本地平台工作效果更好。
  • 自动调试器,它会自动地使用LLVM的中间代码写到内存。

本文描述了由Emscripten提供的用于调试的主要工具和设置,以及如何调试一些Emscripten特有的问题。

调试信息

默认下,如果是优化编译,Emcc会删除大部分调试信息。Optimisation级-01和以上删除LLVM调试信息,也禁用了运行时断言检查。优化级别-02以上,代码被压缩编译器改编,变得几乎不可读。

emcc -g标志可用于在编译的输出中保存调试信息。默认情况下,此选项保护空白、函数名和变量名。

你可以使用五个级别中的一个来指定标记:-g0、-g1、-g2、-g3和-g4。每个级别都在最后编译,以在编译后的输出中逐步提供更多的调试信息。g3标志提供与-g标志相同级别的调试信息。

g4选项提供了最多的调试信息—–—它生成了源映射(source map),允许您在Firefox、Chrome或Safari浏览器的调试器中查看和调试C/C++源代码。

1
2
note:
当你既用调试flag又用优化flag时,有些优化可能会被禁掉,比如,如果你使用-O3 -g4 编译,为了给你提供足够多的调试信息,有一些-O3的优化就得禁用掉。

调试模式(EMCC_DEBUG)

EMCC_DEBUG环境变量可以用来设置启用/不启用Emscripten的调试模式:

1
2
3
4
5
6
7
# Linux or Mac OS X
EMCC_DEBUG=1 ./emcc tests/hello_world.cpp -o hello.html
# Windows
set EMCC_DEBUG=1
emcc tests/hello_world.cpp -o hello.html
set EMCC_DEBUG=0

使用EMCC_DEBUG = 1设置,emcc会产生调试输出文件,并为编译器的各个编译阶段生成中间文件。
EMCC_DEBUG= 2还为每趟JavaScript优化器遍历(pass)生成中间文件。

调试日志和中间文件输出到TEMP_DIR/emscripten_temp,其中TEMP_DIR默认在/tmp(/tmp的位置在.emscripten配置文件定义)。

可以对调试日志进行分析,以对每个步骤中所做的更改进行分析和检查。

编译器设置

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

1
./emcc -01 -s ASSERTIONS=1 tests/hello_world

最重要的设置是:

  • ASSERTIONS=1 用于为内存分配错误启用运行时检查(例如,写入比分配更多的内存)。它还定义了Emscripten如何处理程序流中的错误。可以将值设置为ASSERTIONS=2,以便运行额外的测试。
    不优化编译时,ASSERTIONS=1是默认开启的。对于优化编译的代码(-01和以上级别)它是关闭的。
  • SAFE_HEAP= 1增加了额外的内存访问检查,并将为诸如非内联化0(dereferencing 0)和内存对齐等问题提供清晰的错误。你也可以设置SAFE_HEAP_LOG以打印SAFE_HEAP操作。
  • 通过STACK_OVERFLOW_CHECK =1 标记在堆栈的末尾添加一个运行时的令牌值,令牌值会在某些位置被检查,以验证用户代码是否意外地写出了堆栈的末尾。虽然溢出Emscripten堆栈不是一个安全问题(JavaScript已经被沙箱化了),但写出堆栈将会导致全局数据内存损坏和Emscripten堆中动态分配的内存碎片化,这使得应用程序以意想不到的方式失败。值STACK_OVERFLOW_CHECK = 2启用了更详细的堆栈保护检查,它以牺牲一些性能的代价提供更精确的callstack。如果ASSERTIONS= 1,STACK_OVERFLOW_CHECK默认值为2,ASSERTIONS为其他值时STACK_OVERFLOW_CHECK默认不启用。

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

emcc详细输出

用emcc -v选项编译,将-v传递给LLVM,然后在工具链上运行Emscripten的内部完整性检查。

verbose模式还能启动Emscripten的调试模式(EMCC_DEBUG)以生成编译器的各个阶段的中间文件。

手动打印调试

您还可以用printf()语句手工编写源代码,然后编译并运行代码来研究问题。

如果你对问题行有很好的了解,你可以在JavaScript添加print(新的Error().stack)代码,以得到堆栈跟踪。另外还有stackTrace(),它发出堆栈跟踪,并尝试使用c++的去除改编的函数名(如果你不想或者不需要让c++ 函数名去除改编,你可以调用jsStackTrace())。

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

1
2
3
4
5
6
7
8
function _addAndPrint($left, $right) {
$left = $left | 0;
$right = $right | 0;
//---
if ($left < $right) console.log('l<r at ' + stackTrace());
//---
_printAnInteger($left + $right | 0);
}

禁止优化

有时候,编译的时候,禁用LLVM优化(llvm-opts)或禁用JavaScript优化(js-opts)是很有用的。

比如说,以下命令即允许调试信息又使用-O2优化(既llvm和js都优化),但是又明显关闭了js的优化器。

1
./emcc -O2 --js-opts 0 -g4 tests/hello_world_loop.cpp

这样就能产生相对于llvm优化的代码来说更易调试的js代码:

1
2
3
4
5
function _main() {
var label = 0;
var $puts=_puts(((8)|0)); //@line 4 "tests/hello_world.c"
return 1; //@line 5 "tests/hello_world.c"
}

Emscripten特有问题

内存对齐问题

Emscripten内存表示假定加载和存储是对齐的。在未对齐的地址上执行正常的加载或存储可能会失败。

1
SAFE_HEAP可以用来显示内存对齐问题。

一般来说,最好避免不对齐的读写—–他们通常是由于未定义的行为导致的。然而,在某些情况下,它们是不可避免的——-例如,如果要移植的代码从一些预先存在的数据格式的打包结构(packed structure)中读取int。

Emscripten支持未对齐的读写,但它们要慢得多,而且必须在绝对必要时使用。执行一个不对齐的读或写你可以:

  • 手动读取单个字节并重新构造全部值
  • 使用emscripten_align*类型,它定义了基本类型的不对齐版本(short,int,float,double)。这些类型的所有操作都是不完全对齐的(在大多数情况下使用1个variants,这意味着没有任何对齐)。

函数指针问题

如果你的函数指针调用得到一个abort(),那么问题是在调用时,没有在预期的函数指针表中找到这个函数指针。

1
2
note:
nullFunc是函数指针表中用于填充空索引的函数(b0和b1是优化编译下它的别名)。指向无效索引的函数指针会调用这个函数,然后调用abort().

有几个可能的原因:

  • 您的代码调用了一个从另一个类型转换来的函数指针(这是未定义的行为,但它确实发生在真实的代码中)。在优化的Emscripten输出中,每个函数指针类型都基于它的原始签名,存储在一个单独的表中,因此您必须调用具有相同签名的函数指针以获得正确的行为(更多信息参见代码可移植性部分中的函数指针问题)。
  • 您的代码在空指针或者dereferencing 0上调用方法。这种bug可以由任何类型的编码错误引起,但表现为函数指针错误,因为在运行时的预期表中无法找到函数。

为了调试这些问题:

  • 使用-Werror编译。这就把警告变成了错误,这些错误可能有用,因为一些未定义行为的情况会显示警告。
  • 使用-s ASSERTIONS=2,得到一些 关于被调用的函数指针和它的类型 有用的信息。
  • 查看浏览器堆栈跟踪,查看错误发生的地方以及应该调用哪个函数。
  • 使用SAFE_HEAP=1和禁用函数指针别名(aliasing_function_pointer = 0)编译。这使得错误类型的函数指针 不可能在不引起错误的情况下 调用,换句话说就是这样编译会使 错误类型的函数指针调用 一定会报错。调用命令:-s SAFE_HEAP=1 -s aliasing_function_pointer =0

aliasing_function_pointer = 0也很有用,因为它确保调用错误表中的指针地址会导致明显的错误。如果没有这样的设置,这样的调用只执行地址上的任何函数,这将很难进行调试。

死循环

无限循环导致您的页面挂起。在一段时间之后,浏览器将通知用户该页面被卡住并提供停止或关闭它的选择。

如果您的代码中有无限循环,那么找到问题代码的一个简单方法就是使用JavaScript profiler。在Firefox profiler中,如果代码进入无限循环,您将看到在profiler的末尾有一块代码重复执行相同的操作。

AutoDebugger

1
2
警告:
这个选项主要为Emscripten核心开发者提供使用。

Emscripten代码移植系列文章

Emscripten代码移植主题系列文章是emscripten中文站点的一部分内容。
本文是第三个主题第二篇文章。
第一个主题介绍代码可移植性与限制
第二个主题介绍Emscripten的运行时环境
第三个主题第一篇文章介绍连接C++和JavaScript
第三个主题第二篇文章介绍embind
第四个主题介绍文件和文件系统
第六个主题介绍Emscripten如何调试代码