以太坊虚拟机EVM指令集

EVM基本信息1

编程语言虚拟机一般有两种类型,基于栈,或者基于寄存器。大部分我们所熟知的语言都采用基于栈的虚拟机,比如最著名的Java虚拟机。在游戏领域非常流行的Lua语言则采用了基于寄存器的虚拟机。和JVM一样,EVM也是基于栈的虚拟机

既然是支持栈的虚拟机,那么EVM肯定首先得有个栈。为了方便进行密码学计算,EVM采用了32字节(256比特)的字长。EVM栈以字(Word)为单位进行操作,最多可以容纳1024个字。这就是我们在这篇文章里需要了解的EVM信息,在后面的文章里,我们会逐步介绍更多的EVM细节。下面是EVM栈的示意图:

在这里插入图片描述

EVM指令集

和JVM一样,EVM执行的也是字节码。由于操作码被限制在一个字节以内,所以EVM指令集最多只能容纳256条指令。目前EVM已经定义了约142条指令,还有100多条指令可供以后扩展。这142条指令包括算术运算指令,比较操作指令,按位运算指令,密码学计算指令,栈、memory、storage操作指令,跳转指令,区块、智能合约相关指令等。我们会在后面的文章里逐步讨论这些指令,下面是已经定义的EVM操作码分布图(灰色区域是目前还没有定义的操作码):
在这里插入图片描述

EVM栈操作指令2

本文将介绍POP指令、PUSHx系列指令、DUPx系列指令、SWAPx系列指令。这些指令只对EVM栈进行单纯的操作,它们的操作码分布如下图所示:
在这里插入图片描述

POP指令

POP指令(操作码0x50)从栈顶弹出一个元素。下面是POP指令的操作示意图(白色表示元素即将发生变动):在这里插入图片描述

PUSHx指令

PUSH系列指令把紧跟在指令后面的N(1 ~ 32)字节元素推入栈顶。PUSH系列指令一共有32条,从PUSH1(操作码0x60)一直到PUSH32(操作码0x7A)。EVM是大端机器,以PUSH2指令为例,下面是该指令的操作示意图(不完整的灰色纸带表示字节码):
在这里插入图片描述

DUPx指令

DUP系列指令复制从栈顶开始数的第N(1 ~ 16)个元素,并把复制后的元素推入栈顶。DUP系列指令一共有16条,从DUP1(操作码0x80)一直到DUP16(操作码0x8A)。比如DUP1指令复制栈顶元素,如下图所示:
在这里插入图片描述
下面是DUP2指令的操作示意图:
在这里插入图片描述

SWAPx指令

SWAP系列指令把栈顶元素和从栈顶开始数的第N(1 ~ 16)+ 1 个元素进行交换。SWAP系列指令一共有16条,从SWAP1(操作码0x90)一直到SWAP16(操作码0x9A)。比如SWAP1指令交换位于栈顶的两个元素,如下图所示:
在这里插入图片描述
下面是SWAP2指令的操作示意图:
在这里插入图片描述

SSTORE和SLOAD

存储storage是一个键值存储,可将256位字映射到256位字。使用SSTORE / SLOAD指令访问。存储中的所有位置最初都定义为零。

SSTORE

我们用[]符号来标识栈:
// 空栈
stack: []
// 有3个数据的栈,栈顶项为3,栈底项为1(左侧为栈顶)
stack: [3 2 1]
用{}符号来标识合约存储器:
// 空存储
store: {}
// 数值0x1被保存在0x0的位置上
store: { 0x0 => 0x1 }

汇编代码:

0x1
0x0
sstore

可以模拟上面的3条指令,然后会发现他们的机器状态结果都是一样的:

stack: []
store: { 0x0 => 0x1 }

sstore指令:出栈两个元素,0x0出栈,接着0x0出栈,保存合约存储器中。0x0为键,0x0为值,即store: { 0x0 => 0x1 }

SLOAD

// 有3个数据的栈,栈顶项为3,栈底项为1(左侧为栈顶)
stack: [0x2]
// 数值0x1被保存在0x0的位置上
store: { 0x0 => 0x1 }

汇编代码:

0x5
0x0
SLOAD

模拟上面的3条指令,执行结果如下:

stack: [0x5 0x2] (左侧为栈顶)
stack: [0x0 0x5 0x2] (左侧为栈顶)
SLOAD  store: { 0x0 => 0x1 }
stack: [0x1 0x5 0x2] (左侧为栈顶)

SLOAD指令:先取出栈顶元素x,然后在storage中取以x为键的值(storage[x])存入栈顶。

EVM算术运算指令3

