(2023-06-28追記: 本記事の解決編を書きました)
TL;DR
- WebAssembly(Wasm)フォーマットは文字列リテラルなどのデータを保持するために、
dataセクションという領域を持つことができる -
dataセクションのうち、passiveモードのものはプログラム実行時にmemory.init命令でメモリにロードできる - Rust 1 には LLVM の任意の命令を関数として使用する機能 2 がある
- LLVM の
wasm系バックエンドには、 Wasm のmemory系の命令にそのまま変換される命令がある - Wasm の
memory.initにそのまま変換されるllvm.wasm.memory.initという命令が LLVM IR に存在したが、こっそり消されてしまった - 現状 Rust から
memory.init命令を直接叩く手段はないっぽい?
背景
Wasmファイルの data セクション内のデータを後から書き換えたいようなケースを考えます。
Rust で生成したWasmファイルの場合3、active モードの data セクションが一つ以上あり、それが実行前にロードされるようになっています。そのため、この data セクションを書き換えてやればいいのではと考えますが、これはうまくいきません。
なぜなら、この data セクションがメモリにロードされたすぐ後ろの領域を LLVM がリンクした関数4が使用しており、この data セクションの長さが変わってしまうと、この後ろの領域に触ってしまい、実行時に Wasm ファイルがエラー終了するようになってしまうからです。
解決方法
そこで、あらかじめ Wasm ファイルに passive モードの data セクションを追加しておき、実行中にこの data セクションをメモリにロードするようにします。
passive モードの data セクションをメモリにロードするためには、
スタックにロード先のメモリアドレス、ロードするデータの data セクションでの開始位置、データ長をスタックに積んだ後、 memory.init 命令を実行する必要があります。
Rust で Wasm をターゲットにする場合、Wasm 固有の命令の多くは core::arch::wasm32 内の関数を通じて使えるようになっていますが、 memory.init 命令はこの中に含まれません。
そこで、Rustにある5 link_llvm_intrinsics という feature を使用することを考えます。この feature は、LLVM IR の命令を Rust から関数として呼び出せるようになるものです6。
新たな問題
ネットを検索すると、LLVM IR には Wasm の memory.init 命令に変換される、 llvm.wasm.memory.init という命令があるらしいという資料が見つかりました。
そこで、上記 link_llvm_intrinsics を使用して、Rust コード上にこの llvm.wasm.memory.init 命令を直接記述することを試みましたが、なぜかうまくいきませんでした。
さらに調査した結果、このようなコミット を発見しました。その説明に曰く、
Also removes unused init/drop intrinsics rather than trying to make them work for 64-bit.
どうやら LLVM の Wasm64 対応時に、使用されていないし対応が面倒だからと、命令ごと消されてしまったようです7。
終わりに
どうやら Rust (などの Wasm 生成のバックエンドとして LLVM を使用している処理系)から memory.init 命令を叩くことはできないようです。
この回避策としては、memory.init 命令を使用したい箇所をダミー関数として括りだしておき、後からこのダミー関数に正しい中身を直接注入するのが手っ取り早そうです。