使用新型工具對 WebAssembly 進行偵錯

Ingvar Stepanyan
Ingvar Stepanyan

目前的路線

一年前,Chrome 宣布初步支援 Chrome 開發人員工具中的原生 WebAssembly 偵錯功能。

我們示範了基本逐步執行支援功能,並討論了未來使用 DWARF 資訊而非原始對應圖的機會:

  • 解析變數名稱
  • 美化排版類型
  • 評估原始語言中的運算式
  • ...還有更多!

今天,我們很高興展示承諾的功能已實現,以及 Emscripten 和 Chrome 開發人員工具團隊今年以來的進展,特別是針對 C 和 C++ 應用程式。

在我們開始之前,請注意這仍是新體驗的 Beta 版,您必須自行承擔使用所有工具最新版本的風險,如果遇到任何問題,請前往 https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350 回報。

我們先從上次相同的簡易 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);
}

如要編譯,我們會使用最新的 Emscripten,並傳遞 -g 旗標,就像在原始文章中一樣,納入偵錯資訊:

emcc -g temp.c -o temp.html

我們現在可以透過本機 HTTP 伺服器 (例如使用 serve) 提供產生的網頁,並在最新的 Chrome Canary 中開啟該網頁。

這次我們也需要一個輔助擴充功能,整合 Chrome 開發人員工具,並協助解讀 WebAssembly 檔案中編碼的所有偵錯資訊。請按一下這個連結安裝:goo.gle/wasm-debugging-extension

建議您一併在開發人員工具的「實驗」中啟用 WebAssembly 偵錯功能。開啟 Chrome 開發人員工具,按一下開發人員工具窗格右上角的齒輪 () 圖示,前往「Experiments」面板,勾選「WebAssembly Debugging: Enable DWARF support」

開發人員工具設定的「實驗」窗格

關閉「設定」後,開發人員工具會建議重新載入,以便套用設定,因此我們就這麼做吧。以上就是一次性設定的內容。

接下來,我們可以返回「來源」面板,啟用「遇到例外狀況時暫停」 (⏸ 圖示),然後勾選「遇到已偵測到的例外狀況時暫停」,並重新載入網頁。您應該會看到開發人員工具在例外狀況時暫停:

「來源」面板的螢幕截圖,說明如何啟用「在偵測到例外狀況時暫停」

根據預設,它會在 Emscripten 產生的黏合程式碼上停止,但您可以在右側看到代表錯誤堆疊追蹤的「Call Stack」檢視畫面,並可前往叫用 abort 的原始 C 行:

開發人員工具在 `assert_less` 函式中暫停,並在「範圍」檢視畫面中顯示 `x` 和 `y` 的值

現在,您查看「Scope」檢視畫面時,可在 C/C++ 程式碼中查看變數的原始名稱和值,不再需要找出例如 $localN 這類破壞的名稱是什麼意思,以及這些名稱與您編寫的原始碼有何關聯。

這不僅適用於整數等原始值,也適用於結構體、類別、陣列等複合型別!

豐富類型支援

我們來看看更複雜的範例,這次我們將使用以下 C++ 程式碼繪製 Mandelbrot 分形

#include <SDL2/SDL.h>
#include <complex>

int main() {
  // Init 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);

  // Generate a palette with random colors.
  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,
    };
  }

  // Calculate and draw the Mandelbrot set.
  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);
    }
  }

  // Render everything we've drawn to the canvas.
  SDL_RenderPresent(renderer);

  // SDL_Quit();
}

您可以看到這個應用程式仍相當小,只有一個檔案,內含 50 行程式碼,但這次我也會使用一些外部 API,例如用於繪圖的 SDL 程式庫,以及 C++ 標準程式庫的複數

我會使用上述的 -g 旗標編譯程式碼,以便納入偵錯資訊。此外,我會要求 Emscripten 提供 SDL2 程式庫,並允許使用任意大小的記憶體:

emcc -g mandelbrot.cc -o mandelbrot.html \
     -s USE_SDL=2 \
     -s ALLOW_MEMORY_GROWTH=1

當我在瀏覽器中瀏覽產生的網頁時,我可以看到美麗的碎形圖形,以及一些隨機顏色:

示範頁面

再次開啟開發人員工具時,可以看到原始 C++ 檔案。不過,這次程式碼中沒有錯誤 (呼!),因此我們改為在程式碼開頭設定一些中斷點。

