https://segmentfault.com/a/1190000040867861
最近在研究 WebAssembly,也寫了幾篇全面介紹的文章:
本文是學習 WebAssembly 系列的第三篇文章,也是想探究一下 Chrome 開發者工具對 WebAssembly 的調試支持度如何,通過這個探究的過程,我們會了解到 Chrome 調試工具各種方面的使用方法以及作用,發掘你可能不知道的一些知識點。
所以本文既可以當做學習使用 Chrome Devtools 調試工具的一篇比較全面的文章,也可以當做是介紹現階段我們如何在瀏覽器中對 WebAssembly 相關的代碼進行調試,幫助你成爲一個合格的調試工程師 :)。
WebAssembly 的原始調試方式
Chrome 開發者工具目前已經支持 WebAssembly 的調試,雖然存在一些限制,但是針對 WebAssembly 的文本格式的文件能進行單個指令的分析以及查看原始的堆棧追蹤,具體見如下圖:
上述的方法對於一些無其他依賴函數的 WebAssembly 模塊來說可以很好的運行,因爲這些模塊只涉及到很小的調試範圍。但是對於複雜的應用來說,如 C/C++ 編寫的複雜應用,一個模塊依賴其他很多模塊,且源代碼與編譯後的 WebAssembly 的文本格式的映射有較大的區別時,上述的調試方式就不太直觀了,只能靠猜的方式才能理解其中的代碼運行方式,且大多數人很難以看懂複雜的彙編代碼。
更加直觀的調試方式
現代的 JavaScript 項目在開發時通常也會存在編譯的過程,使用 ES6 進行開發,編譯到 ES5 及以下的版本進行運行,這個時候如果需要調試代碼,就涉及到 Source Map 的概念,source map 用於映射編譯後的對應代碼在源代碼中的位置,source map 使得客戶端的代碼更具可讀性、更方便調試,但是又不會對性能造成很大的影響。
而 C/C++ 到 WebAssembly 代碼的編譯器 Emscripten 則支持在編譯時,爲代碼注入相關的調試信息,生成對應的 source map,然後安裝 Chrome 團隊編寫的 C/C++ Devtools Support 瀏覽器擴展,就可以使用 Chrome 開發者工具調試 C/C++ 代碼了。
這裏的原理其實就是,Emscripten 在編譯時,會生成一種 DWARF 格式的調試文件,這是一種被大多數編譯器使用的通用調試文件格式,而 C/C++ Devtools Support 則會解析 DWARF 文件,爲 Chrome Devtools 在調試時提供 source map 相關的信息,使得開發者可以在 89+ 版本以上的 Chrome Devtools 上調試 C/C++ 代碼。
調試簡單的 C 應用
因爲 DWARF 格式的調試文件可以提供處理變量名、格式化類型打印消化、在源代碼中執行表達式等等,現在就讓我們實際來編寫一個簡單的 C 程序,然後編譯到 WebAssembly 並在瀏覽器中運行,查看實際的調試效果吧。
首先讓我們進入到之前創建的 WebAssembly 目錄下,激活 emcc 相關的命令,然後查看激活效果:
cd emsdk && source emsdk_env.sh
emcc --version # emcc (Emscripten gcc/clang-like replacement) 1.39.18 (a3beeb0d6c9825bd1757d03677e817d819949a77)
接着在 WebAssembly 創建一個 temp
文件夾,然後創建 temp.c
文件,填充如下內容並保存:
#include <stdlib.h>
void assert_less(int x, int y) {
if (x >= y) {
abort();
}
}
int main() {
assert_less(10, 20);
assert_less(30, 20);
}
上述代碼在執行 asset_less
時,如果遇到 x >= y
的情況會拋出異常,終止程序執行。
在終端切換目錄到 temp
目錄下執行 emcc
命令進行編譯:
emcc -g temp.c -o temp.html
上述命令在普通的編譯形式上,加入了 -g
參數,告訴 Emscripten 在編譯時爲代碼注入 DWARF 調試信息。
現在可以開啓一個 HTTP 服務器,可以使用 npx serve .
,然後訪問 localhost:5000/temp.html
查看運行效果。
需要確保已經安裝了 Chrome 擴展:https://chrome.google.com/web...,以及 Chrome Devtools 升級到 89+ 版本。
爲了查看調試效果,需要設置一些內容。
- 打開 Chrome Devtools 裏面的 WebAssembly 調試選項
設置完之後,在工具欄頂部會出現一個 Reload 的藍色按鈕,需要重新加載配置,點擊一下就好。
- 設置調試選項,在遇到異常的地方暫停
- 刷新瀏覽器,然後你會發現斷點停在了
temp.js
,由 Emscripten 編譯生成的 JS 膠水代碼,然後順着調用棧去找,可以查看到temp.c
並定位到拋出異常的位置:
可以看到,我們成功在 Chrome Devtools 裏面查看了 C 代碼,並且代碼停在了 abort()
處,同時還可以類似我們調試 JS 時一樣,查看當前 scope 下的值:
如上述可以查看 x
、y
值,將鼠標浮動到 x
上還可以顯示此時的值。
查看複雜類型值
實際上 Chrome Devtools 不僅可以查看原 C/C++ 代碼中一些變量的普通類型值,如數字、字符串,還可以查看更加複雜的結構,如結構體、數組、類等內容,我們拿另外一個例子來展現這個效果。
我們通過一個在 C++ 裏面繪製 曼德博圖形 的例子來展示上述的效果,同樣在 WebAssembly 目錄下創建 mandelbrot
文件夾,然後添加 `mandelbrot.cc
文件,並填入如下內容:
#include <SDL2/SDL.h>
#include <complex>
int main() {
// 初始化 SDL
int width = 600, height = 600;
SDL_Init(SDL_INIT_VIDEO);
SDL_Window* window;
SDL_Renderer* renderer;
SDL_CreateWindowAndRenderer(width, height, SDL_WINDOW_OPENGL, &window,
&renderer);
// 爲畫板填充隨機的顏色
enum { MAX_ITER_COUNT = 256 };
SDL_Color palette[MAX_ITER_COUNT];
srand(time(0));
for (int i = 0; i < MAX_ITER_COUNT; ++i) {
palette[i] = {
.r = (uint8_t)rand(),
.g = (uint8_t)rand(),
.b = (uint8_t)rand(),
.a = 255,
};
}
// 計算 曼德博 集合並繪製 曼德博 圖形
std::complex<double> center(0.5, 0.5);
double scale = 4.0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
std::complex<double> point((double)x / width, (double)y / height);
std::complex<double> c = (point - center) * scale;
std::complex<double> z(0, 0);
int i = 0;
for (; i < MAX_ITER_COUNT - 1; i++) {
z = z * z + c;
if (abs(z) > 2.0)
break;
}
SDL_Color color = palette[i];
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
SDL_RenderDrawPoint(renderer, x, y);
}
}
// 將我們在 canvas 繪製的內容渲染出來
SDL_RenderPresent(renderer);
// SDL_Quit();
}
上述代碼差不多 50 行左右,但是引用了兩個 C++ 標準庫:SDL 和 complex numbers ,這使得我們的代碼變得有一點複雜了,我們接下來編譯上述代碼,來看看 Chrome Devtools 的調試效果如何。
通過在編譯時帶上 -g
標籤,告訴 Emscripten 編譯器帶上調試信息,並尋求 Emscripten 在編譯時注入 SDL2 庫以及允許庫在運行時可以使用任意內存大小:
emcc -g mandelbrot.cc -o mandelbrot.html \
-s USE_SDL=2 \
-s ALLOW_MEMORY_GROWTH=1
同樣使用 npx serve .
命令開啓一個本地的 Web 服務器,然後訪問 http://localhost:5000/mandelb... 可以看到如下效果:
打開開發者工具,然後可以搜索到 mandelbrot.cc
文件,我們可以看到如下內容:
我們可以在第一個 for 循環裏面的 palette
賦值語句哪一行打一個斷點,然後重新刷新網頁,我們發現執行邏輯會暫停到我們的斷點處,通過查看右側的 Scope 面板,可以看到一些有意思的內容。
使用 Scope 面板
我們可以看到複雜類型如 center
、palette
,還可以展開它們,查看複雜類型裏面具體的值:
直接在程序中查看
同時將鼠標移動到 palette
等變量上面,同樣可以查看值的類型:
在控制檯中使用
同時在控制檯裏面也可以通過輸入變量名獲取到值,依然可以查看複雜類型:
還可以對複雜類型進行取值、計算相關的操作:
使用 watch 功能
我們也可以把使用調試面板裏面的 watch 功能,添加 for 循環裏面的 i 到 watch 列表,然後恢復程序執行就可以看到 i 的變化:
更加複雜的步進調試
我們同樣可以使用另外幾個調試工具:step over、step in、step out、step 等,如我們使用 step over,向後執行兩步:
可以查看到當前步的變量值,也可以在 Scope 面板中看到對應的值。
針對非源碼編譯的第三方庫進行調試
在之前我們只編譯了 mandelbrot.cc
文件,並在編譯時要求 Emscripten 爲我們提供內建的 SDL 相關的庫,由於 SDL 庫並不是我們從源碼編譯而來,所以不會帶上調試相關的信息,所以我們僅僅在 mandelbrot.cc
裏面可以通過查看 C++ 代碼的形式來調試,而對於 SDL 相關的內容則只能查看 WebAssembly 相關的代碼來進行調試。
如我們在 41 行,SDL_SetRenderDrawColor 調用處打上斷點,並使用 step in 進入到函數內部:
會變成如下的形式:
我們又回到了原始的 WebAssembly 的調試形式,這也是難以避免的一種情況,因爲我們在開發過程中可能會遇到各種第三方庫,但是我們並不能保證每個庫都能從源碼編譯而來且帶上了類似 DWARF 的調試信息,絕大部分情況下我們無法控制第三方庫的行爲;而另外一種情況則是有時我們會在生產情況下遇到問題,而生產環境也是沒有調試信息的。
上述情況暫時還沒有比較好的處理方法,但是開發者工具卻改進了上述的調試體驗,將所有的代碼都打包成單一的 WebAssembly 文件,對應到我們這次就是 mandelbrot.wasm
文件,這樣我們再也無需擔心其中的某段代碼到底來自那個源文件。
新的命名生成策略
之前的調試面板裏面,針對 WebAssembly 只有一些數字索引,而對於函數則連名字都沒有,如果沒有必要的類型信息,那麼很難追蹤到某個具體的值,因爲指針將以整數的形式展示出來,但你不知道這些整數背後存儲着什麼。
新的命名策略參考了其他反彙編工具的命名策略,使用了 WebAssembly 命名策略部分的內容、import/export 的路徑相關的內容,可以看到我們現在的調試面板中針對函數可以展示函數名相關的信息:
即使遇到了程序錯誤,基於語句的類型和索引也可以生成類似 $func123
這樣的名字,大大提高了棧追蹤和反彙編的體驗。
查看內存面板
如果想要調試此時程序佔用的內存相關的內容,可以在 WebAssembly 的上下文下,查看 Scope 面板裏的 Module.memories.$env.memory
,但是這隻能看到一些獨立的字節,無法瞭解到這些字節對應到的其他數據格式,如 ASCII 格式。但是 Chrome 開發者工具還爲我們提供了一些其他更加強大的內存查看形式,當我們右鍵點擊 env.memory
時,可以選擇 Reveal in Memory Inspector panel:
或者點擊 env.memory
旁邊的小圖標:
可以打開內存面板:
從內存面板裏面可以查看以十六進制或 ASCII 的形式查看 WebAssembly 的內存,導航到特定的內存地址,將特定數據解析成各種不同的格式,如十六進制 65 代表的 e 這個 ASCII 字符。
對 WebAssembly 代碼進行性能分析
因爲我們在編譯時爲代碼注入了很多調試信息,運行的代碼是未經優化且冗長的代碼,所以運行時會很慢,所以如果爲了評估程序運行的性能,你不能使用 performance.now
或者 console.time
等 API,因爲這些函數調用獲得的性能相關的數字通常不能反應真實世界的效果。
所以如果需要對代碼進行性能分析,你需要使用開發者工具提供的性能面板,性能面板裏面會全速運行代碼,並且提供不同函數執行時花費時間的明確斷點信息:
可以看到上述幾個比較典型的時間點如 161ms,或者 461ms 的 LCP 與 FCP ,這些都是能反應真實世界下的性能指標。
或者你可以在加載網頁時關閉控制檯,這樣就不會涉及到調試信息等相關內容的調用,可以確保比較真實的效果,等到頁面加載完成,然後再打開控制檯查看相關的指標信息。
在不同的機器上進行調試
當在 Docker、虛擬機或者其他原創服務器上進行構建時,你可能會遇到那種構建時使用的源文件路徑和本地文件系統上的文件路徑不一致,這會導致開發者工具在運行時可以在 Sources 面板裏展示出有這個文件,但是無法加載文件內容。
爲了解決這個問題,我們需要在之前安裝的 C/C++ Devtools Support 配置裏面設置路徑映射,點擊擴展的 “選項”:
然後添加路徑映射,在 old/path 裏填入之前的源文件構建時的路徑,在 new/path 裏填入現在存在本地文件系統上的文件路徑:
上述映射的功能和一些 C++ 的調試器如 GDB 的 set substitute-path
以及 LLDB 的 target.source-map
很像。這樣開發者工具在查找源文件時,會查看是否在配置的路徑映射裏有對應的映射,如果源路徑無法加載文件,那麼開發者工具會嘗試從映射路徑加載文件,否則會加載失敗。
調試優化性構建的代碼
如果你想調試一些在構建時進行優化後的代碼,可能會獲得不太理想的調試體驗,因爲進行優化構建時,函數內聯在一起,可能還會對代碼進行重排序或去除一部分無用的代碼,這些都可能會混淆調試者。
目前開發者工具除了對函數內聯時不能搞很好的支持外,能夠支持絕大部分優化後代碼的調試體驗,爲了減少函數內聯支持能力欠缺帶來的調試影響,建議在對代碼進行編譯時加入 -fno-inline
標誌來取消優化構建時(通常是帶上 -O
參數)對函數進行內聯處理的功能,未來開發者工具會修復這個問題。所以針對之前提到的簡單 C 程序的編譯腳本如下:
emcc -g temp.c -o temp.html \
-O3 -fno-inline
將調試信息單獨存儲
調試信息包含代碼的詳細信息,定義的類型、變量、函數、函數作用域、以及文件位置等任何有利於調試器使用的信息,所以通常調試信息比源代碼還要大。
爲了加速 WebAssembly 模塊的編譯和加載速度,你可以在編譯時將調試信息拆分成獨立的 WebAssembly 文件,然後單獨加載,爲了實現拆分單獨文件,可以在編譯時加入 -gseparate-dwarf
操作:
emcc -g temp.c -o temp.html \
-gseparate-dwarf=temp.debug.wasm
進行上述操作之後,編譯之後的主應用代碼只會存儲一個 temp.debug.wasm
的文件名,然後在代碼加載時,插件會定位到調試文件的位置並將其加載進開發者工具。
如果我們想同時進行優化構建,並將調試信息單獨拆分,並在之後需要調試時,加載本地的調試文件進行調試,在這種場景下,我們需要重載調試文件存儲的地址來幫助插件能夠找到這個文件,可以運行如下命令來處理:
emcc -g temp.c -o temp.html \
-O3 -fno-inline \
-gseparate-dwarf=temp.debug.wasm \
-s SEPARATE_DWARF_URL=file://[temp.debug.wasm 在本地文件系統的存儲地址]
在瀏覽器中調試 ffmpeg 代碼
通過這篇文章我們深入瞭解瞭如何在瀏覽器中調試通過 Emscripten 構建而來的 C/C++ 代碼,上述講解了一個普通無依賴的例子以及一個依賴於 C++ 標準庫 SDL 的例子,並且講解了現階段調試工具可以做的事情和限制,接下來我們就通過學到的知識來了解如何在瀏覽器中調試 ffmpeg 相關的代碼。
帶上調試信息的構建
我們只需要修改在之前的文章中提到的構建腳本 build-with-emcc.sh
,加入 -g
對應的標誌:
ROOT=$PWD
BUILD_DIR=$ROOT/build
cd ffmpeg-4.3.2-3
ARGS=(
-g # 在這裏添加,告訴編譯器需要添加調試
-I. -I./fftools -I$BUILD_DIR/include
-Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample -L$BUILD_DIR/lib
-Qunused-arguments
-o wasm/dist/ffmpeg-core.js fftools/ffmpeg_opt.c fftools/ffmpeg_filter.c fftools/ffmpeg_hw.c fftools/cmdutils.c fftools/ffmpeg.c
-lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lpostproc -lm -lx264 -pthread
-O3 # Optimize code with performance first
-s USE_SDL=2 # use SDL2
-s USE_PTHREADS=1 # enable pthreads support
-s PROXY_TO_PTHREAD=1 # detach main() from browser/UI main thread
-s INVOKE_RUN=0 # not to run the main() in the beginning
-s EXPORTED_FUNCTIONS="[_main, _proxy_main]" # export main and proxy_main funcs
-s EXTRA_EXPORTED_RUNTIME_METHODS="[FS, cwrap, setValue, writeAsciiToMemory]" # export preamble funcs
-s INITIAL_MEMORY=268435456 # 268435456 bytes = 268435456 MB
)
emcc "${ARGS[@]}"
cd -
然後以此執行其他操作,最後通過 node server.js
運行我們的腳本,然後打開 http://localhost:8080/ 查看效果如下:
可以看到,我們在 Sources 面板裏面可以搜索到構建後的 ffmpeg.c
文件,我們可以在 4865 行,在循環操作 nb_output
時打一個斷點:
然後在網頁中上傳一個 avi
格式的視頻,接着程序會暫停到斷點位置:
可以發現,我們依然可以像之前一樣在程序中鼠標移動上去查看變量值,以及在右側的 Scope 面板裏查看變量值,以及可以在控制檯中查看變量值。
類似的,我們也可以進行 step over、step in、step out、step 等複雜調試操作,或者 watch 某個變量值,或查看此時的內存等。
可以看到通過這篇文章介紹的知識,你可以在瀏覽器中對任意大小的 C/C++ 項目進行調試,並且可以使用目前開發者工具提供的絕大部分功能。
參考鏈接
- https://www.infoq.com/news/20...
- https://developer.chrome.com/...
- https://lucumr.pocoo.org/2020...
- https://v8.dev/docs/wasm-comp...
- Debugging WebAssembly with Chrome DevTools | by Charuka Herath | Bits and Pieces (bitsrc.io)")
- Making Web Assembly Even Faster: Debugging Web Assembly Performance with AssemblyScript and a Gameboy Emulator | by Aaron Turner | Medium
❤️/ 感謝支持 /
以上便是本次分享的全部內容,希望對你有所幫助^_^
喜歡的話別忘了 分享、點贊、收藏 三連哦~
歡迎關注公衆號 程序員巴士,來自字節、蝦皮、招銀的三端兄弟,分享編程經驗、技術乾貨與職業規劃,助你少走彎路進大廠。