WebAssembly(WASM)のバイトコードやスタックマシンに対する解説が少なかったので、一つガツンと低レイヤーの入門記事を書こうかなと思いました!3句ほど詠んでマニアックなWASMの世界に最速で旅立ちましょう。
WebAssembly自体の楽しみな展望や意義については末尾に書かせてもらいますので、そっちから読んでもらっても構いません!
まず、WASMには3つの形態があります。
①WAT(.wat)
これはLispっぽい感じの人が読み書きできるWASMの形で、WASMの高級言語版です。
②WAST(.wast)
これはほぼ機械語のWASMの表記です。アセンブリ言語っぽいのはこちらで、WATはこれにシンタックスシュガーを加えた形になります。
③WASM(.wasm)
これがWASMの機械語バイナリです。コンパイル後の形であり、C言語、Rust、Go言語、WATなどからコンパイルでき、ブラウザでJSのように実行可能になります。
さて、では単に「42」とコンソールログに表示するプログラムをWATで書いてブラウザで走らせてみましょう。
まずはWASMコンパイラのインストール
$ git clone --recursive https://github.com/WebAssembly/wabt
$ cd wabt
$ make gcc-release
ではWATファイル作ります
$ vim test.wat
(module
(func $main (result i32)
i32.const 42)
(export "main" (func $main)))
まー、だいだい意味はわかりますね。単に42を返す関数です。i32はint32型です。
これを次のようにすると、test.wasmが生まれます。
$ ./wabt/out/gcc/Release/wat2wasm test.wat -o test.wasm
ここで生まれたWASMバイナリをバイナリエディターでみてみましょう。
https://hexed.it/
でtest.wasmをロードしてみて下さい。
このバイナリ列をJSの配列にしてみます。hexdumpコマンドで16進数表記は出ますが、文字列処理面倒なので僕が整えたデータを使ってください。
ブラウザのコンソールで次のように打ち込んで下さい。ChromeならCntl+SHift+Iかな?
> var wasmCode = new Uint8Array([0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00,0x01,0x05,0x01,0x60,0x00,0x01,0x7F,0x03,0x02,0x01,0x00,0x07,0x08,0x01,0x04,0x6D,0x61,0x69,0x6E,0x00,0x00,0x0A,0x09,0x01,0x07,0x00,0x01,0x01,0x01,0x41,0x2A,0x0B])
続いて、このWebAssemblyのコードは次のようにModule読み込み、エクスポートして使えます。
> var wasmModule = new WebAssembly.Module(wasmCode);
> var wasmInstance = new WebAssembly.Instance(wasmModule)
> console.log(wasmInstance.exports.main());
コンソールに42と表示されたはずです。
しかしながら、あのバイトコード一体なんだったのでしょう?
基本的にWASMの仕様は全てここに書いてあります。
https://webassembly.org/docs/binary-encoding/#start-section
この仕様書通りに正確に読み進めていくと全部解読できるようになっています。もっと簡略に意味がある程度分かるラベルが見たい人は次のように打ってください。
$ ./wabt/out/gcc/Release/wat2wasm -v blank.wat
このコマンドで出てくるガイドは多少実際に生まれるバイナリとズレます。いずれにせよ、仕様書を読む必要があり、適宜参照しながら進めていきます。では解釈を始めます。
まずは、出だしから。
WebAssemblyのModuleの構造をみていくべきでしょう。
Moduleはおまじないとバージョン情報から始まり、次はセクションが羅列されていて、その中にサブセクションがあるという構造になっています。
バージョン情報の次の瞬間からセクション1が始まります。
セクション1はこの表の通り、Type(型定義)のセクションです。
引用元:https://ukyo.github.io/wasm-usui-book/webroot/binary-format.html
次にセクションサイズの5が書かれていますが、このセクションサイズにより、この後5バイトでセクションが終わり、次のセクションの読み込みを始められることがVMに伝えることができるでしょう。定義された型の数はが次にかかれます。そして次に一つ一つの型が列挙されていきます。今回は1個のみですが。
タイプは関数を表す0x60が刻まれます。
func $main (result i32)の通り、引数0のi32型の返り値が一つです。
今回は何もインポートしてないのでセクション2:インポートセクションは飛ばされました。
セクション3:関数宣言では、関数の数とそのインデックスが定義されて終わりとなっています。
では次のバイトコード
ここで名前の定義がされています。関数宣言セクションでもNameセクションなどでもないことに注意です。
いよいよ関数本体のセクション、セクション10です。見ていただくと分かりますが、命令セットがあるのはWASM全体で412Aの部分だけなんですね。そりゃそうか。だってやってること42返してるだけだもん。もちろん、長い関数処理を書いたらこの部分も凄く長くなります。WebAssemblyで196個と大量に用意されてるAssemblyオペコードはこの部分を書くことに使われます。
===============
さて、ではWebAssemblyをより深く理解するための遊びに移りましょう。
文化的WASM人としてポエムでイキリ散らかしましょう。
そして日本人で低レイヤー界隈なら短歌ですよね。そりゃ。
というわけで、早速バイトコードで短歌を詠みましょう。
WASMで5バイト、7バイト、5バイト、7バイト、7バイト合計31バイトで 余が7歩を歩む間に一句読め!!!
・・・・・・・・。
そもそもこのWASMの時点で、37バイトで字数超えてるし・・・。
そう。我々がコードとしていじれるのは今回0x41,0x2Aの2バイトだった0x0B直前の関数Body領域です。ここで31バイトで詠みましょう。
詠め!!!!
・・・・・・・・・・・。
===========================
オケです、曹丕並みの即興で詠みましょう!低レイヤーなら誰でも知っているNOPオペコード、つまり何もしないオペコードであと29バイト埋めましょう。WASMのオペコード表見れば分かりますが、WASMのNOPは0x01です。
文法とスタックマシンの関係はこちら(https://webassembly.org/docs/semantics/)
全体をセクションごとに区切って書きます。
> var wasmCode = new Uint8Array([
0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00,
0x01,0x05,0x01,0x60,0x00,0x01,0x7F,
0x03,0x02,0x01,0x00,
0x07,0x08,0x01,0x04,0x6D,0x61,0x69,0x6E,0x00,0x00,
0x0A,0x06,0x01,0x04,0x00,
0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,
0x41,0x2A,0x0B])
これでいいでしょうか!!? いえ、これだとエラーが出るでしょう!! なぜなら、セクションで定義された長さと関数Body長が変わってしまいますから。再掲します。
では直前の0x06と0x04にそれぞれ29を足しましょう。0x23と0x21かな?
ではバイトコードを置き換えます。
> var wasmCode = new Uint8Array([
0x00,0x61,0x73,0x6D,0x01,0x00,0x00,0x00,
0x01,0x05,0x01,0x60,0x00,0x01,0x7F,
0x03,0x02,0x01,0x00,
0x07,0x08,0x01,0x04,0x6D,0x61,0x69,0x6E,0x00,0x00,
0x0A,0x23,0x01,0x21,0x00,
//ここから短歌
0x01,0x01,0x01,0x01,0x01,
0x01,0x01,0x01,0x01,0x01,0x01,0x01,
0x01,0x01,0x01,0x01,0x01,
0x01,0x01,0x01,0x01,0x01,0x01,0x01,
0x01,0x01,0x01,0x01,0x01,0x41,0x2A,
//短歌終わり
0x0B])
さてと、ではConsoleに出して詠みますか。
> var wasmModule = new WebAssembly.Module(wasmCode);
> var wasmInstance = new WebAssembly.Instance(wasmModule)
> console.log(wasmInstance.exports.main());
> 42
詠めた!!いや〜粋だねぇ〜!?このNOPの静けさ、まるで「4分33秒」の演奏を聴いているかのようですよ〜。「29バイト」ってタイトルにしましょう。声に出して読むと
ノップ、ノップ、ノップ、ノップ、ノップ
ノップ、ノップ、ノップ、ノップ、ノップ、ノップ、ノップ
ノップ、ノップ、ノップ、ノップ、ノップ
ノップ、ノップ、ノップ、ノップ、ノップ、ノップ、ノップ
ノップ、ノップ、ノップ、ノップ、ノップ、アイサンジュウニコンスト、ヨンジュウニ
いや〜ね。粋だね〜〜。
けどさ、ピアノのいい曲教えてって聞かれて4分33秒沈黙されたら普通ヒクよね?
初心者にこのイキさは分からないかもしれない。
もう少し、いろんなオペコードを知ってもっと自由な世界で詠もう。
というわけで、そのためにも、他のWASMプログラムも見て見ましょう。
次は引数を2つ取って足し算して返すという関数をエクスポートし、ブラウザで走らせます。
$ vim addTwo.wast
(module
(func $addTwo (param i32 i32) (result i32)
get_local 0
get_local 1
i32.add)
(export "addTwo" (func $addTwo)))
では、WASMのバイナリを作りましょう
$ ./wabt/out/gcc/Release/wat2wasm addTwo.wat -o addTwo.wasm
バイナリエディターでみてみましょう。
https://hexed.it/
でaddTwo.wasmをロードしてみて下さい。今回はこんな感じですね。
では、どんな内容になっていくか1セクションずつ確認しましょう。
今回は引数があるのでちょっと違いますね。
次のバイトコード
ここはエクスポート名がmainがaddTwoになったくらいしか違いはありません。
引数が増えてもローカル変数は増えません。
このようにローカル変数(index)からPop処理をスタックマシンにするのがget_localです。このコードでは2つポップし、i32.addしてスタックにPushします。
こういったStack処理についてはこちらが分かりやすく書いてます。
https://retrage01.hateblo.jp/entry/2018/03/04/144355
こんな感じでオペコードというのは使っていくことができるのです。
というわけで。。。。。。。詠め!!!!
お気付きの方は多かと思いますが、get_local、set_localには引数かローカル変数を関数で定義しないといけないので、さっきみたく、オペコードを自由に改造して一句詠むことことが難しくなります。引数定義する場合はそういう設定の関数を一度コンパイルしてから改造する形になるでしょう。
基本的にx86アーキテクチャーでアセンブリ短歌が気軽に楽しめるのは、x86アーキテクチャーがEAXなどのレジスタ記憶領域に直接アクセスでき、定義せずとも変数として扱えるからです。ここはWebAssemblyと違います。
ではどうするか?
よく、オペコード表と仕様書を読むと、スタックとは独立な記憶領域である線形メモリとその操作オペコードが定義されていることが分かります。i32.load、i32.storeです。これを変数代わりに使えば詠めそうですね!!
よし、では先ほどの短歌のNOPの部分をちょっと削ってi32.load、i32.storeを入れて見ます。
i32.const 0
i32.const 42
i32.store 2 0
これが0番アドレスに42を書き込むという意味になります。
細かく言えば、i32の0をスタックにPush,i32の42をスタックにPush、二つPoPしてi32.storeをオフセット0からi32型2個分読み込むんで上記の処理に使うということです。
そして、これをロードしてStackにpushするのは
i32.const 0
i32.load 2 0
二つ合わせて翻訳します。
0x41 0x00
0x41 0x2A
0x36 0x02 0x00
0x41 0x00
0x28 0x02 0x00
これをそのまま、この前のNOPに置き換えたいところですが、メモリを用意しないとエラーが出てしまうので、最初のバイナリにメモリ領域を追加しましょう。
$ vim blank.wat
module
(import "js" "memory" (memory 1))
(func $main (result i32)
i32.const 42)
(export "main" (func $main)))
では生成されたバイナリをブラウザコンソールのJSで読み込みます。
セクションごとに区切ってJSスタイルで見ましょう。
> var wasmCode = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f,
0x02, 0x0e, 0x01, 0x02, 0x6a, 0x73, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x01,
0x03, 0x02, 0x01, 0x00,
0x07, 0x08, 0x01, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x0a, 0x06, 0x01, 0x04, 0x00,
0x41, 0x2a,
0x0b])
メモリセクションであるセクション2が追加されてることが分かりますね。これを追加しておけば今後汎用的に使えるでしょう。この方法は変数・引数を定義して詠みたい人はそれでもいいでしょう。
個人的には最後の0x0b直前にオペコードを31バイト書いて詠んでいれば十分風流で粋だと思います。
さて、これに先ほど作った42をi32.storeとi32.loadするコード、0x41,0x00,0x41,0x2A,0x36,
0x02,0x00,0x41,0x00,0x28,0x02,0x00,をNOPつきで挿入しましょう。
> var wasmCode = new Uint8Array([
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7f,
0x02, 0x0e, 0x01, 0x02, 0x6a, 0x73, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x01,
0x03, 0x02, 0x01, 0x00,
0x07, 0x08, 0x01, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x00, 0x00, 0x0a, 0x23, 0x01, 0x21, 0x00,
//ここから短歌
0x01,0x01,0x01,0x01,0x01,
0x01,0x01,0x01,0x01,0x01,0x01,0x01,
0x01,0x01,0x01,0x01,0x01,
0x01,0x01,0x41,0x00,0x41,0x2A,0x36,
0x02,0x00,0x41,0x00,0x28,0x02,0x00,
//終わり
0x0b])
さて、実行しますが、メモリをJSサイドで渡してあげないといけないので、注意です。
> var wasmModule = new WebAssembly.Module(wasmCode);
> const memory = new WebAssembly.Memory({initial:1});
> var importObject = {
js: {
memory: memory
}
};
> var wasmInstance = new WebAssembly.Instance(wasmModule,importObject )
> console.log(wasmInstance.exports.main());
> 42
いや〜〜〜ね〜、粋だね〜!この最初の575のシンとした静けさを破る最後のstoreとload。ブラウザのコンソールでこんな粋な歌を詠ん出るなんてChromeの開発チームもび〜っくりだよ〜!いや〜粋だね〜。
もう変数を読み書きできるんだから直接42を出すこと以外もできるでしょう。なんかプログラマっぽいことやりましょう。
Loop開始のオペコードはLoopです。0x03ですな。ここでWASM(WAT)のLOOP文法は少々特殊なので特記しておきます。
(block ;;ここがdepth 1
(loop ;; ここがdepth 0
(br_if 1(これはdepth) 条件式のオペコード)
(br 0) ;;何も書かないとループされない。一番下にdepthを指定してJUMPする必要
)
)
Depthを指定してJUMPするのがループになるんですね。これをオペコード表と見比べながら組んでいきましょう。
では、今までにやった操作も加えて面白くしてみます。
(module
(import "js" "memory" (memory 1))
(func $main (result i32)
(block
(loop
(br_if 1 (i32.eq (i32.load (i32.load (i32.load (i32.const 8)))) (i32.const 17)))
(br 0)
)
)
(i32.load (i32.const 0))
)
(export "main" (func $main)))
これはアドレス8をロードしたら出てくるアドレスをロードしたら出てくるアドレスをロードしたら17が出てくると、ループを脱して0番アドレスの中身を出力します。
出来なかったら無限ループです。これは入力を間違うと、(OSX上のChromeで試したところ)タブが全然閉じれない不具合を引き起こしたもので、間違わないようにする緊張感が出ますね。上の条件を満たすメモリ(WebAssembly.memory Object)を渡さなければなりません。
コンパイルするとbody部分は29byte。字足らずですが、粋なので良いでしょう。
ではどうやってメモリオブジェクトを操作するかという話になりますが、公式ドキュメントから操作方法が出されています。
https://developer.mozilla.org/ja/docs/WebAssembly/Using_the_JavaScript_API
> var memory = new WebAssembly.Memory({initial:1});
> new Uint32Array(memory.buffer)[0] = 17;
> new Uint32Array(memory.buffer)[1] = 0;
> new Uint32Array(memory.buffer)[2] = 4;
ここは一つの配列要素ごとに32bit=4byte取っています。
つまり、0が入っているアドレスは0x04です。アドレス8は[2]で入力出来ます。
よって上記をWASMを実行するとアドレス8を読んで4が出て、4を読んだら0が出て、0を読んだら17が出てきてループを抜けれるはずです。
では短歌から。
> var wasmCode = new Uint8Array([
0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,
0x01,0x05,0x01,0x60,0x00,0x01,0x7f,
0x02,0x0e,0x01,0x02,0x6a,0x73,0x06,0x6d,0x65,0x6d,0x6f,0x72,0x79,0x02,0x00,0x01,
0x03,0x02,0x01,0x00,
0x07,0x08,0x01,0x04,0x6d,0x61,0x69,0x6e,0x00,0x00,0x0a,0x22,0x01,0x20,0x00,
//短歌始まり
0x02,0x40,0x03,0x40,0x01,
0x41,0x08,0x28,0x02,0x00,0x28,0x02,
0x00,0x28,0x02,0x00,0x41,
0x11,0x46,0x0d,0x01,0x0c,0x00,0x0b,
0x0b,0x41,0x00,0x28,0x02,0x00,
//短歌終わり
0x0b
])
で、メモリの設定をします。
> var memory = new WebAssembly.Memory({initial:1});
> new Uint32Array(memory.buffer)[0] = 17;
> new Uint32Array(memory.buffer)[1] = 0;
> new Uint32Array(memory.buffer)[2] = 4;
> var importObject = {
js: {
memory: memory
}
};
【警告】ここから、実行に移りますが、少しでも数字にズレがあれば無限ループに入ってしまいます。
止める為にはタスクマネージャーからブラウザを強制終了する必要があるので、タスクマネージャーを起動しておいてください。
> var wasmModule = new WebAssembly.Module(wasmCode);
> var wasmInstance = new WebAssembly.Instance(wasmModule,importObject )
> console.log(wasmInstance.exports.main());
> 17
いや〜〜〜〜粋だね〜〜〜。ちゃんとメモリ作れればちゃんと返答してくれて、間違った奴には無限ループを永遠に歌わせる短歌。
こんな短歌、百人一首の殿方もび〜〜っくりだよ。「短歌をウィルスのように詠む」ってボタン作りたいくらいだ。いや〜〜粋だね〜〜。
ウィルスとか詐欺とか不謹慎ですって?分かりました。 そろそろなぜWebAssemblyがそんなに重要なのか夢のある話しましょう。
==============================
WASMで結局どうなる?
WebAssemblyは命令セットアーキテクチャーです。
今まではブラウザで実行できるものと言えばJavaScriptというスクリプト言語でした。これは他の多くの言語との互換性はないものだったので、ウェブブラウザ、ウェブサイトで動くものを作る際に多くの言語は使えませんでした。
しかしながらWASMは機械語的な命令セットであり、LLVM等をコンパイラにもつ多くの言語から翻訳可能になります。C言語やGo,Rustなどの言語が使えることから、ウェブエンジニアの潜在的な数が爆発的に増えたことになります。
また、WASMは速いです。今はChromeのV8コンパイラとあまり差がありませんが、ウェブが拡大するにつれ、そして最適化に加わる人の数が増えるにつれ、差はついて行くことでしょう。
さて、それだけでしょうか?多くのことを次の記事に昔書きました。
全てのサイトがアプリ化していく世界で、どうなるかは分かりませんが、高レイヤーと呼ばれたWebと低レイヤーと呼ばれたOS・アーキテクチャーの世界がぐちゃぐちゃに混じるであろうWASMの世界は絶対に面白いです。