上一篇文章介绍了WebAssembly(简称Wasm)指令集以及指令的操作码(Opcode)、立即数(Immediate Arguments)、操作数(Operands)、操作数栈(Operand Stack,简称栈)等概念,并且详细介绍了参数指令和数值指令。这篇文章将介绍Wasm内存和相关指令。
内存
每个Wasm模块都可以定义或者导入一个内存,内存大小以页为单位,每一页是64K。定义内存时,需要指定内存的页数下限。页数上限可选,可以指定也可以不指定。内存的初始数据则可以在数据段中指定。下面是一个WAT例子,展示了内存和数据段的定义:
(module
(memory 1 8) ;; { min: 1, max: 8 }
(data 0 (offset (i32.const 100)) "hello")
;; ...
)
复制代码
和内存相关的指令共有25条,下面分别介绍。
memory.size
memory.size
指令(操作码0x3F
)把内存的当前页数按i32
类型推入栈顶。memory.size
指令带有一个1字节立即数,可以指定操作的是哪个内存。由于Wasm1.0规范规定最多只能有一个内存,所以目前这个立即数只能是0。下面是memory.size
指令的示意图:
bytecode:
...][ memory.size ][ 0 ][...
stack:
| | | |
| | ➘| p(i32) | # page count
| d | | d |
| c | | c |
| b | | b |
| a | | a |
└───────────┘ └───────────┘
memory.grow
memory.grow
指令(操作码0x40
)将内存增长n
页,其中n
是一个i32
类型的整数,从栈顶弹出。如果操作成功,将增长前的页数按i32
类型推入栈顶,否则将-1
推入栈顶。和memory.size
指令一样,memory.grow
指令也带有一个1字节立即数,且取值必须为0。下面是memory.grow
指令的示意图:
bytecode:
...][ memory.grow ][ 0 ][...
stack:
| | | |
| n(i32) |➚ ➘| p(i32) | # grow n pages
| d | | d |
| c | | c |
| b | | b |
| a | | a |
└───────────┘ └───────────┘
load
load
指令从内存读取数据,然后推入栈顶。具体读取多少字节的数据,以及将数据解释为何种类型的数,因指令而异。Wasm采用了“立即数+操作数”的内存寻址方式,所有load
指令都带有两个u32
类型(LEB28编码的32位无符号整数)的立即数,一个表示对齐方式,另一个表示内存偏移量。load
指令还需要从栈顶弹出一个i32
类型的操作数,立即数和操作数相加即可得到实际要读取的内存起始地址。对齐方式仅起提示作用,不影响实际操作,本文不做介绍,具体请参考Wasm规范。以i64.load
指令(操作码0x29
)为例,下面是它的示意图:
bytecode:
...][ i64.load ][ align ][ offset ][...
stack:
| | | |
| | | |
| d(i32) |➚ ➘|m[offset+d]| # i64
| c | | c |
| b | | b |
| a | | a |
└───────────┘ └───────────┘
load
指令一共有14条,为了统一说明这些指令,我们假设指令执行时计算出的内存地址是a
,此处存放的数据是0xABCDEF1234567890
。由于Wasm使用小端在前的方式存放数据,因此内存数据看起来是下面这样:
mem:
...[ 0x90 ][ 0x78 ][ 0x56 ][ 0x34 ][ 0x12 ][ 0xEF ][ 0xCD ][ 0xAB ]...
复制代码
下表给出这14条load
指令的操作码、实际读取到的字节,以及如何解释这些字节:
Instruction | Opcode | Read | Interpreted As |
---|---|---|---|
i32.load |
0x28 |
0x34567890 |
int32 |
i64.load |
0x29 |
0xABCDEF1234567890 |
int64 |
f32.load |
0x2A |
0x34567890 |
float32 |
f64.load |
0x2B |
0xABCDEF1234567890 |
float64 |
i32.load8_s |
0x2C |
0x90 |
int8 |
i32.load8_u |
0x2D |
0x90 |
uint8 |
i32.load16_s |
0x2E |
0x7890 |
int16 |
i32.load16_u |
0x2F |
0x7890 |
uint16 |
i64.load8_s |
0x30 |
0x90 |
int8 |
i64.load8_u |
0x31 |
0x90 |
uint8 |
i64.load16_s |
0x32 |
0x7890 |
int16 |
i64.load16_u |
0x33 |
0x7890 |
uint16 |
i64.load32_s |
0x34 |
0x34567890 |
int32 |
i64.load32_u |
0x35 |
0x34567890 |
uint32 |
store
store
指令从栈顶弹出操作数,然后写入内存。具体如何解释操作数,以及写入多少字节,因指令而异。所有的store
指令也都带有两个立即数,含义和 load
指令一样。和load
指令不同的是,store
指令要从栈顶弹出两个操作数,一个用于计算内存地址,另一个是要写入的数据。以i64.store
指令(操作码0x37
)为例,下面是它的示意图:
bytecode:
...][ i64.store ][ align ][ offset ][...
stack:
| | | |
| e(i64) |➚ | |
| d(i32) |➚ | | # m[offset+d]=e
| c | | c |
| b | | b |
| a | | a |
└───────────┘ └───────────┘
store
指令一共有9条,为了统一说明这些指令,我们也假设指令执行时计算出的内存地址是a
。下表给出这9条指令的操作码、栈顶操作数以及实际执行效果(Go伪代码,mem
表示内存,LE
表示小端编码后的字节数组):
Instruction | Opcode | Top Operand | Effect |
---|---|---|---|
i32.store |
0x36 |
0x34567890 |
mem[a: a+4] = LE(0x34567890) |
i64.store |
0x37 |
0xABCDEF1234567890 |
mem[a: a+8] = LE(0xABCDEF1234567890) |
f32.store |
0x38 |
0x34567890 |
mem[a: a+4] = LE(0x34567890) |
f64.store |
0x39 |
0xABCDEF1234567890 |
mem[a: a+8] = LE(0xABCDEF1234567890) |
i32.store8 |
0x3A |
0x34567890 |
mem[a: a+1] = LE(0x90) |
i32.store16 |
0x3B |
0x34567890 |
mem[a: a+2] = LE(0x7890) |
i64.store8 |
0x3C |
0xABCDEF1234567890 |
mem[a: a+1] = LE(0x90) |
i64.store16 |
0x3D |
0xABCDEF1234567890 |
mem[a: a+2] = LE(0x7890) |
i64.store32 |
0x3E |
0xABCDEF1234567890 |
mem[a: a+4] = LE(0x34567890) |
*本文由CoinEx Chain开发团队成员Chase撰写。CoinEx Chain是全球首条基于Tendermint共识协议和Cosmos SDK开发的DEX专用公链,借助IBC来实现DEX公链、智能合约链、隐私链三条链合一的方式去解决可扩展性(Scalability)、去中心化(Decentralization)、安全性(security)区块链不可能三角的问题,能够高性能的支持数字资产的交易以及基于智能合约的Defi应用。