主页
» 使用Sanitizers调试
Clang的未定义行为sanitizer (UBSan) 可用于Emscripten。这使得捕捉代码中的bug变得更加容易。
要使用UBSan,只需将-fsanitize=undefined
传递给emcc
或em++
。请注意,您需要在编译和链接阶段都传递此选项,因为它会影响代码生成和系统库。
默认情况下,在Emscripten中,解引用空指针不会立即导致段错误,与传统平台不同,因为0在WebAssembly内存中只是一个普通地址。0在JavaScript Typed Array中也是一个正常的位置,这是一个JavaScript和WebAssembly(运行时支持代码、JS库方法、EM_ASM/EM_JS
等)之间的错误,如果使用-sWASM=0
构建,对于编译后的代码来说也是一个错误。
在启用ASSERTIONS
的构建中,会在程序执行结束时检查存储在地址0处的魔术cookie。也就是说,如果程序运行时有任何内容写入该位置,它会通知你。这只会检测写入操作,不会检测读取操作,并且无法帮助你找到实际发生错误写入的位置。
考虑以下程序,null-assign.c
int main(void) {
int *a = 0;
*a = 0;
}
如果没有UBSan,您会在程序退出时收到错误
$ emcc null-assign.c
$ node a.out.js
Runtime error: The application has corrupted its heap memory area (address zero)!
使用UBSan,您会得到发生此错误的准确行号
$ emcc -fsanitize=undefined null-assign.c
$ node a.out.js
null-assign.c:3:5: runtime error: store to null pointer of type 'int'
Runtime error: The application has corrupted its heap memory area (address zero)!
考虑以下程序,null-read.c
int main(void) {
int *a = 0, b;
b = *a;
}
如果没有UBSan,不会有任何反馈
$ emcc null-read.c
$ node a.out.js
$
使用UBSan,您会得到发生此错误的准确行号
$ emcc -fsanitize=undefined null-assign.c
$ node a.out.js
null-read.c:3:9: runtime error: load of null pointer of type 'int'
UBSan的运行时是非平凡的,它的使用会不必要地增加攻击面。出于这个原因,有一个针对生产用途而设计的最小UBSan运行时。
Emscripten支持最小运行时。要使用它,请除了-fsanitize
标志外,还传递-fsanitize-minimal-runtime
标志。
$ emcc -fsanitize=null -fsanitize-minimal-runtime null-read.c
$ node a.out.js
ubsan: type-mismatch
$ emcc -fsanitize=null -fsanitize-minimal-runtime null-assign.c
$ node a.out.js
ubsan: type-mismatch
Runtime error: The application has corrupted its heap memory area (address zero)!
Clang的地址sanitizer (ASan) 也可用于Emscripten。这使得捕捉代码中的缓冲区溢出、内存泄漏和其他相关错误变得更加容易。
要使用ASan,只需将-fsanitize=address
传递给emcc
或em++
。与UBSan一样,您需要在编译和链接阶段都传递此选项,因为它会影响代码生成和系统库。
您可能需要将INITIAL_MEMORY 增加到至少64 MB,或者设置ALLOW_MEMORY_GROWTH,以便ASan有足够的内存启动。否则,您将收到类似于以下内容的错误消息
无法将内存数组的大小扩展到 55152640 字节 (OOM)。可以 (1) 使用
-sINITIAL_MEMORY=X
编译,其中 X 大于当前值 50331648,(2) 使用-sALLOW_MEMORY_GROWTH
编译,它允许在运行时增加大小,或者 (3) 如果你希望 malloc 返回 NULL (0) 而不是中止,请使用-sABORTING_MALLOC=0
编译。
ASan 完全支持多线程环境。ASan 还对 JS 支持代码进行操作,也就是说,如果 JS 尝试从无效的内存地址读取数据,它将被捕获,就像从 Wasm 中进行访问一样。
以下是一些关于如何使用 AddressSanitizer 帮助查找错误的示例。
考虑 buffer_overflow.c
#include <string.h>
int main(void) {
char x[10];
memset(x, 0, 11);
}
$ emcc -gsource-map -fsanitize=address -sALLOW_MEMORY_GROWTH buffer_overflow.c
$ node a.out.js
=================================================================
==42==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x02965e5a at pc 0x000015f0 bp 0x02965a30 sp 0x02965a30
WRITE of size 11 at 0x02965e5a thread T0
#0 0x15f0 in __asan_memset+0x15f0 (a.out.wasm+0x15f0)
#1 0xc46 in __original_main stack_buffer_overflow.c:5:3
#2 0xcbc in main+0xcbc (a.out.wasm+0xcbc)
#3 0x800019bc in Object.Module._main a.out.js:6588:32
#4 0x80001aeb in Object.callMain a.out.js:6891:30
#5 0x80001b25 in doRun a.out.js:6949:60
#6 0x80001b33 in run a.out.js:6963:5
#7 0x80001ad6 in runCaller a.out.js:6870:29
Address 0x02965e5a is located in stack of thread T0 at offset 26 in frame
#0 0x11 (a.out.wasm+0x11)
This frame has 1 object(s):
[16, 26) 'x' (line 4) <== Memory access at offset 26 overflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow (a.out.wasm+0x15ef)
...
考虑 use_after_free.cpp
int main() {
int *array = new int[100];
delete [] array;
return array[0];
}
$ em++ -gsource-map -fsanitize=address -sALLOW_MEMORY_GROWTH use_after_free.cpp
$ node a.out.js
=================================================================
==42==ERROR: AddressSanitizer: heap-use-after-free on address 0x03203e40 at pc 0x00000c1b bp 0x02965e70 sp 0x02965e7c
READ of size 4 at 0x03203e40 thread T0
#0 0xc1b in __original_main use_after_free.cpp:4:10
#1 0xc48 in main+0xc48 (a.out.wasm+0xc48)
0x03203e40 is located 0 bytes inside of 400-byte region [0x03203e40,0x03203fd0)
freed by thread T0 here:
#0 0x5fe8 in operator delete[](void*)+0x5fe8 (a.out.wasm+0x5fe8)
#1 0xb76 in __original_main use_after_free.cpp:3:3
#2 0xc48 in main+0xc48 (a.out.wasm+0xc48)
#3 0x800019b5 in Object.Module._main a.out.js:6581:32
#4 0x80001ade in Object.callMain a.out.js:6878:30
#5 0x80001b18 in doRun a.out.js:6936:60
#6 0x80001b26 in run a.out.js:6950:5
#7 0x80001ac9 in runCaller a.out.js:6857:29
previously allocated by thread T0 here:
#0 0x5db4 in operator new[](unsigned long)+0x5db4 (a.out.wasm+0x5db4)
#1 0xb41 in __original_main use_after_free.cpp:2:16
#2 0xc48 in main+0xc48 (a.out.wasm+0xc48)
#3 0x800019b5 in Object.Module._main a.out.js:6581:32
#4 0x80001ade in Object.callMain a.out.js:6878:30
#5 0x80001b18 in doRun a.out.js:6936:60
#6 0x80001b26 in run a.out.js:6950:5
#7 0x80001ac9 in runCaller a.out.js:6857:29
SUMMARY: AddressSanitizer: heap-use-after-free (a.out.wasm+0xc1a)
...
考虑 leak.cpp
int main() {
new int[10];
}
$ em++ -gsource-map -fsanitize=address -sALLOW_MEMORY_GROWTH -sEXIT_RUNTIME leak.cpp
$ node a.out.js
=================================================================
==42==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 40 byte(s) in 1 object(s) allocated from:
#0 0x5ce5 in operator new[](unsigned long)+0x5ce5 (a.out.wasm+0x5ce5)
#1 0xb24 in __original_main leak.cpp:2:3
#2 0xb3a in main+0xb3a (a.out.wasm+0xb3a)
#3 0x800019b8 in Object.Module._main a.out.js:6584:32
#4 0x80001ae1 in Object.callMain a.out.js:6881:30
#5 0x80001b1b in doRun a.out.js:6939:60
#6 0x80001b29 in run a.out.js:6953:5
#7 0x80001acc in runCaller a.out.js:6860:29
SUMMARY: AddressSanitizer: 40 byte(s) leaked in 1 allocation(s).
请注意,由于泄漏检查是在程序退出时进行的,因此您必须使用 -sEXIT_RUNTIME
,或者手动调用 __lsan_do_leak_check
或 __lsan_do_recoverable_leak_check
。
您可以通过执行以下操作来检测 AddressSanitizer 是否已启用并运行 __lsan_do_leak_check
#include <sanitizer/lsan_interface.h>
#if defined(__has_feature)
#if __has_feature(address_sanitizer)
// code for ASan-enabled builds
__lsan_do_leak_check();
#endif
#endif
如果存在内存泄漏,这将是致命的。要检查内存泄漏并允许进程继续运行,请使用 __lsan_do_recoverable_leak_check
。
此外,如果您只希望检查内存泄漏,您可以使用 -fsanitize=leak
而不是 -fsanitize=address
。 -fsanitize=leak
不会分析所有内存访问,因此速度比 -fsanitize=address
快得多。
考虑 use_after_return.c
#include <stdio.h>
const char *__asan_default_options() {
return "detect_stack_use_after_return=1";
}
int *f() {
int buf[10];
return buf;
}
int main() {
*f() = 1;
}
请注意,要执行此检查,您必须使用 ASan 选项 detect_stack_use_after_return
。您可以通过像示例一样声明一个名为 __asan_default_options
的函数来启用此选项,或者您可以在生成的 JavaScript 中定义 Module['ASAN_OPTIONS'] = 'detect_stack_use_after_return=1'
。 --pre-js
在这里很有用。
此选项相当昂贵,因为它会将堆栈分配转换为堆分配,并且这些分配不会被重用,因此未来的访问可能会导致陷阱。因此,它默认情况下未启用。
$ emcc -gsource-map -fsanitize=address -sALLOW_MEMORY_GROWTH use_after_return.c
$ node a.out.js
=================================================================
==42==ERROR: AddressSanitizer: stack-use-after-return on address 0x02a95010 at pc 0x00000d90 bp 0x02965f70 sp 0x02965f7c
WRITE of size 4 at 0x02a95010 thread T0
#0 0xd90 in __original_main use_after_return.c:13:10
#1 0xe0a in main+0xe0a (a.out.wasm+0xe0a)
Address 0x02a95010 is located in stack of thread T0 at offset 16 in frame
#0 0x11 (a.out.wasm+0x11)
This frame has 1 object(s):
[16, 56) 'buf' (line 8) <== Memory access at offset 16 is inside this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-return (a.out.wasm+0xd8f)
...
ASan 可以通过 --pre-js
文件进行配置
Module.ASAN_OPTIONS = 'option1=a:option2=b';
例如,将上面的代码段与您的选项一起放入 asan_options.js
中,然后使用 --pre-js asan_options.js
编译。
对于独立的 LSan,请使用 Module.LSAN_OPTIONS
而不是。
要详细了解这些标志,请参阅 ASan 文档。请注意,大多数标志组合都没有经过测试,可能有效也可能无效。
malloc
/free
堆栈跟踪¶在一个经常使用 malloc
/free
(或它们的 C++ 等价物 operator new
/operator delete
)的程序中,在所有调用 malloc
/free
时都进行堆栈跟踪可能会非常昂贵。因此,如果您发现程序在使用 ASan 时速度非常慢,可以尝试使用 malloc_context_size=0
选项,如下所示
Module.ASAN_OPTIONS = 'malloc_context_size=0';
这会阻止 ASan 报告内存泄漏的位置,或者提供有关堆内存错误的内存来源的洞察力,但可能会提供巨大的速度提升。
SAFE_HEAP
的比较¶Emscripten 提供了一个 SAFE_HEAP
模式,可以通过使用 -sSAFE_HEAP
运行 emcc
来激活它。这会做很多事情,其中一些与 sanitizers 重叠。
总的来说,SAFE_HEAP
专注于针对 Wasm 时出现的具体痛点。另一方面,sanitizers 专注于使用 C/C++ 等语言时所涉及的具体痛点。这两个集合相互重叠,但并不完全相同。您应该使用哪一个取决于您要查找的错误类型。您可能希望使用所有 sanitizers 和 SAFE_HEAP
进行最大范围的测试,但您可能需要分别为每种模式进行构建,因为并非所有 sanitizers 都相互兼容,并且并非所有 sanitizers 都与 SAFE_HEAP
兼容(因为 sanitizers 会执行一些非常激进的操作!)。如果您的传递的标志存在问题,您将收到一个编译器错误。一个合理的单独测试构建集可能是:ASan、UBsan 和 SAFE_HEAP
。
SAFE_HEAP
错误的具体内容包括
空指针(地址 0)读取或写入。如前所述,这在 WebAssembly 和 JavaScript 中很烦人,因为 0 只是一个普通地址,因此您不会立即得到段错误,这可能会令人困惑。
未对齐的读写操作。这些在 WebAssembly 中有效,但在某些平台上,未对齐的读写操作可能要慢得多,而使用 wasm2js (WASM=0
) 则会导致错误,因为 JavaScript 类型化数组不允许未对齐操作。
越过有效内存顶部的读写操作,这些内存由 sbrk()
管理,也就是说,不是由 malloc()
正确分配的内存。这并非 Wasm 独有的问题,但在 JavaScript 中,如果地址过大以至于超出了类型化数组的范围,则会返回 undefined
,这可能非常令人困惑,因此添加了此功能(至少在 Wasm 中会抛出错误;SAFE_HEAP
仍然可以帮助 Wasm,通过检查 sbrk()
内存顶部和 Wasm 内存末尾之间的区域)。
SAFE_HEAP
通过对每个加载和存储操作进行检测来执行这些检查。这会减慢速度,但它确实保证可以找到所有此类问题。它也可以在编译后对任意 Wasm 二进制文件执行,而消毒器必须在从源代码编译时执行。
相比之下,UBSan 也可以找到空指针读写操作。但是,它不会对每个加载和存储操作进行检测,因为它是在源代码编译期间完成的,因此检查是在 clang 知道需要它们的地方添加的。这效率要高得多,但存在代码生成和优化可能改变某些内容,或者 clang 错过特定位置的风险。
ASan 可以找到未分配内存的读写操作,包括高于 sbrk()
管理内存的地址。在某些情况下,它可能比 SAFE_HEAP
更有效率:虽然它也检查每个加载和存储操作,但 LLVM 优化器在添加这些检查后运行,这可以消除其中的一些检查。