EVM总共定义了11条算术运算指令,见下表:
在这里插入图片描述
这些指令从栈顶弹出两到三个元素,进行相应计算,然后把结果推入栈顶。参与计算的元素和结果均被解释为按二的补码编码的整数。如果计算结果(假设为x)溢出(超出2^256),则最终的结果x’取值x % 2^256(%表示取模运算,^表示指数运算)。

下面是算术运算指令的操作码分布图:
在这里插入图片描述

ADD、MUL、SUB、DIV、SDIV、MOD、SMOD、EXP

这8条指令操作方式比较类似,从栈顶弹出两个元素,进行计算,然后把计算结果推入栈顶由于采用二的补码表示整数时,加法、减法和乘法运算不用考虑符号位,所以加法、减法和乘法运算都只有一条指令。整除和取模运算需要考虑符号位,所以各有两条指令。指数运算只操作无符号整数。以ADD指令为例,下面是它的操作示意图:
在这里插入图片描述

ADDMOD和MULMOD

MULMOD指令依次从栈顶弹出x、y、z三个数,先计算x和y的乘积(不受溢出限制),再计算乘积和z的模,最后把结果推入栈顶。假定乘积不会溢出,那么MULMOD(x, y, z)等价于x * y % z,下面是MULMOD指令的操作示意图:
在这里插入图片描述
ADDMOD指令和MULMOD指令类似,只不过把乘法换成了加法。下面是ADDMOD指令的操作示意图:
在这里插入图片描述

SIGNEXTEND

SIGNEXTEND指令从栈顶依次弹出k和x,并把x解释为k+1(0 <= k <= 31)字节有符号整数,然后把x符号扩展至32字节。比如x是二进制10000000,k是0,则符号扩展之后,结果为二进制1111…10000000(共249个1)。下面是SIGNEXTEND指令的操作示意图:
在这里插入图片描述

EVM按位运算指令4

EVM定义了8条按位运算指令,见下表:
在这里插入图片描述
下面是按位运算指令的操作码分布图:
在这里插入图片描述

AND、OR、XOR、NOT

AND、OR、XOR指令从栈顶弹出两个元素,进行按位运算,然后把结果推入栈顶。以AND指令为例,下面是它的操作示意图:
在这里插入图片描述
NOT指令将栈元素按位取反,下面是它的操作示意图:
在这里插入图片描述

BYTE

BYTE指令先后从栈顶弹出n和x,取x的第n个字节并推入栈顶。由于EVM的字长是32个字节,所以n在[0, 31]区间内才有意义,否则BYTE的运算结果就是0。另外,字节是从左到右数的,因此第0个字节占据字的最高位8个比特。以n=1为例,下面是BYTE指令操作示意图:
在这里插入图片描述

SHL、SHR、SAR

这三条指令都是先后从栈顶弹出两个数n和x,其中x是要进行位移操作顶数,n是位移比特数,然后把结果推入栈顶。以左移指令SHL为例,下面是它的操作示意图:
在这里插入图片描述
SHRSAR的区别在于,前者执行逻辑右移(空缺补0)后者执行算术右移(空缺补符号位)。下表总结了这三条位移指令对于操作数的解释,以及计算结果(这里^表示指数运算):
在这里插入图片描述
Solidity语言提供了<<>>运算符,下表总结了这两个运算符的含义(这里**表示指数运算):

运算符 解释
x << n x * 2**n
x >> n x / 2**n

EVM比较操作指令5

VM定义了6条比较操作指令,见下表:
在这里插入图片描述
下面是比较操作指令的操作码分布图:
在这里插入图片描述

LT、GT、SLT、SGT、EQ

这5条指令都是从栈顶弹出两个元素,进行比较,然后把结果(1表示true,0表示false)推入栈顶。其中LT和GT把弹出的元素解释为无符号整数进行比较,SLT和SGT把弹出的元素解释为有符号数进行比较,EQ不关心符号。以LT指令为例,下面是它的操作示意图:
在这里插入图片描述

ISZERO

SZERO指令从栈顶弹出一个元素,判断它是否为0,如果是,则把1推入栈顶,否则把0推入栈顶。下面是ISZERO指令的操作示意图:
在这里插入图片描述


  1. 以太坊虚拟机介绍 ↩︎

  2. 以太坊虚拟机介绍2-栈操作指令 ↩︎

  3. 以太坊虚拟机介绍3-算术运算指令 ↩︎

  4. 以太坊虚拟机介绍4-按位运算指令 ↩︎

  5. 以太坊虚拟机介绍5-比较操作指令 ↩︎

猜你喜欢

转载自blog.csdn.net/weixin_43405220/article/details/100280818