最新のツールを使用した WebAssembly のデバッグ

Ingvar Stepanyan
Ingvar Stepanyan

これまでの道のり

1 年前、Chrome は Chrome DevTools でのネイティブ WebAssembly デバッグの初期サポートを発表しました。

基本的なステップ実行のサポートをデモし、今後、ソースマップではなく DWARF 情報を使用する機会について説明しました。

  • 変数名の解決
  • プリティ プリント タイプ
  • ソース言語での式の評価
  • ほか多数

本日は、約束した機能が実現したことと、Emscripten チームと Chrome DevTools チームが今年、特に C アプリと C++ アプリに対して進めた進歩についてお知らせします。

始める前に、これは新しいエクスペリエンスのベータ版であり、すべてのツールの最新バージョンは自己責任で使用する必要があることにご注意ください。問題が発生した場合は、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

これで、生成されたページを localhost HTTP サーバー(serve など)から提供し、最新の Chrome Canary で開くことができます。

今回は、Chrome DevTools と統合され、WebAssembly ファイルにエンコードされたすべてのデバッグ情報を解釈するヘルパー拡張機能も必要になります。インストールするには、goo.gle/wasm-debugging-extension のリンクにアクセスしてください。

また、DevTools の [Experiments] で WebAssembly デバッグを有効にする必要があります。Chrome DevTools を開き、DevTools ペインの右上にある歯車アイコン()をクリックして [試験運用版] パネルに移動し、[WebAssembly デバッグ: DWARF サポートを有効にする] をオンにします。

DevTools 設定の [テスト] ペイン

[設定] を閉じると、DevTools から設定を適用するために DevTools を再読み込みするよう提案されます。これで 1 回限りの設定は完了です。

これで、[ソース] パネルに戻り、[例外で停止](⏸ アイコン)を有効にして、[キャッチされた例外で停止] をオンにして、ページを再読み込みできます。DevTools が例外で一時停止しているはずです。

[ソース] パネルのスクリーンショット。[キャッチされた例外で一時停止] を有効にする方法を示しています。

デフォルトでは、Emscripten によって生成されたグルーコードで停止しますが、右側にはエラーのスタックトレースを示すコールスタック ビューが表示され、abort を呼び出した元の C 行に移動できます。

DevTools が assert_less 関数で一時停止し、スコープビューに x と y の値が表示されている

これで、[スコープ] ビューで、C/C++ コード内の変数の元の名前と値を確認できるようになりました。$localN などのマングリングされた名前の意味や、作成したソースコードとの関係を把握する必要がなくなりました。

これは、整数などのプリミティブ値だけでなく、構造体、クラス、配列などの複合型にも適用されます。

リッチタイプのサポート

より複雑な例を見てみましょう。今回は、次の C++ コードを使用して マンデルブロ フラクタルを描画します。

#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 行のコードを 1 つのファイルにまとめていますが、今回は外部 API(グラフィック用の SDL ライブラリや C++ 標準ライブラリの複素数など)も使用しています。

上記と同じ -g フラグを使用してコンパイルし、デバッグ情報を含めます。また、Emscripten に SDL2 ライブラリを提供し、任意サイズのメモリを許可するよう指示します。

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

生成されたページをブラウザで開くと、ランダムな色の美しいフラクタル形状が表示されます。

デモページ

DevTools を開くと、元の C++ ファイルが再び表示されます。しかし今回はコードにエラーはありません(ホッ)。代わりに、コードの先頭にブレークポイントを設定しましょう。

ページを再度読み込むと、デバッガは C++ ソース内で一時停止します。

DevTools が「SDL_Init」呼び出しで一時停止する

右側にはすべての変数が表示されていますが、現時点では widthheight のみが初期化されているため、検査する内容はあまりありません。

メインのマンデルブロット ループ内に別のブレークポイントを設定し、実行を再開して少し先に進みましょう。

ネストされたループ内で DevTools が一時停止する