當我們重新載入網頁時,偵錯工具會直接在 C++ 原始碼中暫停:

開發人員工具在 `SDL_Init` 呼叫時暫停

我們已經在右側看到所有變數,但目前只有 widthheight 已完成初始化,因此沒有太多內容可檢查。

讓我們在主要曼德勃羅圖迴圈中設定另一個中斷點,並繼續執行,略過一點點。

已在巢狀迴圈中暫停開發人員工具

此時,我們的 palette 已填入一些隨機顏色,我們可以展開陣列本身和個別 SDL_Color 結構,並檢查其元件,確認一切正常 (例如,「alpha」管道一律設為完全不透明)。同樣地,我們可以展開並檢查儲存在 center 變數中的複數的實部和虛部。

如果您想存取深層巢狀資源,但透過範圍檢視畫面很難導覽,也可以使用控制台評估功能!不過,請注意,系統目前不支援較複雜的 C++ 運算式。

主控台面板顯示 `palette[10].r` 的結果

我們可以暫停執行幾次,看看內部 x 的變化情形,方法是再次查看「範圍」檢視畫面、將變數名稱加入監控清單、在控制台中評估,或是將滑鼠游標懸停在原始程式碼中的變數上:

來源中變數 `x` 的工具提示,顯示其值為 `3`

從這裡,我們可以逐步執行或略過 C++ 陳述式,並觀察其他變數的變化情形:

工具提示和範圍檢視畫面,顯示「color」、「point」和其他變數的值

好,如果有偵錯資訊,這一切都會運作良好,但如果我們想偵錯未使用偵錯選項建構的程式碼,該怎麼辦呢?

原始 WebAssembly 偵錯

舉例來說,我們要求 Emscripten 為我們提供預先建構的 SDL 程式庫,而不是自行從來源編譯,因此目前至少無法讓偵錯工具找到相關聯的來源。讓我們再次介紹 SDL_RenderDrawColor

開發人員工具顯示 `mandelbrot.wasm` 的解組畫面

我們將回到原始的 WebAssembly 偵錯體驗。

雖然這看起來有點嚇人,而且大多數網頁開發人員都不會遇到這種情況,但有時您可能需要對沒有偵錯資訊的建構程式庫進行偵錯,可能是因為這是您無法控制的 3rd 方程式庫,或是您遇到只會在實際環境中發生的錯誤。

為協助處理這些情況,我們也對基本偵錯體驗進行了一些改善。

首先,如果您之前曾使用原始 WebAssembly 偵錯功能,您可能會發現整個反組譯作業現在會顯示在單一檔案中,不再需要猜測 Sources 項目 wasm-53834e3e/ wasm-53834e3e-7 可能對應的函式。

新建名稱產生配置

我們也改善了反組譯檢視畫面中的名稱。先前您只會看到數字索引,或是在函式中完全沒有名稱。

我們現在會使用 WebAssembly 名稱部分的提示、匯入/匯出路徑,以及在所有其他方法都失敗時,根據 $func123 等項目的類型和索引來產生名稱,以便產生名稱,這與其他解組工具類似。您可以看到,在上述螢幕截圖中,這項功能已可協助您取得較易讀的堆疊追蹤和反組譯。

當沒有可用的類型資訊時,您可能很難檢查基本類型以外的任何值。舉例來說,指標會顯示為一般整數,而您無法得知記憶體中儲存了什麼。

記憶體檢查

先前您只能展開 WebAssembly 記憶體物件 (在「Scope」檢視畫面中以 env.memory 表示) 來查詢個別位元組。這在一些小情況下可以使用,但擴充不是特別方便,而且不允許以位元組值以外的格式重新解譯資料。我們也新增一項新功能,協助您進行線性記憶體檢查器。

如果您在 env.memory 上按一下滑鼠右鍵,現在應該會看到名為「Inspect memory」的新選項:

範圍窗格中 `env.memory` 的內容選單,顯示「檢查記憶體」項目

點選後,系統會顯示記憶體檢查器,您可以在其中以十六進制和 ASCII 檢視方式檢查 WebAssembly 記憶體、前往特定位址,以及解讀不同格式的資料:

開發人員工具中的「記憶體檢查工具」窗格,顯示記憶體的十六進位和 ASCII 檢視畫面

進階情況和注意事項

