偵錯跨平台 Emscripten 程式碼的主要優點之一是,相同的跨平台原始碼可以在原生平台上偵錯,也可以使用網頁瀏覽器功能日益強大的工具集進行偵錯,包括偵錯工具、分析工具和其他工具。
Emscripten 提供了許多功能和工具來協助偵錯
編譯器偵錯資訊旗標,可讓您在編譯後的程式碼中保留偵錯資訊,甚至可以建立來源對應,讓您可以在瀏覽器中偵錯時逐步執行原生 C++ 原始碼。
偵錯模式,會發出偵錯日誌並儲存中間建置檔案以供分析。
編譯器設定,可啟用記憶體存取和常見配置錯誤的執行階段檢查。
手動列印偵錯也支援 Emscripten 產生的程式碼,在某些方面甚至比原生平台更好。
AutoDebugger,會自動檢測 LLVM 位元碼,以寫出每個儲存至記憶體的動作。
本文說明 Emscripten 提供的主要工具和設定以進行偵錯,並附帶一個章節說明如何偵錯許多Emscripten 特定問題。
Emcc 可以兩種格式輸出偵錯資訊,一種是 DWARF 符號,另一種是來源對應。兩者都可讓您在瀏覽器的偵錯工具中檢視和偵錯C/C++ 原始碼。DWARF 提供最精確和詳細的偵錯體驗,並在 Chrome 88 中以實驗形式支援,並搭配一個擴充功能 <https://goo.gle/wasm-debugging-extension>。如需詳細的使用者指南,請參閱此處 <https://developer.chrome.com/blog/wasm-debugging-2020/>。來源對應在 Firefox、Chrome 和 Safari 中獲得更廣泛的支援,但與 DWARF 不同的是,它們不能用於檢查變數等。
Emcc 預設會從最佳化建置中移除大部分的偵錯資訊。DWARF 可以使用 emcc -g 旗標產生,而來源對應可以使用 -gsource-map 選項發出。請注意,最佳化層級 -O1 及以上會越來越多地移除 LLVM 偵錯資訊,並停用執行階段 ASSERTIONS 檢查。傳遞 -g
旗標也會影響產生的 JavaScript 程式碼,並保留空格、函式名稱和變數名稱。
提示
即使是中型專案,DWARF 偵錯資訊也可能非常龐大,並對頁面效能產生負面影響,特別是模組的編譯和載入。也可以使用 -gseparate-dwarf 選項,將偵錯資訊發出到側邊的檔案中!偵錯資訊大小也會影響連結時間,因為所有物件檔案中的偵錯資訊也需要連結。在這裡,傳遞 -gsplit-dwarf 選項會有所幫助,這會導致 clang 將偵錯資訊散佈在物件檔案中。然後,需要使用 emdwp
工具將該偵錯資訊連結到 DWARF 套件檔案 (.dwp
) 中,但這可以在編譯輸出的連結平行進行!在連結後執行時,就像 emdwp -e foo.wasm -o foo.wasm.dwp
一樣簡單,或是在與 -gseparate-dwarf
一起使用時,如同 emdwp -e foo.debug.wasm -o foo.debug.wasm.dwp
一樣簡單 (dwp 檔案應該與主符號檔案具有相同檔名,並帶有額外的 .dwp
副檔名)。
也可以使用整數層級來指定 -g
旗標:-g0、-g1、-g2 (預設為 -gsource-map
) 和 -g3 (預設為 -g
)。每個層級都以前一個層級為基礎,以在編譯輸出中提供越來越多的偵錯資訊。
注意
因為 Binaryen 最佳化會進一步降低 DWARF 資訊的品質,-O1 -g
將會跳過執行 Binaryen 最佳化程式 (wasm-opt
),除非其他選項有要求。如果您想要確保保留偵錯資訊,也可以加入 -sERROR_ON_WASM_CHANGES_AFTER_LINK
選項。如需更多詳細資訊,請參閱 跳過 Binaryen。
注意
在與 Binaryen 最佳化程式 (即使它有執行) 和 JavaScript 最佳化程式中的偵錯旗標結合使用時,可能會停用某些最佳化。例如,如果您使用 -O3 -g
編譯,Binaryen 最佳化程式將會跳過一些不會產生有效 DWARF 資訊的最佳化過程,並且會停用一些正常的 JavaScript 最佳化,以便更好地提供所要求的偵錯資訊。
可以設定 EMCC_DEBUG
環境變數以啟用 Emscripten 的偵錯模式
# Linux or macOS
EMCC_DEBUG=1 emcc test/hello_world.cpp -o hello.html
# Windows
set EMCC_DEBUG=1
emcc test/hello_world.cpp -o hello.html
set EMCC_DEBUG=0
設定 EMCC_DEBUG=1
後,emcc 會發出偵錯輸出,並為編譯器的各個階段產生中繼檔案。EMCC_DEBUG=2
還會為每個 JavaScript 最佳化程式過程產生中繼檔案。
偵錯日誌和中繼檔案會輸出至 TEMP_DIR/emscripten_temp,其中 TEMP_DIR
是作業系統預設的暫存目錄 (例如 UNIX 上的 /tmp)。
可以分析偵錯日誌,以分析和檢視每個步驟中所做的變更。
注意
也可以透過指定 詳細輸出 編譯器旗標 (emcc -v
) 來啟用更有限的偵錯資訊。
Emscripten 提供了許多編譯器設定,可用於除錯。這些設定是使用 emcc -s 選項設定,並且會覆蓋任何最佳化標誌。例如:
emcc -O1 -sASSERTIONS test/hello_world
一些重要的設定如下:
ASSERTIONS=1
用於啟用常見記憶體配置錯誤的執行階段檢查(例如,寫入超過已配置的記憶體)。它也定義了 Emscripten 應如何處理程式流程中的錯誤。可以將值設定為ASSERTIONS=2
以執行額外的測試。
ASSERTIONS=1
預設為啟用。對於最佳化程式碼(-O1 及以上),斷言會被關閉。
SAFE_HEAP=1
會增加額外的記憶體存取檢查,並會針對諸如取消引用 0 和記憶體對齊問題等問題提供明確的錯誤訊息。您也可以設定
SAFE_HEAP_LOG
來記錄SAFE_HEAP
操作。傳遞
STACK_OVERFLOW_CHECK=1
連結器標誌會在堆疊末端新增一個執行階段魔術權杖值,該值會在特定位置檢查,以驗證使用者程式碼是否不小心寫入了堆疊末端之外。雖然覆寫 Emscripten 堆疊對於 JavaScript(不受影響)而言不是安全性問題,但寫入堆疊之外會在 Emscripten HEAP 的全域資料和動態配置的記憶體區段中造成記憶體損毀,這會導致應用程式以意想不到的方式失敗。值STACK_OVERFLOW_CHECK=2
可啟用稍微更詳細的堆疊保護檢查,這可以提供更精確的呼叫堆疊,但會犧牲一些效能。如果設定了ASSERTIONS=1
,則預設值為 1,否則會停用。
許多其他有用的除錯設定定義於 src/settings.js。如需更多資訊,請在該檔案中搜尋關鍵字「check」和「debug」。
使用 emcc -v 進行編譯會導致 Emscripten 輸出其執行的子命令,並將 -v
傳遞給 Clang。
您也可以使用 printf()
陳述式手動檢測原始程式碼,然後編譯並執行程式碼以調查問題。請注意,printf()
是行緩衝的,請務必新增 \n
以在主控台中查看輸出。
如果您對問題所在行有很好的想法,您可以在 JavaScript 中新增 print(new Error().stack)
以在該點取得堆疊追蹤。
除錯列印甚至可以執行任意 JavaScript。例如:
function _addAndPrint($left, $right) {
$left = $left | 0;
$right = $right | 0;
//---
if ($left < $right) console.log('l<r at ' + stackTrace());
//---
_printAnInteger($left + $right | 0);
}
Chrome 開發人員工具支援在具有 DWARF 資訊的 WebAssembly 檔案上進行原始碼層級除錯。若要使用該功能,您需要此處的 Wasm 除錯擴充功能外掛程式:https://goo.gle/wasm-debugging-extension
如需詳細資訊,請參閱 使用現代工具除錯 WebAssembly。
Emscripten 記憶體表示法與 C 和 C++ 相容。但是,當涉及未定義的行為時,您可能會看到與原生架構的差異,以及 Emscripten 對 asm.js 和 WebAssembly 的輸出之間的差異。
在 asm.js 中,載入和儲存必須對齊,並且在未對齊的位址上執行正常的載入或儲存可能會無聲無息地失敗(存取錯誤的位址)。如果編譯器知道載入或儲存未對齊,它可以以一種有效但速度慢的方式來模擬它。
在 WebAssembly 中,未對齊的載入和儲存將會正常運作。每個載入和儲存都會使用其預期的對齊方式進行註釋。如果實際的對齊方式不符,它仍然會正常運作,但在某些 CPU 架構上可能會很慢。
提示
可以使用 SAFE_HEAP 來揭示記憶體對齊問題。
一般而言,最好避免未對齊的讀取和寫入,因為如上所述,它們通常是未定義行為的結果。但是,在某些情況下,它們是不可避免的,例如,如果要移植的程式碼從某些預先存在資料格式的封裝結構中讀取 int
。在這種情況下,為了使事情在 asm.js 中正確運作,並且在 WebAssembly 中快速運作,您必須確保編譯器知道載入或儲存未對齊。為此,您可以:
手動讀取個別位元組並重建完整值
使用 emscripten_align*
typedef,它定義了基本類型(short
、int
、float
、double
)的未對齊版本。對這些類型進行的所有操作都不是完全對齊的(在大多數情況下使用 1
變體,這意味著根本沒有對齊)。
如果您從對 nullFunc
或 b0
或 b1
的函式指標呼叫收到 abort()
(可能帶有錯誤訊息「函式指標不正確」),則問題是當呼叫函式指標時,在預期的函式指標表中找不到該函式指標。
注意
nullFunc
是用於填入函式指標表中的空索引項目的函式(b0
和 b1
是在更多最佳化組建中用於 nullFunc
的較短名稱)。指向無效索引的函式指標將會呼叫此函式,該函式只是呼叫 abort()
。
有幾種可能的原因:
您的程式碼正在呼叫已從另一個類型轉換的函式指標(這是未定義的行為,但它確實發生在真實世界的程式碼中)。在最佳化的 Emscripten 輸出中,每個函式指標類型都會根據其原始簽名儲存在單獨的表格中,因此您必須使用相同的簽名來呼叫函式指標才能獲得正確的行為(如需更多資訊,請參閱程式碼可攜性區段中的 函式指標問題)。
您的程式碼正在對 NULL
指標呼叫方法或取消引用 0。這類錯誤可能是由任何類型的程式碼錯誤引起的,但會顯示為函式指標錯誤,因為在執行階段無法在預期的表格中找到該函式。
為了除錯這些類型的問題:
使用 -Werror
進行編譯。這會將警告轉換為錯誤,這很有用,因為某些未定義行為的情況否則會顯示警告。
使用 -sASSERTIONS=2
來取得有關所呼叫的函式指標及其類型的一些實用資訊。
查看瀏覽器堆疊追蹤,以了解錯誤發生在哪裡以及應該呼叫哪個函式。
使用 -Wcast-function-type
在危險的函式指標轉換上啟用 clang 警告。
使用 SAFE_HEAP=1 進行組建。
使用清理器進行除錯在這裡可能會有所幫助,尤其是 UBSan。
另一個函式指標問題是當呼叫了錯誤的函式時。SAFE_HEAP=1 可以幫助解決此問題,因為它可以偵測到函式表格存取的一些可能錯誤。
無限迴圈會導致您的頁面掛起。經過一段時間後,瀏覽器會通知使用者頁面卡住,並提供停止或關閉頁面的選項。
如果您的程式碼遇到無限迴圈,找到問題程式碼的一個簡單方法是使用JavaScript 分析器。在 Firefox 分析器中,如果程式碼進入無限迴圈,您會在分析的末端附近看到一區重複執行相同操作的程式碼。
注意
如果您的應用程式使用無限主迴圈,則可能需要重新編碼 瀏覽器主迴圈。
要分析程式碼的執行速度,請使用分析資訊進行建置,然後在瀏覽器的開發人員工具分析器中執行程式碼。接著您應該可以看到大部分時間都花費在哪些函式中。
瀏覽器的記憶體分析工具通常只理解 JavaScript 層級的記憶體配置。從這個角度來看,emscripten 編譯的應用程式使用的整個線性記憶體是一個單獨的大型配置 (一個 WebAssembly.Memory
)。開發人員工具不會顯示有關該物件內部使用情況的資訊,因此您需要其他工具,我們現在將說明這些工具。
Emscripten 支援 mallinfo(),讓您可以從 dlmalloc
取得有關目前配置的資訊。例如,請參閱 測試。
Emscripten 還有一個 --memoryprofiler
選項,可以視覺化方式顯示記憶體使用情況,讓您查看記憶體碎片化程度等等。要使用它,您可以執行類似以下的操作:
emcc test/hello_world.c --memoryprofiler -o page.html
請注意,您需要像範例中那樣發出 HTML,因為記憶體分析器的輸出會渲染到頁面上。要檢視它,請在瀏覽器中載入 page.html
(請記住使用本機網路伺服器)。顯示畫面會自動更新,因此您可以開啟開發人員工具主控台並執行類似 _malloc(1024 * 1024)
的指令。這將配置 1MB 的記憶體,然後將顯示在記憶體分析器顯示畫面上。
自動除錯器是除錯 Emscripten 程式碼的「核彈級選項」。
警告
此選項主要適用於 Emscripten 核心開發人員。
自動除錯器將會重寫輸出,使其印出每次寫入記憶體的動作。這很有用,因為您可以比較不同編譯器設定的輸出,以便偵測迴歸問題。
自動除錯器有可能找到產生程式碼中的任何問題,因此它比 CHECK_*
設定和 SAFE_HEAP
更強大。自動除錯器的一個用途是快速發出大量的日誌輸出,然後可以檢查這些輸出是否有異常行為。自動除錯器對於除錯迴歸問題也特別有用。
自動除錯器有一些限制
它會產生大量輸出。使用 diff 可以非常有效地識別變更。
它印出簡單的數值,而不是指標位址 (因為指標位址在執行之間會變更,因此無法比較)。這是一個限制,因為有時檢查位址可以顯示指標位址為 0 或過大的錯誤。可以修改 tools/autodebugger.py
中的工具,以將位址印出為整數。
要執行自動除錯器,請在設定環境變數 EMCC_AUTODEBUG=1
的情況下進行編譯。例如
# Linux or macOS
EMCC_AUTODEBUG=1 emcc test/hello_world.cpp -o hello.html
# Windows
set EMCC_AUTODEBUG=1
emcc test/hello_world.cpp -o hello.html
set EMCC_AUTODEBUG=0
請使用以下工作流程,透過自動除錯器尋找迴歸問題
在環境中設定 EMCC_AUTODEBUG=1
的情況下,編譯可正常運作的程式碼。
再次在環境中設定 EMCC_AUTODEBUG=1
的情況下,編譯程式碼,但這次使用會導致迴歸的設定。執行此步驟後,我們會得到一個迴歸之前的建置版本和一個迴歸之後的建置版本。
執行兩個編譯版本的程式碼,並儲存其輸出。
使用 diff 工具比較輸出。
輸出之間的任何差異都可能是由錯誤所造成。
注意
您可能想要使用 -sDETERMINISTIC
,這可確保計時和其他問題不會導致誤判。
Emscripten 測試套件包含 Emscripten 提供的幾乎所有功能的良好範例。如果您有問題,最好搜尋套件,以確定是否可以執行具有類似行為的測試程式碼。
如果您已嘗試此處的想法,但仍需要更多協助,請取得聯繫。