この時点で、palette はランダムな色で塗りつぶされています。配列自体と個々の SDL_Color 構造の両方を展開し、コンポーネントを調べて、すべて正常に動作していることを確認できます(たとえば、「alpha」チャネルが常に完全な不透明度に設定されていることなど)。同様に、center 変数に格納されている複素数の実数部と虚数部を開いて確認できます。

スコープ ビューから移動するのが難しい深くネストされたプロパティにアクセスする場合は、コンソール評価を使用することもできます。ただし、より複雑な C++ 式はまだサポートされていません。

「palette[10].r」の結果を示すコンソール パネル

実行を数回再開すると、内部の x もどのように変化しているかを確認できます。スコープ ビューをもう一度確認するか、変数名をウォッチリストに追加してコンソールで評価するか、ソースコードで変数にカーソルを合わせます。

ソース内の変数「x」の上に表示されたツールチップ。値は「3」です。

ここから、C++ ステートメントにステップインまたはステップオーバーして、他の変数の変化も確認できます。

「color」、「point」などの変数の値を示すツールチップとスコープビュー

デバッグ情報が利用可能な場合は、すべて問題なく動作しますが、デバッグ オプションを使用してビルドされていないコードをデバッグする場合はどうすればよいでしょうか。

未加工の WebAssembly デバッグ

たとえば、ソースから自分でコンパイルするのではなく、Emscripten にビルド済みの SDL ライブラリを提供するように依頼したため、少なくとも現時点では、デバッガが関連するソースを見つける方法はありません。もう一度ステップインして SDL_RenderDrawColor に入りましょう。

「mandelbrot.wasm」の分解ビューを示す DevTools

元の WebAssembly デバッグ エクスペリエンスに戻ります。

少し怖いように見えますが、ほとんどのウェブ デベロッパーが対処する必要はありません。ただし、デバッグ情報なしでビルドされたライブラリをデバッグする必要がある場合があります。これは、制御できないサードパーティ ライブラリであるか、本番環境でのみ発生するバグに遭遇しているためです。

このようなケースに対応するため、基本的なデバッグ機能も改善されています。

まず、以前に元の WebAssembly デバッグを行ったことがある場合は、逆アセンブル全体が 1 つのファイルに表示されるようになりました。ソース エントリ wasm-53834e3e/ wasm-53834e3e-7 がどの関数に対応しているかを推測する必要がなくなりました。

新しい名前生成スキーム

また、分解ビューの名前も改善されました。以前は、数値インデックスのみが表示されていました。関数の場合は、名前は表示されませんでした。

現在、他の逆アセンブル ツールと同様に、WebAssembly の名前セクションのヒント、インポート/エクスポート パスを使用して名前を生成しています。それでも名前が生成されない場合は、$func123 などのアイテムのタイプとインデックスに基づいて名前を生成します。上記のスクリーンショットでは、これにより、読みやすくなるようにスタックトレースや逆アセンブルが少し改善されていることがわかります。

利用可能な型情報がない場合、プリミティブ以外の値を検査するのが難しい場合があります。たとえば、ポインタは通常の整数として表示され、メモリ内に何が格納されているかを知る方法はありません。

メモリ検査

これまで、個々のバイトを検索するには、[スコープ] ビューで env.memory で表される WebAssembly メモリ オブジェクトを展開するしかありませんでした。これは単純なシナリオでは機能しましたが、拡張するには特に便利ではなく、バイト値以外の形式でデータを再解釈できませんでした。これにも役立つ新機能として、リニアメモリ インスペクタが追加されました。

env.memory を右クリックすると、[メモリを検査] という新しいオプションが表示されます。

[スコープ] ペインで [env.memory] のコンテキスト メニューを開き、[メモリを検査] 項目が表示されている

クリックすると、メモリ インスペクタが表示されます。ここでは、WebAssembly メモリを 16 進数ビューと ASCII ビューで検査したり、特定のアドレスに移動したり、さまざまな形式でデータを解釈したりできます。

