WebAssembly向けのデバッガを開発しました
こんにちは。私はSwift言語のWebAssembly向けコンパイルをサポートするプロジェクトを進めている一人です。
その開発を効率的に進めていくためにWebAssembly向けのデバッガwasminspectを開発しました。
Swift for WebAssemblyの進捗
この間フォーラムに詳しい進捗報告をしました。 概要としては
- 約90%のテストケースが通るようになった
- まだ一部の実行時の言語仕様が動かない
- 実行時キャストなど
という感じです。
Our co-maintainer @kateinoigakukun has posted a detailed update on the current status of SwiftWasm and remaining issues that are being worked on or require contributions from the community (please do get involved!):https://t.co/LHMAXy3hwv
— SwiftWasm (@swiftwasm) 2020年2月18日
これをデバッグする必要が出てきたこと、既存のデバッグ手法では太刀打ちできないことから今回のデバッガを開発することにしました。
既存のデバッグ手法
既存のデバッグ手法としては以下のようなものが挙げられます。
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とのインテグレーション作業に時間がかかりそうなことから、見送りました。
Initial DWARF support has landed in Chrome DevTools!
— Chrome DevTools (@ChromeDevTools) 2019年11月8日
It means that you can resolve stack traces, set breakpoints and step-in/-over source code in C/C++/Rust natively, without generating source maps. pic.twitter.com/s3IwkJV6Tr
LLDB + wasmtime
wasmtimeはMozillaが開発しているWebAssemblyのJITコンパイラです。
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するだけで十分ですが、デバッグ時には実際にどの型を比較したのか、などの追加の情報が欲しいです。これを実現するためにはデバッグ時のみネイティブコードにデバッグ情報を乗せる、などの作業が必要になります。しかし、それぞれの例外をハンドリングするためにデバッグ情報を伝播するのは非常に大変でした。
本当に欲しかったデバッガの要件
コンパイラ開発において本当に欲しかったデバッガの要件は以下のとおりです。
- 実行時にどのwasm命令を実行しているか把握できる
- プロセスの制御ができる
- メモリダンプ
- DWARFを解釈できる
- 命令とソースコードの対応
- 変数のダンプ
しかしこれを満たすようなデバッガが見つからなかったため、自作する結論に至りました。
wasminspect
GIFでだいたいの雰囲気は伝わると思います。
冬休みの一週間強でデバッガ用のWebAssemblyランタイムを書いて、春休みの最初の一週間でデバッガフロントエンドを書いて、やっとまともに使えるようになりました。今ではバリバリコンパイラ開発で活躍しています。
実装
基本的にLLDBのコードを参考にしながら実装しました。大きな違いはLLDBがOSのシステムコールを使ってプロセスを制御するのに対して、wasminspectでは内蔵したランタイムを直接制御する部分です。
ランタイムはJITコンパイルもAOTコンパイルもせず、愚直にインタプリタとして実装しました。これはWebAssembly命令レベルでのステップ実行を実現するためです。また、wasmtimeとLLDBの組み合わせのときに発生したデバッグ情報の伝播も必要なくなりました。
ランタイムの開発は公式のドキュメントやテストケースが充実していて非常にスムーズに進みました。
DWARF for WebAssembly
正直DWARF対応だけで何本かブログが書けそうなくらいしんどかったです。基本的に仕様を読みながらLLDBのコードを参考にして実装するだけでしたが、単純に量が多かったです。
WebAssemblyに固有の話としてはDWARF上のスタックとLLVMが吐くWebAssemblyのスタックが一致しない、ということがありました。
LLVMが吐くWebAssemblyでは線形メモリ上に擬似スタックのような空間を確保してそこを伸び縮みさせてフレーム変数を管理しています。(WebAssemblyの仕様で定義されているスタックとは別)
rsp
に対応する値がグローバルに保持されていて、x86_64向けの命令のようにプロローグとエピローグが擬似スタック上で実行されます。
DWARFが示すスタックはこの擬似スタックにほぼ対応しているので、LLVMの実装に依存することでローカル変数のダンプができるようになりました。
将来的にはWebAssembly向けの位置情報がDWARFの仕様に追加されるんじゃないかなぁと予想しています。既にドラフトの文書があったりします。
自作デバッガの功績
今回作ったデバッガは既にいくつかのSwiftのバグを修正するのに大きく貢献しています。特にメモリダンプ機能は非常に便利で、正常に動くx86_64向けバイナリと比較することで破壊されたメモリの位置を特定できます。
僕もメモリダンプとにらめっこしてる pic.twitter.com/lwLBWK2rkT
— kateinoigakukun (@kateinoigakukun) 2020年2月21日
まとめ
一応一通り動くデバッガができましたが、基本的に僕自身がSwift for WASMの開発をするための道具として開発しているだけなので、あまり他の環境で使われることを考慮していません。試していないですが、多分EmscriptenではDWARFの解釈がうまく行かないと思います。
ドキュメントを軽く書いたのでもし興味があれば触ってみてください。
0からデバッガを作る機会はなかなか無いので楽しかったです。これからも「無いなら作ればいい」の精神でやっていきたいと思います。