剖析 WebAssembly 程式碼

開啟開發人員工具時,WebAssembly 程式碼會「降級」至未最佳化的版本,以便進行偵錯。這個版本的執行速度慢很多,也就是說,在開發人員工具開啟時,您無法仰賴 console.timeperformance.now 及其他評估程式碼速度的方法,因為您取得的數字完全並不代表實際的效能。

請改用開發人員工具效能面板,以便完整速度執行程式碼,並提供不同函式所花費時間的詳細說明:

顯示各種 Wasm 函式的剖析面板

或者,您也可以在關閉開發人員工具的情況下執行應用程式,並在完成後開啟開發人員工具,以便檢查控制台

我們會在日後改善剖析情境,但目前這項功能仍有待改進。如要進一步瞭解 WebAssembly 分層情境,請參閱 WebAssembly 編譯管道的說明文件。

在不同機器 (包括 Docker / 主機) 上建構及偵錯

在 Docker、虛擬機器或遠端建構伺服器中建構時,您可能會遇到以下情況:建構期間使用的來源檔案路徑,與 Chrome 開發人員工具執行時所用的檔案系統路徑不符。在這種情況下,檔案會顯示在「Sources」面板中,但無法載入。

為修正這個問題,我們已在 C/C++ 擴充功能選項中實作路徑對應功能。您可以使用它重新對應任意路徑,並協助開發人員工具找出來源。

舉例來說,如果主機電腦上的專案位於路徑 C:\src\my_project 下方,但在 Docker 容器中建構時,該路徑會以 /mnt/c/src/my_project 表示,您可以在偵錯期間將其重新對應,方法是將這些路徑指定為前置字串:

C/C++ 偵錯擴充功能的「Options」頁面

第一個相符的前置字元「wins」。如果您熟悉其他 C++ 偵錯工具,這個選項就類似於 GDB 中的 set substitute-path 指令,或 LLDB 中的 target.source-map 設定。

對最佳化版本進行偵錯

與其他語言一樣,最佳化功能停用時,偵錯功能的運作效果最佳。最佳化程序可能會將函式內嵌至另一個函式、重新排序程式碼,或完全移除部分程式碼,而這一切都可能會讓偵錯工具和使用者感到困惑。

如果您不介意較受限的偵錯體驗,但仍想對經過最佳化的版本進行偵錯,那麼除了函式內嵌功能外,大多數最佳化功能都會正常運作。我們預計日後會解決剩餘的問題,但目前請在使用任何 -O 級別最佳化功能編譯時,使用 -fno-inline 停用該功能,例如:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline

分隔偵錯資訊

偵錯資訊會保留許多程式碼詳細資料、定義的類型、變數、函式、範圍和位置,也就是任何對偵錯工具有用的資訊。因此,通常比程式碼本身更大。

為加快 WebAssembly 模組的載入和編譯作業,您可能會想要將這些偵錯資訊拆分為個別的 WebAssembly 檔案。如要在 Emscripten 中執行此操作,請傳送包含所需檔案名稱的 -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://[local path to temp.debug.wasm]

待續…

太好了,那是很多新功能的誕生!

藉由上述這些新的整合項目,Chrome 開發人員工具不僅會變成 JavaScript 的可行性、功能強大,也適用於 C 和 C++ 應用程式,讓開發人員可以更輕鬆地使用以各種技術建構的應用程式,並提供給共用的跨平台網路。

不過,我們的旅程尚未結束。我們接下來會著手處理以下事項:

  • 清除偵錯過程中的粗糙邊緣。
  • 新增對自訂類型格式設定工具的支援。
  • 改善 WebAssembly 應用程式的剖析功能。
  • 新增程式碼涵蓋率支援功能,方便找出未使用的程式碼。
  • 改善對主控台評估作業中運算式的支援功能。
  • 新增支援更多語言。
  • …還有更多!

同時,請在自己的程式碼上試用目前的 Beta 版,並將任何發現的問題回報至 https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350

下載預覽管道

建議您將 Chrome Canary開發人員版Beta 版設為預設開發人員版瀏覽器。這些預覽管道可讓您存取最新的 DevTools 功能,測試最新的網路平台 API,並在使用者發現問題前,協助您找出網站的問題!

與 Chrome 開發人員工具團隊聯絡

請使用下列選項討論新功能、更新或任何與開發人員工具相關的內容。