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

Ingvar Stepanyan
Ingvar Stepanyan

これまでの道のり

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

基本的なステップ サポートを紹介し、今後ソースマップが開かれる代わりに DWARF 情報が使用される機会について説明しました。

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

本日は、Emscripten チームと Chrome DevTools チームが今年行った、特に C / C++ アプリに関するこれまでの成果と、約束した機能が実現したことをご紹介します。

開始する前に、これはまだベータ版であるため、すべてのツールの最新バージョンを自己責任で使用してください。問題が発生した場合は、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 DevTools と統合され、WebAssembly ファイルにエンコードされたすべてのデバッグ情報を理解できるようにするヘルパー拡張機能も必要です。goo.gle/wasm-debugging-extension にアクセスしてインストールしてください。

また、DevTools の [Experiments] で WebAssembly デバッグを有効にすることをおすすめします。Chrome DevTools を開き、DevTools ペインの右上にある歯車アイコン()をクリックして [Experiments] パネルに移動し、[WebAssembly Debugging: Enable DWARF support] をオンにします。

DevTools 設定の [Experiments] ペイン

[Settings] を閉じると、DevTools は設定を再読み込みして設定を適用するように提案するので、それを行います。以上が 1 回限りの設定です

ここで、[Sources] パネルに戻り、[Pause onexceptions](⏸ アイコン)を有効にして、[Pause on cCatexception] をオンにして、ページを再読み込みできます。例外で DevTools が一時停止します。

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

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

DevTools が「assert_less」関数で一時停止し、[Scope] ビューに「x」と「y」の値が表示される

これで、[スコープ] ビューを見ると、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 行のコードを含む 1 つのファイルですが、今回はグラフィック用の SDL ライブラリや、C++ 標準ライブラリの複素数などの外部 API も使用しています。

上記と同じ -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 構造体を展開し、そのコンポーネントを検査して、すべてが適切に表示されていること(「アルファ」チャンネルが常に完全な不透明度に設定されていること)を確認できます。同様に、center 変数に格納された複素数の実数部と虚数部を展開してチェックできます。

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

「palette[10].r」の結果が表示されたコンソール パネル

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

値「3」が表示されているソース内の変数「x」のツールチップ

ここから、C++ ステートメントをステップインまたはステップオーバーし、他の変数の変化を観察できます。

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

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

Raw WebAssembly のデバッグ

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

「mandelbrot.wasm」の逆アセンブリ ビューを表示している DevTools

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

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

このような状況に対応するため、基本的なデバッグ エクスペリエンスも改善しました。

まず、未加工の WebAssembly デバッグを使用していた場合、Sources エントリ wasm-53834e3e/ wasm-53834e3e-7 がどの関数に対応するかを推測する必要がなくなり、逆アセンブリ全体が 1 つのファイルに表示されるようになりました。

新しい名前生成スキーム

逆アセンブリ ビューの名前も改善しました。以前は、数値インデックスしか表示されず、関数の場合は名前が表示されませんでした。

現在は、WebAssembly の名前セクションにあるインポート/エクスポート パスのヒントを使用して、他の逆アセンブリ ツールと同様の名前を生成しています。最後に、その他のすべてが失敗した場合は、$func123 などのアイテムの型とインデックスに基づいて名前を生成します。上のスクリーンショットでは、やや読みやすいスタック トレースと逆アセンブルの取得にすでに役立っています。

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

メモリ検査

以前は、[Scope] ビューで env.memory によって表される WebAssembly メモリ オブジェクトを展開して、個々のバイトを検索することしかできませんでした。これはいくつかの簡単なシナリオで機能しましたが、拡張が特に便利ではなく、バイト値以外の形式でデータを再解釈できませんでした。これを支援する新機能「線形メモリインスペクタ」も 追加されました

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

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

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

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

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

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://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue に報告してください。

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

Chrome CanaryDevBeta を既定の開発ブラウザとして使用することをご検討ください。これらのプレビュー チャンネルでは、最新の DevTools 機能にアクセスしたり、最先端のウェブ プラットフォーム API をテストしたり、ユーザーが実際に体験する前にサイト上の問題を検出したりできます。

Chrome DevTools チームへのお問い合わせ

投稿内の新機能や変更点、または DevTools に関するその他のことについて話し合うには、次のオプションを使用します。

  • crbug.com からご提案やフィードバックをお送りください。
  • DevTools の問題を報告するには、DevTools でその他のオプション アイコン その他   > [ヘルプ] > [DevTools の問題を報告する] を選択します。
  • @ChromeDevTools にツイートします。
  • 「DevTools の新機能」の YouTube 動画または DevTools のヒントの YouTube 動画でコメントを残してください。