DevTools の Memory Inspector ペインに、メモリの 16 進数ビューと ASCII ビューが表示されている

高度なシナリオと注意事項

WebAssembly コードのプロファイリング

DevTools を開くと、デバッグを有効にするために、WebAssembly コードが最適化されていないバージョンに「階層化」されます。このバージョンは非常に遅いため、DevTools が開いている間は、console.timeperformance.now などの方法でコードの速度を測定することはできません。得られる数値は実際のパフォーマンスをまったく表さないためです。

代わりに、DevTools のパフォーマンス パネルを使用することをおすすめします。このパネルでは、コードが全速力で実行され、さまざまな関数で費やされた時間の詳細な内訳を確認できます。

さまざまな Wasm 関数を示すプロファイリング パネル

また、DevTools を閉じてアプリケーションを実行し、完了後に開いてコンソールを検査することもできます。

プロファイリング シナリオは今後改善される予定ですが、現時点では注意が必要です。WebAssembly の階層化シナリオの詳細については、WebAssembly コンパイル パイプラインのドキュメントをご覧ください。

異なるマシン(Docker / ホストを含む)でのビルドとデバッグ

Docker、仮想マシン、リモート ビルドサーバーでビルドする場合、ビルド中に使用されるソースファイルのパスが、Chrome DevTools が実行されている独自のファイルシステムのパスと一致しない場合があります。この場合、ファイルは [ソース] パネルに表示されますが、読み込まれません。

この問題を解決するため、C/C++ 拡張機能オプションにパス マッピング機能を実装しました。これを使用して任意のパスのリマッピングを行い、DevTools がソースを見つけられるようにすることができます。

たとえば、ホストマシン上のプロジェクトがパス C:\src\my_project にあるが、そのパスが /mnt/c/src/my_project として表される Docker コンテナ内でビルドされている場合、これらのパスを接頭辞として指定することで、デバッグ中に再マッピングできます。

C/C++ デバッグ拡張機能のオプション ページ

最初に一致したプレフィックスが「勝ち」ます。他の 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 を開いたときにヘルパー拡張機能がそのファイルを検出して読み込むことができます。

この機能は、上記のような最適化と組み合わせて、ほぼ最適化されたアプリケーションの本番環境ビルドを配布し、後でローカルサイドファイルでデバッグするためにも使用できます。この場合、拡張機能がサイドファイルを検出できるように、保存されている URL をオーバーライドする必要があります。次に例を示します。

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 DevTools は JavaScript だけでなく、C と C++ のアプリでも実用的で強力なデバッガになります。これにより、さまざまなテクノロジーで構築されたアプリを、共有のクロス プラットフォーム ウェブにこれまで以上に簡単に移行できるようになります。

ただし、この取り組みはまだ完了していません。今後取り組む予定の機能の一部をご紹介します。

  • デバッグ エクスペリエンスの粗さを解消。
  • カスタム型フォーマッタのサポートを追加しました。
  • WebAssembly アプリのプロファイリングの改善に取り組んでいます。
  • コード カバレッジのサポートを追加し、未使用のコードを簡単に見つけられるようにしました。
  • コンソール評価での式のサポートを改善しました。
  • サポートされている言語を追加しました。
  • …など

ご自身のコードで現在のベータ版をお試しいただき、問題が見つかった場合は https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350 までご報告ください。

プレビュー チャネルをダウンロードする

デフォルトの開発用ブラウザとして Chrome の CanaryDevBeta を使用することを検討してください。これらのプレビュー チャンネルでは、最新の DevTools 機能にアクセスしたり、最先端のウェブ プラットフォーム API をテストしたりできます。また、ユーザーよりも早くサイトの問題を見つけることもできます。

Chrome DevTools チームに問い合わせる

次のオプションを使用して、DevTools の新機能、アップデート、その他のトピックについて話し合います。