上一篇文章介绍了WebAssembly(后文简称Wasm)二进制格式,这一篇文章将介绍Wasm指令集、操作数栈和部分指令。
Wasm指令集
和真实的机器码一样,Wasm二进制文件中的代码也由一条一条的指令构成。同样,Wasm指令也包含两部分信息:操作码(Opcode)和操作数 (Operands)。Wasm操作码固定为一个字节,因此最多能表示256条指令,这一点和Java字节码一样。Wasm1.0规范一共定义了172条指令,这些指令按功能可以分为5大类,分别是:
- 控制指令(Control Instructions),共13条。
- 参数指令(Parametric Instructions),共2条。
- 变量指令(Variable Instructions),共5条。
- 内存指令(Memory Instructions),共25条。
- 数值指令(Numeric Instructions),共127条。
可以看到,已经定义的指令中,有超过2/3属于数值指令。为了方便人类书写和理解,Wasm规范给也给每个操作码定义了助记符(Mnemonic),比如说操作码0x41
的助记符是i32.const
。下面是已定义指令的操作码分布示意图:
有一部分指令需要携带一些信息,这些信息编码后紧跟在指令操作数的后面,叫做静态立即参数(Static Immediate Arguments,后文简称立即数)。 以i32.const
指令为例,操作码0x41
后面要跟一个编码后的32位整数。在后面的例子中,我们将用类似下面这样的示意图来表示编码后的指令:
bytecode:
...][ i32.const ][ 123 ][...
和JVM等栈式虚拟机一样,大部分Wasm指令也会用到操作数栈(Operand Stack,后文简称栈)。这些指令从栈顶弹出一个或多个数,进行计算,然后把结果推入栈顶。被指令操作的这些栈顶元素叫做指令的动态操作数(Dynamic Operands,后文简称操作数)。在后面的例子中,我们将用类似下面这样的示意图来表示指令执行前后栈的状态(小箭头表示弹出或推入操作):
stack:
| | | |
| e |➚ | |
| d |➚ ➘| d+e |
| c | | c |
| b | | b |
| a | | a |
└───────────┘ └───────────┘
before after
参数指令和数值指令仅仅对栈进行操作,行为比较简单,由本文进行介绍。其他指令将在后续文章中介绍。
参数指令
参数指令有两条:drop
(操作码是0x1A
)和select
(操作码是0x1B
)。
drop
drop
指令,从栈顶弹出一个任意类型的操作数。drop
指令没有立即数,下面是它的示意图:
bytecode:
...][ drop ][...
stack:
| | | |
| | | |
| d |➚ | |
| c | | c |
| b | | b |
| a | | a |
└───────────┘ └───────────┘
select
select
指令先后从栈顶弹出3个操作数,如果最先弹出的操作数等于0则将第二个弹出的操作数推入栈,否则将第三个弹出的操作数推入栈。select
指令也没有立即数,下面是它的示意图:
bytecode:
...][ select ][...
stack:
| | | |
| e(i32) |➚ | |
| d |➚ | |
| c |➚ ➘| e!=0?c:d |
| b | | b |
| a | | a |
└───────────┘ └───────────┘
注意位于栈顶的操作数必须是i32
类型,其余两个操作数必须有相同类型。当需要强调操作数的具体类型时,我们会在示意图中用圆括号标出类型。drop
和select
是比较特殊的两条指令,因为只有这两条指令没有将操作数的类型完全限定。对于其他的指令,所有操作数的类型都是完全限定的。
数值指令
数值指令可以按操作数类型分成i32
、i64
、f32
、f64
四组,每一组指令又可以按照操作进一步分为:
- 常量指令(Constant Instructions)
- 测试指令(Test Instructions)
- 比较指令(Comparison Instructions)
- 算术指令(Arithmetic Instructions)
- 一元(Unary)算术指令
- 二元(Binary)算术指令
- 转换指令(Conversion Instructions)
除常量指令外,其余数值指令都没有立即数。
常量指令
常量指令将立即数推入栈顶,以i32.const
指令(操作码0x41
)为例,下面是它的示意图:
bytecode:
...][ i32.const ][ 123 ][...
stack:
| | | |
| | ➘| 123(i32) |
| d | | d |
| c | | c |
| b | | b |
| a | | a |
└───────────┘ └───────────┘
常量指令一共有四条,另外三条是: i64.const
(操作码0x42
)、 f32.const
(操作码0x43
)、 f64.const
(操作码0x44
)。不难发现,Wasm操作码助记符的命名规则是:如果指令执行后栈顶元素的类型是t
,那么助记符就以t.
开头。
测试指令
测试指令从栈顶弹出一个操作数,测试它是否是0,如果是则将i32
类型1推入栈,否则将i32
类型0推入栈。测试指令只有两条:i32.eqz
(操作码0x45
)和 i64.eqz
(操作码0x50
)。以i64.eqz
指令为例,下面是它的示意图:
bytecode:
...][ i64.eqz ][...
stack:
| | | |
| | | |
| d(i64) |➚ ➘| d==0(i32) |
| c | | c |
| b | | b |
| a | | a |
└───────────┘ └───────────┘
可以看到,测试指令的结果其实是布尔值,只不过Wasm没有定义bool
类型,而是用i32
类型来表示(1表示ture
,0表示false
)。
比较指令
比较指令从栈顶弹出两个相同类型的操作数,进行比较,然后将结果压栈。和测试指令一样,比较指令的结果也是布尔值(也就是i32
类型)。以i64.lt_s
指令(操作码0x53
)为例,下面是它的示意图:
bytecode:
...][ i64.lt_s ][...
stack:
| | | |
| e(i64) |➚ | |
| d(i64) |➚ ➘| d<e(i32) |
| c | | c |
| b | | b |
| a | | a |
└───────────┘ └───────────┘
除了等于(eq),还有进行不等于(ne)、小于(le)、大于(gt)、小于等于(le)、大于等于(ge)比较的指令,这里就不一一介绍了。需要说明的是,对于有些整数类型的指令,需要明确指出如何解释操作数:将其当成有符号数(Signed,助记符带_s
后缀)还是无符号数(Unsigned,助记符带_u
后缀)。这类指令一般是成对儿出现,比如上面例子中的i64.lt_s
指令,与之对应的还有i64.lt_u
指令(操作码0x54
)。
一元算术运算
一元算术指令从栈顶弹出一个操作数,进行计算,然后把结果推入栈顶。以f32.neg
(操作码0x8C
)指令为例,下面是它的示意图:
bytecode:
...][ f32.neg ][...
stack:
| | | |
| | | |
| d(f32) |➚ ➘| -d(f32) |
| c | | c |
| b | | b |
| a | | a |
└───────────┘ └───────────┘
二元算术运算
二元算术指令从栈顶弹出两个操作数,进行计算,然后将结果推入栈顶。以f32.sub
指令(操作码0x93
)为例,下面是它的示意图:
bytecode:
...][ f32.sub ][...
stack:
| | | |
| e(i64) |➚ | |
| d(i64) |➚ ➘| d-e(i64) |
| c | | c |
| b | | b |
| a | | a |
└───────────┘ └───────────┘
类型转换
类型转换指令从栈顶弹出一个操作数,进行类型转换,然后把结果推入栈顶。如果操作数在类型转换之前的类型是t
,之后的类型是t'
,转换操作是conv
,则指令的助记符是t'.conv_t
。以i32.wrap_i64
(操作码0xA7
)指令为例,下面是它的示意图:
bytecode:
...][ i32.wrap_i64 ][...
stack:
| | | |
| | | |
| d(i64) |➚ ➘| d'(i32) |
| c | | c |
| b | | b |
| a | | a |
└───────────┘ └───────────┘
比较、算 术、转换指令数量比较多,本文无法一一介绍,请读者参考Wasm规范。