首頁
» 使用 Sanitizer 除錯
Clang 的未定義行為 Sanitizer (UBSan) 可用於 Emscripten。這使得更容易捕捉程式碼中的錯誤。
若要使用 UBSan,只需將 -fsanitize=undefined
傳遞給 emcc
或 em++
。請注意,您需要在編譯和連結階段都傳遞此選項,因為它會影響程式碼產生和系統函式庫。
預設情況下,使用 Emscripten 時,解參照空指標不會像傳統平台一樣立即導致區段錯誤,因為 0 只是 WebAssembly 記憶體中的一個正常位址。0 在 JavaScript Typed Array 中也是一個正常位置,這在 WebAssembly (執行階段支援程式碼、JS 函式庫方法、EM_ASM/EM_JS
等) 旁邊的 JavaScript 中是一個問題,如果您使用 -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)
...
可以透過 --pre-js
檔案設定 ASan
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
模式,可以透過執行 emcc
並加上 -sSAFE_HEAP
來啟用。這個模式會執行一些操作,其中某些操作與除錯工具(sanitizers)的功能重疊。
一般來說,SAFE_HEAP
著重於在以 Wasm 為目標時會出現的特定痛點。另一方面,除錯工具則著重於使用 C/C++ 等語言時會出現的特定痛點。這兩者之間的功能有所重疊,但並不完全相同。您應該使用哪一個取決於您想要尋找的問題類型。為了達到最大的覆蓋率,您可能會想使用所有除錯工具和 SAFE_HEAP
進行測試,但您可能需要為每個模式分別進行建置,因為並非所有除錯工具都彼此相容,而且也並非所有除錯工具都與 SAFE_HEAP
相容(因為除錯工具會做一些相當激進的事情!)。如果傳遞的旗標有問題,您會收到編譯器錯誤。一個合理的獨立測試建置組合可能是:ASan、UBsan 和 SAFE_HEAP
。
SAFE_HEAP
會針對以下特定情況產生錯誤:
NULL 指標(位址 0)的讀取或寫入。如前所述,這在 WebAssembly 和 JavaScript 中很麻煩,因為 0 只是個普通位址,因此您不會立即得到記憶體區段錯誤(segfault),這可能會令人困惑。
未對齊的讀取或寫入。這些在 WebAssembly 中可以運作,但在某些平台上,未正確對齊的讀取或寫入可能會慢很多,而且使用 wasm2js (WASM=0
) 時,它會是錯誤的,因為 JavaScript 的類型化陣列(Typed Arrays)不允許未對齊的操作。
讀取或寫入超出 sbrk()
所管理的有效記憶體頂端,也就是說,讀取或寫入的記憶體並非由 malloc()
正確分配。這並非 Wasm 特有的問題,然而,在 JavaScript 中,如果位址夠大而超出類型化陣列的範圍,則會傳回 undefined
,這可能會非常令人困惑,這就是添加這個檢查的原因(至少在 Wasm 中會拋出錯誤;SAFE_HEAP
仍然在 Wasm 中有所幫助,它會檢查 sbrk()
的記憶體頂端和 Wasm 記憶體末端之間的區域)。
SAFE_HEAP
透過對每個載入和儲存操作進行檢測來執行這些檢查。這會導致速度變慢,但它確實提供了找到所有此類問題的簡單保證。它也可以在編譯後對任意 Wasm 二進位檔執行,而除錯工具必須在從原始碼編譯時完成。
相比之下,UBSan 也可以找到 NULL 指標的讀取和寫入。然而,它不會檢測每個載入和儲存操作,因為它是在原始碼編譯期間完成的,因此只有在 clang 知道需要時才會添加檢查。這效率更高,但存在程式碼產生和最佳化改變某些東西,或 clang 遺漏特定位置的風險。
ASan 可以找到未分配記憶體的讀取或寫入,包括高於 sbrk()
管理的記憶體的位址。在某些情況下,它可能比 SAFE_HEAP
更有效率:雖然它也會檢查每個載入和儲存操作,但在它添加這些檢查後會執行 LLVM 最佳化器,這可以移除其中的一些檢查。