本文へスキップ
AZ Tools

WebAssembly モジュールインスペクタ

WebAssembly のモジュールは、マジック \0asm とバージョンからなる 8 バイトのプリアンブルに続いて、1 バイトの ID と LEB128 の長さ、その長さぶんのバイト列からなるセクションが並ぶ構造です。このツールはその並びをブラウザの中でたどり、モジュールが自分について宣言している内容を示します。バージョン、ファイル順に並べた全セクションとそれぞれのバイトサイズ・オフセット・項目数、完全な型を伴うインポートとエクスポート、ページ数とバイト数の両方で示したメモリの上下限、start 関数、末尾のカスタムセクションです。 バイナリを開く理由はたいていサイズです。多くのビルドでは code セクションがファイルの大半を占めますが、常にそうとは限りません。Go のモジュールは data セクションだけで 1 MB を超えることがあり、アセットを埋め込んだ emscripten ビルドはデータのほうが大きく、デバッグビルドは実行に影響しないカスタムセクションに重さを隠しています。インポートはホストが満たすべき契約で、emscripten は env と wasi_snapshot_preview1 を、wasm-bindgen は生成したグルーコードのパスを、Go は gojs を要求します。ひとつでも欠けるとインスタンス化の時点で LinkError になります。 名前が落とされたバイナリでは、二つのカスタムセクションが何よりも多くを語ります。name セクションはモジュール名と関数名を取り戻し、producers セクションはどのコンパイラとツールがこのファイルを作ったかを記録します。どちらも存在すればここで復号します。 一方で、モジュールが何をするかはわかりません。code セクションはサイズを測るだけで逆アセンブルせず、何も実行しません。したがって関数本体の中だけで使われる機能、たとえば SIMD 演算やアトミック命令は、シグネチャやリミットのフラグに現れない限り表示されません。ここに並ぶ機能は、エンコーディングが証明できるものだけです。

使い方

  1. .wasm ファイルをボックスにドロップするか、クリックして選びます。ファイルはページ内で読まれ、アップロードされません。
  2. まずセクション表を見ます。各セクションのバイト数とファイル全体に占める割合が出るので、「なぜ 3 MB もあるのか」の答えになります。
  3. インポートはホストが満たすべき契約です。モジュール名、フィールド名、種類、そして期待されるシグネチャを確認します。
  4. エクスポートは JavaScript から呼べる API です。引数と戻り値の型はモジュールの type セクションからそのまま読み取っています。
  5. メモリ欄で初期ページ数と最大ページ数を確認し(1 ページは 65,536 バイト)、カスタムセクションに name や producers が残っているかを見ます。

よくある質問

.wasm がなぜこんなに大きいのですか。
まずセクション表を見てください。典型的な Rust や C のビルドでは code セクションがファイルの 70〜90% を占め、その場合の対処はコードそのものを減らすことです。ジェネリクスの実体化を減らす、パニックの整形を外す、wasm-opt -Oz をかける、といった手が効きます。ただし内訳が意外なことも多くあります。Go のビルドはランタイムの静的データを抱えるため data セクションが大きく、ファイルを埋め込んだ emscripten ビルドはデータが主体になり、DWARF のデバッグ情報を残したビルドは .debug_info などのカスタムセクションにその重さを持っています。最後のものは実行時にはまったく使われないので、wasm-strip で落とせます。
メモリが「17..∞ ページ」と出るのはどういう意味ですか。
WebAssembly のメモリは 65,536 バイトのページ単位で数えます。初期値 17 ページは、インスタンス化した瞬間に約 1.1 MB を確保するという意味です。二つ目の数字は宣言された最大値で、無い場合はホストが許すかぎり拡張できます。32 ビットの WebAssembly ではその上限が 65,536 ページ、つまり 4 GiB です。最大値が書いてあることは節約の約束ではなく、ホストがアドレス空間を先取りしてよい上限にすぎません。スレッドと共に使う共有メモリだけは、最大値が必須です。
インポートもエクスポートも a、b、c ばかりなのはなぜですか。
最適化と Closure を有効にした emscripten の出力です。インポート名とエクスポート名はバイナリ中の文字列なので、短くすればファイルが縮み、一緒に配られる JavaScript のグルーコードも同じ名前に短縮されます。実行には支障ありませんが、バイナリ単体では読めません。関数インデックスを元の名前へ戻す name カスタムセクションも同じ工程で削られます。残っていればこのツールが復号し、無ければ producers セクションが誰が作ったかを示す最後の手がかりになることが多いです。
env や wasi_snapshot_preview1 をインポートしています。何を渡せばよいのですか。
インポートの各項目は、ホストオブジェクトがモジュール名とフィールド名で埋める場所で、インスタンス化の際にエンジンが型まで検査します。env は emscripten が JavaScript 側に期待する C ランタイムのコールバックをまとめた名前空間、wasi_snapshot_preview1 は WASI 向けにビルドされたという意味で、ブラウザでは WASI のシムが要ります。gojs は Go ランタイムの橋渡し、./something_bg.js のようなパスは wasm-bindgen が生成したグルーモジュールの待ち受けです。ひとつでも欠けたり引数の数が違えば、WebAssembly.instantiate がその名前を挙げて LinkError を投げます。
モジュールを実行したり逆アセンブルしたりしますか。
どちらもしません。セクション、型、インポート、エクスポート、リミット、start 関数の索引、カスタムセクションといった宣言だけを解析します。関数本体は数と合計サイズを測るだけで命令は復号せず、エンジンも作らないので、悪意あるモジュールでもここでは何も起きません。その代わり挙動は見えません。どの関数が何を計算するのか、どのインポートが実際に呼ばれるのか、実行時にメモリをどれだけ使うのか、型に現れない SIMD やアトミック命令が本体にあるのかは分かりません。
コードを読まないのに、参照型やマルチバリューを使っていると言えるのはなぜですか。
MVP 以降の提案の一部は、命令だけでなくエンコーディングの形そのものを変えるからです。戻り値が二つ以上ある関数型はマルチバリュー、シグネチャの externref や二つ目のテーブルは参照型、メモリのリミットのフラグの三番目のビットはスレッドに必要な共有ビットです。data count セクションやアクティブでないパッシブなセグメントはバルクメモリでのみ現れ、シグネチャの v128 は SIMD です。バイトが証明するものだけを示し、関数本体の命令としてしか現れない機能は主張しません。そのため、この一覧は実際に使われている機能より短いことがあります。

関連ツール