翻譯:雲荒杯傾
本文是Emscripten-WebAssembly專欄系列文章之一,更多文章請檢視專欄。
也可以去作者的部落格閱讀文章。
歡迎加入Wasm和emscripten技術交流群,群聊號碼:939206522。
除錯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++原始碼。
note:
當你既用除錯flag又用優化flag時,有些優化可能會被禁掉,比如,如果你使用-O3 -g4 編譯,為了給你提供足夠多的除錯資訊,有一些-O3的優化就得禁用掉。
除錯模式(EMCC_DEBUG)
EMCC_DEBUG環境變數可以用來設定啟用/不啟用Emscripten的除錯模式:
# 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選項選擇這些設定,他們將覆蓋任何優化標誌。例如:
./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。例如:
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的優化器。
./emcc -O2 --js-opts 0 -g4 tests/hello_world_loop.cpp
這樣就能產生相對於llvm優化的程式碼來說更易除錯的js程式碼:
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記憶體表示假定載入和儲存是對齊的。在未對齊的地址上執行正常的載入或儲存可能會失敗。
SAFE_HEAP可以用來顯示記憶體對齊問題。
一般來說,最好避免不對齊的讀寫—–他們通常是由於未定義的行為導致的。然而,在某些情況下,它們是不可避免的——-例如,如果要移植的程式碼從一些預先存在的資料格式的打包結構(packed structure)中讀取int。
Emscripten支援未對齊的讀寫,但它們要慢得多,而且必須在絕對必要時使用。執行一個不對齊的讀或寫你可以:
- 手動讀取單個位元組並重新構造全部值
- 使用emscripten_align*型別,它定義了基本型別的不對齊版本(short,int,float,double)。這些型別的所有操作都是不完全對齊的(在大多數情況下使用1個variants,這意味著沒有任何對齊)。
函式指標問題
如果你的函式指標呼叫得到一個abort(),那麼問題是在呼叫時,沒有在預期的函式指標表中找到這個函式指標。
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
警告:
這個選項主要為Emscripten核心開發者提供使用。
Emscripten程式碼移植系列文章
Emscripten程式碼移植主題系列文章是emscripten中文站點的一部分內容。
本文是第三個主題第二篇文章。
第一個主題介紹程式碼可移植性與限制
第二個主題介紹Emscripten的執行時環境
第三個主題第一篇文章介紹連線C++和JavaScript
第三個主題第二篇文章介紹embind
第四個主題介紹檔案和檔案系統
第六個主題介紹Emscripten如何除錯程式碼