kateinoigakukunのブログ

思考垂れ流しObserver

WebAssembly向けのデバッガを開発しました

こんにちは。私はSwift言語のWebAssembly向けコンパイルをサポートするプロジェクトを進めている一人です。

その開発を効率的に進めていくためにWebAssembly向けのデバッガwasminspectを開発しました。

Swift for WebAssemblyの進捗

この間フォーラムに詳しい進捗報告をしました。 概要としては

  • 約90%のテストケースが通るようになった
  • まだ一部の実行時の言語仕様が動かない
    • 実行時キャストなど

という感じです。

これをデバッグする必要が出てきたこと、既存のデバッグ手法では太刀打ちできないことから今回のデバッガを開発することにしました。

既存のデバッグ手法

既存のデバッグ手法としては以下のようなものが挙げられます。

  1. WebAssembly以外のアーキテクチャデバッグ
  2. printデバッグ
  3. Chrome DevTools
  4. LLDB + wasmtime

WebAssembly以外のアーキテクチャデバッグ

大抵の場合、自分が元の言語で書いたロジックが間違っているので、わざわざWebAssembly向けバイナリでデバッグする必要はありません。RustでWebAssembly開発をしている場合でもRustをx86_64向けにコンパイルしてLLDBでデバッグすればよいのです。

これは手慣れた手法でデバッグでき、LLDBやGDBなどデバッグする環境が十分に整っていることが大きなメリットです。

一方、実際に動かすバイナリをデバッグできるわけではないので、コンパイラが吐くコードが信用できないときには使えません。あくまでデバッグできるのは手元のマシンのアーキテクチャ向けに吐かれたバイナリであり、WebAssemblyバイナリではありません。今回はWebAssembly向けコンパイルのパイプラインをデバッグしたかったため、この手法は不採用でした。

printデバッグ

古典的なデバッグ手法ですが、デバッガが無い環境では非常に有用な手法です。printデバッグの最大のメリットはWebAssemblyバイナリ自体をデバッグできることです。

しかし、printのためのコードが入り込むため、結局デバッグ対象のバイナリ自身をデバッグできるわけではないです。デバッグ用のコードを入れたらスタックが破壊される、ということがザラに起きるのでこれは致命的です。

また、あくまで中間状態をロギングできるだけなので、プロセスの制御ができません。例えばブレークポイントを貼って任意のタイミングでプロセスを止めたり、ステップ実行しながら条件分岐の進み方を確認することはできません。

Chrome DevTools

最新のChromeではDevToolsのWebAssembly対応が進んできています。最近はDWARFの解釈もできるようになったようです。実際にブラウザ上でデバッグできるのは非常に大きなメリットです。また将来的にWebAssemblyをWeb開発で普及させていくためには必須の機能だと思います。

ただ、現状フレーム上の変数やメモリ空間をダンプできなかったり、機能不足感が否めないです。

OSSとして開発されているので開発に参加することも考えましたが、プロジェクトが非常に大きいこと、v8とのインテグレーション作業に時間がかかりそうなことから、見送りました。

developers.google.com

LLDB + wasmtime

wasmtimeはMozillaが開発しているWebAssemblyのJITコンパイラです。

github.com

LLDBやGDBにはJITコンパイルされたネイティブコードとデバッグ情報からデバッグする機能があります。

Debugging JIT-ed Code With GDB — LLVM 10 documentation

wasmtimeはWASMに埋め込まれたDWARFをJITコンパイル後のアーキテクチャ向けに変換して、LLDBと通信することでソースレベルデバッグを実現しています。

実際に自分でデバッガを作り始めるまではこの手法でデバッグしていました。 ソースレベルのステップ実行やフレームの変数ダンプなどはそこそこ動いていました。 何よりもLLDBの基盤に乗ることで、既存のデバッガ資産をそのまま使えるというのが大きいです。

