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

Ingvar Stepanyan
Ingvar Stepanyan

目前為止的道路

一年前,Chrome 宣布在 Chrome 開發人員工具中支援原生 WebAssembly 偵錯

我們示範了基本步驟支援服務,並談到未來是否會開放使用來源地圖,而是使用 DWARF 資訊 (而非來源地圖):

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

我們今天很高興向大家展示幾項預期的功能,以及 Emscripten 和 Chrome 開發人員工具團隊今年的進展,特別是 C 和 C++ 應用程式的進展。

在我們開始之前,提醒您,此新版本仍是新版服務的 Beta 版,請自行斟酌使用最新版所有工具,並自行承擔風險。如果遇到任何問題,請向 https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue 回報問題。

讓我們先從上次的相同 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

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

這次我們也會需要與 Chrome 開發人員工具整合的輔助擴充功能,可協助您瞭解 WebAssembly 檔案編碼的所有偵錯資訊。請前往以下連結進行安裝:goo.gle/wasm-debugging-extension

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

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

關閉「Settings」後,開發人員工具就會建議自行重新載入以套用設定,讓我們開始吧!以上就是一次性設定

現在我們可以返回「Sources」面板,啟用「Pause onexceptions」 (⏸圖示),勾選「Pause on saughtException」,然後重新載入頁面。開發人員工具應在例外狀況時暫停:

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

根據預設,此項目會在 Emscripten 產生的 glue 程式碼上停止,但畫面右側會顯示代表錯誤的堆疊追蹤的「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 初始化,所以沒有太多需要檢查。

讓我們在主要的 Mandelbrot 迴圈中設定另一個中斷點,然後繼續執行以跳過一點。

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

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

如要存取難以透過「範圍」檢視畫面存取的深巢狀屬性,您也可以使用主控台評估!不過請注意,目前不支援較複雜的 C++ 運算式。

顯示「palette[10].r」結果的控制檯面板

讓我們恢復執行數次,觀察內部 x 的變化,方法如下:再次查看「Scope」(範圍) 檢視畫面、將變數名稱新增至觀察清單、在主控台中評估,或是將滑鼠遊標懸停在原始碼中的變數上:

來源中變數「x」的工具提示,顯示值「3」

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

顯示「顏色」、「點」和其他變數值的工具提示與範圍檢視畫面

好的,當有偵錯資訊可用時,這一切就非常實用,但如果要對非使用偵錯選項建構的程式碼進行偵錯,該怎麼辦?

原始 WebAssembly 偵錯

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

開發人員工具顯示「mandelbrot.wasm」的反組譯碼

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

現在,看起來有點可怕,而且大多數網頁開發人員都不需要處理,但有時您可能要對建構的程式庫進行偵錯,無論該程式庫是您無法控管的「第三方」程式庫,或是您正在執行僅在實際工作環境會發生的其中一個錯誤。

為了因應這類情況,我們也對基本偵錯體驗做了一些改善。

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

全新名稱產生配置

另外,我們也改善了反組譯碼檢視畫面中的名稱。您之前看到的是數字索引;如果使用的是函式,則不會顯示名稱。

我們現在使用 WebAssembly 名稱區段、匯入/匯出路徑等提示,產生與其他反組工具的名稱,最後如果一切失敗,則根據項目的類型和索引 (如 $func123) 產生這些名稱。您可以從上方的螢幕截圖瞭解,這種做法已有助於取得更易讀的堆疊追蹤及拆解。

如果沒有可用的類型資訊,可能就很難檢查原始值以外的任何值,例如指標會顯示為一般整數,因為無法得知這些值儲存在記憶體背後的內容。

記憶體檢查

先前,您只能展開 Scope 檢視畫面中的 env.memory 表示 WebAssembly 記憶體物件,以查詢個別位元組。這種做法可以在一些小的情況下發揮,但不太方便展開,也不允許以位元組值以外的格式重新解讀資料。我們還新增了一些新功能:線性記憶體檢查器。

env.memory 上按一下滑鼠右鍵,應該會看到名為「Inspect memory」(檢查記憶體) 的新選項:

「Scope」窗格中「env.memory」的內容選單,顯示「Inspect Memory」項目

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

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

進階情境和注意事項

剖析 WebAssembly 程式碼

開啟 DevTools 後,WebAssembly 程式碼會「分層」為未最佳化的版本,以便啟用偵錯功能。這個版本的速度明顯變慢,也就是說,您無法在開發人員工具開啟時,仰賴 console.timeperformance.now 和其他測量程式碼速度的方法,因為您取得的數據並不代表實際效能。

請改用開發人員工具的效能面板,以完整速度執行程式碼,並提供不同功能使用時間的詳細資料:

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

您也可以在 DevTools 關閉的情況下執行應用程式,並在完成後開啟應用程式來檢查控制台

我們會在日後改善剖析情境,但目前請多加留意。如要進一步瞭解 WebAssembly 分層情境,請參閱 WebAssembly 編譯管道的說明文件。

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

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

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

舉例來說,如果主體機器上的專案位於路徑 C:\src\my_project 下,但建立在 Docker 容器內該路徑顯示為 /mnt/c/src/my_project 的 Docker 容器內,則您可以在偵錯期間將這些路徑指定為前置字串,以重新對應其:

C/C++ 偵錯擴充功能的選項頁面

第一個相符的前置字串「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,而當您開啟 DevTools 時,輔助程式擴充功能將可找到並載入該檔案。

與上述最佳化功能搭配使用時,這項功能甚至可用於發布幾乎最佳化的應用程式正式版本,之後再使用本機端檔案對這些版本進行偵錯。在此情況下,我們還需要覆寫儲存的網址,協助擴充功能尋找側邊檔案,例如:

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://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue

下載預覽管道

考慮使用 Chrome Canary 版開發人員版Beta 版做為預設開發瀏覽器。這些預覽管道可讓您存取開發人員工具的最新功能、測試最先進的網路平台 API,並在使用者使用之前就在網站上發現問題!

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

使用下列選項,討論文章的新功能和異動,以及其他與開發人員工具相關的事項。

  • 請透過 crbug.com 提交建議或意見回饋。
  • 如要回報開發人員工具的問題,請在開發人員工具中依序點選「更多選項」圖示 更多   >「說明」 >「回報開發人員工具的問題」
  • @ChromeDevTools 張貼推文。
  • 歡迎對開發人員工具的 YouTube 影片或開發人員工具秘訣 (YouTube 影片) 提供意見。