Open5

WebAssemblyのblock系のデコードで誤りやすいポイント

chobi_echobi_e

Opcode block (0x02)をはじめとしたblock系のParseは少しだけハマりポイントがあります。直後に来るLEB128の値ではブロック構造が返す型を表しているのですが、ここでのIntは符号なしではなく符号ありということです。

chobi_echobi_e

5.4.1 Control Instructions

Control instructions have varying encodings. For structured instructions, the instruction sequences forming nested blocks are terminated with explicit opcodes for end and else. Block types are encoded in special compressed form, by either the byte 0x40 indicating the empty type, as a single value type, or as a type index encoded as a positive signed integer.

翻訳をかけてみるとこのように書かれています。

制御命令はさまざまなエンコーディングを持っています。構造化された命令においては、ネストされたブロックを形成する命令シーケンスは、終了やそれ以外の命令のための明示的なオペコードで終了します。ブロックの種類は、空のタイプを示すバイト0x40としてエンコードされているか、単一の値タイプとしてエンコードされているか、または、正の符号付き整数としてエンコードされたタイプインデックスとして特別な圧縮形式でエンコードされています。

実は、i32(0x7E)などの単一の値型は符号付きのleb128で処理を行うと負の値になります。つまり、マイナスの値であれば単一型もしくは空(0x40)。ポジティブな値であれば事前定義された型となります。

chobi_echobi_e

2.4.8 Control Instructionsだけを見ていると一見blocktypeは符号なしのleb128ではないか?typeidxの定義がu32だし、Number typesもNumber types are encoded by a single byte.と書かれているので符号なしだろう。

と思いきや、実際は符号有りなので注意しておきましょう。

chobi_echobi_e

0x7Fはsigned leb128で表した場合0b0111_1111でMSBが0のため最終バイトです。7bit目が1のため、符号付きである事がわかります。と、いうことで126を最終的に符号拡張して-1

Block Type 符号付きLEB128での値
i32 i32 -1
i64 i64 -2
f32 f32 -3
f64 f64 -4
funcref funcref -16
externref externref -17
[] empty -64

i32の型はバイト値でみれば0x7E(126)ではあるのですが、符号付きleb128の特徴を使うと負の値(-1)として判定できるようになっています。他の単一型および空表現も同様にsigned leb128で読んだ場合は負の値になるようになっています。

// 型の判定としてはこのようになる
var blocktype = Leb128ReadSigned(data, ref offset);
if (blocktype < 0) {
   // 単一型
   switch (blocktype) {
       case -1: // i32
          ...
          break;
   }
} else {
  // 複数型(types参照)
}

これによりtypesが大量に定義されていたとしても正しく型の判定処理が行えるようになっています。
WebAssemblyの仕様ではu32と書かれている値(leb128周り)はわりと自由に解釈して良いと思いきや、きちんと見ないと境界系で違う挙動になってしまうので注意が必要です。

chobi_echobi_e

この問題はmulti-value関連の仕様に関する問題であり、関数の型や、多値で返ってくるようなものを大量に定義して使っていなければ遭遇しない問題です。