しかし、この手法でデバッグできるのはあくまでJITコンパイルされたネイティブコードであり、WebAssemblyではありません。なのでWebAssembly命令レベルでのステップ実行などはできないです。また、WebAssemblyをネイティブコードに変換する際に一部の情報が落ちてしまう問題もありました。

例えばWebAssemblyには関数呼び出し時にシグネチャの一致をチェックして、一致しない場合trapする仕様があります。デバッグの必要がない場合、不一致の時abortするだけで十分ですが、デバッグ時には実際にどの型を比較したのか、などの追加の情報が欲しいです。これを実現するためにはデバッグ時のみネイティブコードにデバッグ情報を乗せる、などの作業が必要になります。しかし、それぞれの例外をハンドリングするためにデバッグ情報を伝播するのは非常に大変でした。

hacks.mozilla.org

本当に欲しかったデバッガの要件

コンパイラ開発において本当に欲しかったデバッガの要件は以下のとおりです。

  • 実行時にどのwasm命令を実行しているか把握できる
  • プロセスの制御ができる
  • メモリダンプ
  • DWARFを解釈できる

しかしこれを満たすようなデバッガが見つからなかったため、自作する結論に至りました。

wasminspect

https://raw.githubusercontent.com/kateinoigakukun/wasminspect/master/assets/demo.gif

github.com

GIFでだいたいの雰囲気は伝わると思います。

冬休みの一週間強でデバッガ用のWebAssemblyランタイムを書いて、春休みの最初の一週間でデバッガフロントエンドを書いて、やっとまともに使えるようになりました。今ではバリバリコンパイラ開発で活躍しています。

実装

基本的にLLDBのコードを参考にしながら実装しました。大きな違いはLLDBがOSのシステムコールを使ってプロセスを制御するのに対して、wasminspectでは内蔵したランタイムを直接制御する部分です。

ランタイムはJITコンパイルもAOTコンパイルもせず、愚直にインタプリタとして実装しました。これはWebAssembly命令レベルでのステップ実行を実現するためです。また、wasmtimeとLLDBの組み合わせのときに発生したデバッグ情報の伝播も必要なくなりました。

ランタイムの開発は公式のドキュメントテストケースが充実していて非常にスムーズに進みました。

DWARF for WebAssembly

正直DWARF対応だけで何本かブログが書けそうなくらいしんどかったです。基本的に仕様を読みながらLLDBのコードを参考にして実装するだけでしたが、単純に量が多かったです。

dwarfstd.org

WebAssemblyに固有の話としてはDWARF上のスタックとLLVMが吐くWebAssemblyのスタックが一致しない、ということがありました。 LLVMが吐くWebAssemblyでは線形メモリ上に擬似スタックのような空間を確保してそこを伸び縮みさせてフレーム変数を管理しています。(WebAssemblyの仕様で定義されているスタックとは別) rspに対応する値がグローバルに保持されていて、x86_64向けの命令のようにプロローグとエピローグが擬似スタック上で実行されます。

DWARFが示すスタックはこの擬似スタックにほぼ対応しているので、LLVMの実装に依存することでローカル変数のダンプができるようになりました。

github.com

将来的にはWebAssembly向けの位置情報がDWARFの仕様に追加されるんじゃないかなぁと予想しています。既にドラフトの文書があったりします。

yurydelendik.github.io

自作デバッガの功績

今回作ったデバッガは既にいくつかのSwiftのバグを修正するのに大きく貢献しています。特にメモリダンプ機能は非常に便利で、正常に動くx86_64向けバイナリと比較することで破壊されたメモリの位置を特定できます。

まとめ

一応一通り動くデバッガができましたが、基本的に僕自身がSwift for WASMの開発をするための道具として開発しているだけなので、あまり他の環境で使われることを考慮していません。試していないですが、多分EmscriptenではDWARFの解釈がうまく行かないと思います。

ドキュメントを軽く書いたのでもし興味があれば触ってみてください。

0からデバッガを作る機会はなかなか無いので楽しかったです。これからも「無いなら作ればいい」の精神でやっていきたいと思います。