大廠程式設計師的除錯技巧,偷學到了!

程式設計師巴士發表於2021-10-27

最近在研究 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+ 版本。

為了檢視除錯效果,需要設定一些內容。

  1. 開啟 Chrome Devtools 裡面的 WebAssembly 除錯選項

設定完之後,在工具欄頂部會出現一個 Reload 的藍色按鈕,需要重新載入配置,點選一下就好。

  1. 設定除錯選項,在遇到異常的地方暫停

  1. 重新整理瀏覽器,然後你會發現斷點停在了 temp.js ,由 Emscripten 編譯生成的 JS 膠水程式碼,然後順著呼叫棧去找,可以檢視到 temp.c 並定位到丟擲異常的位置:

可以看到,我們成功在 Chrome Devtools 裡面檢視了 C 程式碼,並且程式碼停在了 abort() 處,同時還可以類似我們除錯 JS 時一樣,檢視當前 scope 下的值:

如上述可以檢視 xy 值,將滑鼠浮動到 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++ 標準庫:SDLcomplex 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 皮膚

我們可以看到複雜型別如 centerpalette ,還可以展開它們,檢視複雜型別裡面具體的值:

直接在程式中檢視

同時將滑鼠移動到 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++ 專案進行除錯,並且可以使用目前開發者工具提供的絕大部分功能。

參考連結

❤️/ 感謝支援 /

以上便是本次分享的全部內容,希望對你有所幫助^_^

喜歡的話別忘了 分享、點贊、收藏 三連哦~

歡迎關注公眾號 程式設計師巴士,來自位元組、蝦皮、招銀的三端兄弟,分享程式設計經驗、技術乾貨與職業規劃,助你少走彎路進大廠。

相關文章