Wasm指令的操作码分为两部分信息:操作码和操作数
操作码指指令的ID,决定指令将执行的操作;操作数则相当于指令的参数。
操作码
Wasm指令的操作码固定为一个字节,因此指令集最多只能包含256条指令,名称为字节码。Wasm规范一共定义了178条指令,这些指令按功能分为5大类,分别为:
控制(Control)指令,共13条
参数(Parmetric)指令,共2条
变量(Variable)指令,共5条
内存(Memory)指令,共25条
数值(Numeric)指令,共133条,可以按操作进一步分为常量指令、测试指令、比较指令、算术运算指令、类型转换指令
0x00-0x11 Control
0x1A-0x1B Parmetric
0x20-0x24 Variable
0x28-0x40 Memory
0x41… Numeric
未包含地方属于未定义操作码
根据Wasm规范,我们可以把这178个操作码定义为常量
助记符
Wasm规范给每个操作码定义了助记符,比如操作码0x01的助记符是nop,表示无操作;操作码0x10的助记符是call,表示函数调用。
符号后缀
Wasm规范要求整数采用2的补码表示,这种补码格式,数据里并没有显式编码整数的符号位,所以数值可以被解释为正数,也可以被解释为负数。(加减乘)无论怎么解释最终的计算结果都是一样的,(除、求余、比较)就必须指定数值有无符号。如果整型指令的结果不受符号影响,则操作码助记符无特别后缀,比如i32.add。否则由指令决定将整数解释为有符号(助记符以_s结尾)还是无符号(助记符以_u结尾)。强调符号的指令一般成对出现,比如i64.div_s和i64.div_u
助记符不是Wasm实现的必要元素,但有助于调试代码。
立即数
操作数分为两种:静态操作数、动态操作数
静态操作数直接编码在指令里,跟在操作码后面。
动态操作数在运行时从操作数栈获取。
静态操作数称为指令的静态立即参数简称立即数
之后出现的操作数特指动态操作数
Wasm大部分指令没有立即数,指令立即数可以大致分为:数值(包括常量和索引)、内存指令和控制指令参数。
内存指令
内存加载/存储系列指令需要指定内存偏移量和对其提示。
block和loop指令
Wasm彻底摒弃了低级的GOTO和JUMP指令,采用更为高级的结构化指令,就是必须使用block、loop和if这3种指令定义顺序、循环和分支结构的起点,都必须以end指令为终点,形成内部是嵌套的指令顺序,可使用br系列指令跳出block和if块,或重新开始loop块,都可以带有参数和结果值。
除了跳转目标不同block指令和loop指令在代码结构上是完全一样的。
在多返回值提案被接受之前,块类型很简单:不能有参数,且最多只能有一个结果,当时块类型是用一个字节表示的:
0x7F表示有一个i32类型结果、
0x7E表示有一个i64类型结果、
0x7D表示有一个f32类型结果、
0x7C表示有一个f64类型结果、
0x40表示没有结果,随着限制的放开
块类型在Wasm二进制中被重新解释为LEB128 有符号整数,解码后的数值对应两种可能:
1、负数:必须是-1、-2、-3、-4或者-64,对应限制放开前的5种结果
2、非负数:必须是有效的类型索引(块类型也存在类型段中)
if指令
if块比以上两种要复杂些,因为内部的指令顺序被else指令一分为二。
br_table指令
br系列指令包括4条:br、 br_if、br_table和return。其中return指令没有立即数,br和br_if指令的立即数是索引类型不需单独定义,br_table指令的立即数比较复杂,包括一个跳转表和默认跳转标签。
操作数
Wasm实际上定义了一台概念上的栈式虚拟机,绝大多数的Wasm指令都是基于一个虚拟栈工作:从栈顶弹出若干个数,进行计算,然后把结果压栈。
我们把这些运行时位于栈顶并被指令操纵的数叫作指令的动态操作数,简称操作数,我们称这个栈为操作数栈。为了实现控制指令,Wasm还需要一个控制栈,在某些地方可以将操作数栈简称为栈。