シリーズWasmバイナリを読めるようになろうWasmの命令
WebAssemblyの命令には様々なものがある。
WebAssemblyはスタックマシンであり、全ての命令はスタックから0個以上の要素をpopし、0個以上の要素をpushする形式で表される。
例えば、(a+b)*2を計算するwasmコードは以下のような挙動を示す。
Wasm(func $calc
(param $a i32) (param $b i32)
(result i32)
local.get $a ;; $aをスタックに追加
local.get $b ;; $bをスタックに追加
i32.add ;; $a,$bをスタックから除き、$a+$bをスタックに追加
i32.const 2 ;; 2をスタックに追加
i32.mul) ;; 先頭2個の値をスタックから除き、($a+$b)*2をスタックに追加
wasm命令の特徴としては、コードにジャンプ命令が存在しないことが挙げられる。 x86に存在するjump命令やret命令は、悪用されることでROP(Return Oriented Programming)などの攻撃につながる。Wasmでは、これらの命令を排除し、条件分岐やループといった構造化制御のみ可能なフローを制限することで、外部からの攻撃を軽減することを狙っている。
さらに、Wasmコードの構造化制御命令であるブロックやループは、関数と同じく入力と出力を持つ。つまり、ブロック中で消費するスタック数と、ブロック内で生産されるスタック数が予め決められている。
Wasm(func $count (result i32)
(local $n i32)
i32.const 3
(loop $again (param i32) (result i32) ;; このloopでは、i32を1つ消費し、1つ生み出す
i32.const 1
i32.sub
local.tee $n
local.get $n
br_if $again))
本稿では、Wasmの命令群をいくつかに分類し、代表的なものを紹介する。Wasmの命令としてどのようなものが存在するかの傾向がつかめるようになることを目指す。
新しい数値をスタックに積む命令
nは命令コード中にハードコートされた即値で、これらの命令によりスタックトップに値が詰まれる。
Wasmi32.const n
f64.const f
ヒープ型に関する命令
Wasmstruct.new x
struct.set x i
array.init_data x y
struct.set命令では、型インデックスxの、構造体の要素iにスタックトップの値を設定する。
array.init_data命令では、データコンポーネントyの値を元に型xのデータを持つ配列を作成する。
参照型に関する命令
Wasmref.null
ref.func x
ベクターに関する命令
Wasmv128.load x ao
i8*16.splat
演算子
入出力が同じ型の命令
値を1つ使う命令
Wasmi32.clz
i32.popcnt
f32.abs
f32.sqrt
値を2つ使う命令
Wasmi32.add
i32.or
f32.sub
f64.min
真偽値を返す命令
Wasmi32.eqz
i32.eq
f32.gt
構造化命令
Wasmloop ((param i32), (param i64), (result f64))
block ((param i32), (result f64))
br 1
end
end
抽象仕様中にendという命令は存在しないが、バイナリ形式中には0x0Bとしてブロックの終わりを明示する命令が存在する。本稿では、わかりやすさのためend命令をブロックの終わりを示す命令とした。
br命令は、パラメータの数値をnとして、ブロックを現在位置からn個抜ける命令である。
通常、Wasmの命令はパラメータや引数で何らかの絶対値を示すことが多いため、br命令において現在地からの相対値を使用することはとても珍しい。
br命令で複数のブロックを抜けた際は、最後に抜けたブロックのブロック型に応じてスタックの調整がなされる。つまり、ループの引数とループ中に足した値がスタックから全て取り除かれ、返り値となる先頭の何要素かだけが戻される。
関数呼び出し
Wasmcall $func
call_ref $funcref
call_indirect 0 1
call命令は、Wasmストア中でインデックス付された全ての関数のうち$func番目の関数を呼び出す。
call_ref命令は、参照$funcrefが指し示す関数を呼び出す。
call_indirect命令は、Table 0の1番に入っている関数参照を呼び出す。
ローカル変数の使用
Wasmlocal.get 0
local.set 1
local.get命令は、ローカル変数0番から値を取得し、スタックの先頭に積む。local.set命令は、逆にスタックの先頭から値をポップし、ローカル変数1番に格納している。
グローバル変数の使用
Wasmglobal.get 0
global.set 1
メモリ命令
Wasmi32.load x ao
i32.store x ao
memory.grow x
memory.init x y
例えば、i32.load命令では、x番の線形メモリから、aoを{align, offset}の組として値をスタックにロードする。
memory.init x y命令では、データコンポーネントxの内容を元にメモリyを初期化する。
メモリと同様の命令はテーブルにも存在するが、テーブ雨についてはtable.newのような関数は存在していない。
その他
Wasmnop
unreachable
drop
select
nopは何もしない命令
unreachableはtrapを起こす命令
dropはスタックの値を捨てる命令
selectはスタックトップの値に応じて、スタックの2番めと3番めのどちらかをスタックに戻す、3項演算子のような命令