Emscripten教程之如何除錯程式碼(六)

雲荒杯傾發表於2019-05-12

翻譯:雲荒杯傾
本文是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如何除錯程式碼

相關文章