VM与Opcodes
以太坊设计EVM的核心初衷:
V神:以太坊设计原理
EVM规范比其他许多虚拟机简单的多,因为其他虚拟机为复杂性付出的代价更小,也就是说它们更容易变得复杂;然而,在我们的方案中每额外增加一点复杂性,都会可能造成潜在的安全缺陷,比如造成共识失败,这就让我们的复杂性成本很高,因而不容易造成复杂;
EVM更加专业化,如支持32字节;
EVM没有复杂的外部依赖,复杂的外部依赖会导致我们安装失败,以太坊虚拟机运行在沙盒环境中,智能合约代码可在以太坊虚拟机内部运行并对外完全隔离,一定程度上提升了安全性;
完善的审查机制,可以具体到特殊的安全需求。
①概念理解
Ethereum Virtual Machine 是所有以太坊帐户和智能合约依存的环境。从一个区块到另一个区块,EVM作为执行层,依照ETH协议层,计算了新的有效状态的规则。区别于普通的计算机(键盘、网络、鼠标等输入和显示器、打印机、网络等输出),它的输入输出很少。
输入:
另一个VM传来的函数调用所携带的数据
EOA调用合约账户所携带的交易数据
输出
运行过程中修改的区块链账户的存储(Account State Storage)
日志Log
此外,如前文所述,在链上任何给定的区块处,以太坊有且只有一个“规范”状态,EVM 作为一个堆栈机运行的执行层,具有“确定性”的特点。
当输入确定的时候,EVM的输出无论重复多少次运行也是唯一不变的输出。所以在以太坊虚拟机中寻找一个随机源是很困难的。
合约调用合约的底层:
以太坊中,执行代码的一台虚拟机可以启动另一台虚拟机来执行部分指令。这两台虚拟机都将在一个全节点上运行,形成多线程并行。这称之为“合约调用合约”。
如:合约A在执行计算时候需要调用 SafeMath 之类的安全数学计算库,而该库早已用合约的形式部署在以太坊网络上。 则该合约A可以通过直接调用SafeMath库合约为自己服务。
在调用时发送方将发出 CALL的虚拟机指令,并将环境变量例如
msg.sender
(此时为EOA)等设置好。启动另一台虚拟机运行被调用方的代码,得出结果后通过
RETURN
虚拟机指令发还给调用方的内存区,完成调用过程。
gas消耗:
②源码分析
Stack
Memory
EVM的数据结构与初始化:
EVM解释器Interpreter的创建初始化流程,主要是对Opcodes根据不同分叉版本的适配管理:
回到EVM的初始化入口。
Ethereum的虚拟机源码所有部分在core/vm下。EVM的调用的入口在core/state_transition.go
目录中
合约部署:
evm.Create() => evm.interpreter.Run()
到了这里整个部署合约流程就完成了,回到
evm.Create
函数中可以看到了当run执行完成后会把runtime code最终设置到合约地址名下(opcodes会执行codeCopy
指令后把runtime code从内input data加载到内存并返回),整个合约部署就算完成了。合约调用:
evm.Call() => evm.interpreter.Run()
调用智能合约和部署合约,在EVM看来就是交易的to地址不在
nil
而是一个具体的合约地址。 同时input data不再是整个合约编译后的字节码了而是调用函数和对应的实参组合。 这里就涉及到另一个东西那就是abi的概念(可查看编码方式章节)。
2)字节码Opcodes指令
①源码分析
EVM的操作码和其他汇编语言的指令码类似。 只是一般的CPU是基于寄存器的哈弗架构或者冯诺依曼架构。 EVM是基于栈式结构,大端序(数据的低位字节存放在内存的高位地址,高位字节存放在低位地)的256bit的虚拟机。 每一个字节码是一个字节。
jump_table
是一个 [256]*operation
的数据结构。每个下标对应了一种指令,使用operation来存储了指令对应的处理逻辑、gas消耗、 堆栈验证方法、memory使用的大小等功能,数据结构operation
存储了一条指令的所需要的属性和方法。
②Opcodes学习(结合Solidity)
Opcodes的定义可见:
core/vm/opcodes.go
Opcodes指令集:opcode的长度为1个字节也就是最多支持256种opcode,现在EVM已使用140种(2022.12)
知识点实例一:内存数据长度(length)
对于动态数组如bytes/string类型,solidity编译时,data指针指向的是内存数据块的长度,而紧接着的data+0x20指针指向的是内存数据库开始位置处,故这里作了
add(data, 0x20)
处理
知识点实例二:内存布局(layout in memory)与空闲指针(Free Memory Pointer)
Solidity的内存布局保留了4个32字节的插槽:
0x00 - 0x3f (64 bytes): scratch space:哈希计算方法的预留空间,以便在inline assembly可以使用方法
0x40 - 0x5f (32 bytes): free memory pointer
0x60 - 0x7f (32 bytes): zero slot:被用作动态内存数组的初始值,永远不能被写入
可以看到,0x40是solidity为freeMemoryPointer预留的位置。值0x80只是在4个保留的32字节的插槽之后第一个可写的位置。
Free Memory Pointer是一个指向自由内存开始位置的指针。它确保智能合约知道已写入和未写入的内存位置。 这可以防止合约覆盖一些已经分配给另一个变量的内存。 当一个变量被写入内存时,合约将首先参考Free Memory Pointer,以确定数据应该被存储在哪里。 然后,它更新Free Memory Pointer,指出有多少数据将被写入新的位置。这两个值的简单相加将产生新的自由内存的起始位置。
freeMemoryPointer + dataSizeBytes(数据大小) = newFreeMemoryPointer
Solidity 通过free Memory Pointer管理内存,如果要分配内存,需从此指针指向的位置开始使用内存并更新内存
1、初始化:
实际上说明了free Memory Pointer在内存中位于memory中的0x40(十进制:64)位置,其值为0x80(十进制128)。
2、使用
读取当前free pointer memory指向的位置
本次使用
code
大小过后,计算并更新free pointer memory下一个要指向的值
知识点实例三:内存拓展(memory expansion)
(32字节的数据为右对齐)
当合约写内存时,你必须为所写的字节数付费。如果写到一个以前没有被写过的内存区域,那么第一次使用该区域会有一个额外的内存扩展费用。 所以,当写到以前未使用的内存空间时,EVM会直接讲内存以32字节(256位)的增量进行扩展。
当再写入一个单字节数据0x22,即使MSTORE8写到内存中后,结果也是如图
知识点实例四:负整数(Negative integers)与溢出管理
负整数通常使用二进制补码的方式表示。 int8 编码类型的值
-1
将全部为 11111 1111
。ABI 用 1 填充负整数,因此
-1
将被填充为:
ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
注意:小的负数大部分是 1,这会花费很多 gas
EVM在处理溢出问题时直接对溢出位进行舍弃,对结果进行取模(默认modulo为2^256 )
知识点实例五:Opcodes执行环境
当 EVM 执行智能合约时,将为其创建context执行上下文环境。context由几个内存区域组成,每个区域都有自己的用途,如下:
The Code:
account code的存储区域,它是持久性的,是帐户属性的一部分(对应accountState的字段)。在智能合约执行期间,EVM 将读取、解释和执行这些字节码。code是只读的,但可以使用指令
CODESIZE
和CODECOPY
读取该区域。其他合约也可以用EXTCODESIZE
和EXTCODECOPY
读取。EOA该区域为空。
The Stack:
一个包含32字节*1024元素的列表。堆栈用于放置指令所需的参数及其结果值。当一个新的值被放到堆栈上时,它被放到顶部,并且指令只使用顶部的值(前文也多次详述)。
The Memory:
一个只在智能合约执行期间存在的字节数组,以32字节为单位扩展,以通过
offset
(字节偏移量) 访问。每个字节初始化为0,但不管是否使用,大小将按访问的最大值计数。通常使用MLOAD
和MSTORE
指令来读写,但也被其他指令如CREATE
或EXTCODECOPY
使用。
The Storage:
对应Solidity动态类型的存储规则
map:存储于第position位置slot的map,其value的存储位置默认为
slot[keccak256(bytes32(key) + bytes32(position))]
,因为每个slot只有 32 个字节,如果value超过32字节(比如struct),超出的部分存储于上述keccak计算结果+1(以32字节为单位递增)的位置slice:原slot[position]位置存储slice.length,下标0的元素存储于
slot[keccak256(bytes32(position))]
,下标index存储于slot[keccak256(bytes32(position)) + 1]
智能合约的持久化存储区域,对应account Storage Trie中每个账户的storage空间。它是32字节到32字节值的映射,保留写入的每个值直到将其设置为0或
SELFDESTRUCT
。读取未设置过的键也会返回0。通过SLOAD
和SSTORE
指令读写。此外,storage在EVM中存储时存在32字节紧打包优化
The Calldata:
存储的是跟随transaction一起发送的数据。在创建合约时,它是存储creation code (构造函数代码)。同样该区域是不可改变的,可以通过使用
CALLDATASIZE
的CALLDATALOAD
和CALLDATaCOPY
指令来读取。
The Return Data:
存储智能合约在调用后返回的值。将存储通过
RETURN
和REVERT
指令调用外部合约后,外部合约的返回值。也可以在本合约通过使用RETURNDATASIZE
和RETURNDATACOPY
指令读取。
知识点实例六:预编译合约(Precompiled Contract)
core/vm/contracts.go
EVM 通过预编译合约提供了一组更高级的功能,并且可以避免常用的复杂计算所带来的代价。预编译合约是一种特殊的合约,有固定的地址和固定的gas消耗(合约自身内部代码执行的消耗,并不考虑 CALL
行为本身所带来的gas消耗)。其在opcodes中的使用方式、调用方式如普通合约一样,通过如 CALL
这样的指令 。
另外在输入参数方面,对于所有预编译合约,如果输入比预期的要短,则默认用0填充。如果输入比预期的长,则忽略末尾多余的字节。另外在 Berlin分叉 之后,所有预编译的合约地址总是被认为是 'warm' (access set)。
注:Solidity中的
keccak256
是EVM内部实现的SHA3系列,未通过预编译合约形式其它两个哈希算法虽然没怎么用到,但源于以太坊一开始是基于比特币设计的,黄皮书中有定义,故而保留(注:SHA256属于SHA2-family)
哈希算法的三个特性:
唯一性:输入任意内容可输出定长的内容,相同的输入一定会产出相同的输出,抗碰撞;
雪崩效应:即使一个极小的改变都会产生几乎完全不同的哈希值;
单向性:无法反向推导出pre-image(被哈希的内容的)。
0x01
ecRecover
通过signed transaction hash
及签名v,r,s
,进行椭圆曲线数字签名算法(ECDSA)公钥恢复signer address
,gas不足返回值为空
txhash,v,r,s
address
3000
0x02
SHA2-256
SHA256哈希算法(Bitcoin使用,属于SHA2系列),产生256位输出,gas不足返回值为空,对应Solidity中sha256
data
hash
60+
0x03
RIPEMD-160
RIPEMD-160哈希算法 (Bitcoin使用),产生160位输出,gas不足返回值为空,对应Solidity中ripemd160
data
hash
600+
0x04
identity
返回输入的data
,通常用于复制内存块,gas不足返回值为空
data
data
15+
0x05
modexp
计算 $B^{E}\ mod \ M$ (B/E/Msize
为对应值所占字节大小)的任意精度指数,E为0固定返回1,M为1固定返回0,gas不足返回值为空
Bsize,Esize,Msize,B,E,M
value
200+
0x06
ecAdd
椭圆曲线'alt_bn128'上两点 (x,y) 的相加(ADD),无穷远点的 x 和 y 均为0,入参无效或gas不足返回值为空
x1,y1,x2,y2
x,y
150+
0x07
ecMul
BN128椭圆曲线上点与标量相乘(MUL),s为标量scalar,无穷远点的 x 和 y 均为0
x1,y1,s
x,y
6000+
定义类知识点:
Empty Account:如果帐户的balance为0,nonce 为0且没有code,则定于该帐户为空账户
Intrinsic Gas:每笔transaction的 “基本花销” 为21000 gas。在该基础上,部署一个contract需要花费 “基本花销” 32000 gas。之后,对于calldata,每0字节花费4gas,非0则花费16gas( Istanbul分叉之前是64gas)。这些费用需在执行任何opcodes或transfer之前被支付。
Gas Refund:部分opcodes可以触发gas refund,从而降低交易的gas成本。然而,gas refund是在一笔transaction最后执行,这意味着transaction仍然需要足够的gas来运行完成(就好像不存在gas refund一样)。
此外,可以退还的gas数量也是有限的,不能超过整个transaction成本的一半(London分叉前),现在不能超过五分之一。并且从London分叉开始,只有
SSTORE
可能会触发gas refund,在此之前,SELFDESTRCT 也可以。Access Set:Berlin分叉后出现的概念,存在于access set的地址标识为'warm',不存在则标识为'cold',一些opcodes的动态开销与其有关。access set与每笔transaction绑定(而不是调用context)。access set中存在两个变量如下:
touched_addresses:存储一组当前transaction中被访问过的contract address。它被初始化为sender、receiver (CA/EOA)和预编译合约。当操作码访问access set中不存在的地址时,它会将其添加到集合中。相关的操作码为
EXTCODESIZE
、EXTCODECOPY
、EXTCODEHASH
、BALANCE
、CALL
、CALLCODE
、DELEGATECALL
、STATICCCALL
、CREATE
、CREATE2
和SELFDESTRUCT
。touched_storage_slots: 存储一组已访问的contract address及其slot key。它被初始化为空。当操作码访问access set中不存在的slot时,它会将其添加到其中。相关的操作码为
SLOAD
和SSTORE
注:如果发生context revert,access set也会恢复到它们在该context之前的状态
0x00
STOP
结束合约执行并退出
-
-
0
0x01
ADD
(u)int256,取模$2^{256}$
a,b
(a+b)
3
0x02
MUL
(u)int256,取模$2^{256}$
a,b
(a*b)
5
0x03
SUB
(u)int256,取模$2^{256}$
a,b
(a-b)
3
0x04
DIV
整除,uint256除法
a,b
(a/b)
5
0x05
SDIV
整除,int256除法
a,b
(a/b)
5
0x06
MOD
uint256,取模$2^{256}$
a,b
(a%b)
5
0x07
SMOD
int256,取模$2^{256}$
a,b
(a%b)
5
0x08
ADDMOD
(u)int256加法,取模N
a,b,N
(a+b)%N
8
0x09
MULMOD
(u)int256乘法,取模N
a,b,N
(a*b)%N
8
0x0a
EXP
uint256指数结果,取模$2^{256}$
a,exp
a**exp
10+
0x0b
SIGNEXTEND
把x
解释为b+1(0 <= b
<= 31)字节有符号整数(二进制补码形式),然后把x的符号位复制填充,至扩展输出为32字节
b,x
y
5
0x0c
- 0x0f
-
Unused
-
-
-
0x10
LT
uint256小于比较,满足返回1,不满足返回0
a,b
a<b
3
0x11
GT
uint256大于比较,满足返回1,不满足返回0
a,b
a>b
3
0x12
SLT
int256(补码)小于比较,满足返回1,不满足返回0
a,b
a<b
3
0x13
SGT
int256(补码)小于比较,满足返回1,不满足返回0
a,b
a>b
3
0x14
EQ
(u)int256相等比较,满足返回1,不满足返回0
a,b
a==b
3
0x15
ISZERO
(u)int256零比较,满足返回1,不满足返回0
a
a==0
3
0x16
AND
256位的位与计算
a,b
a&b
3
0x17
OR
256位的位或计算
a,b
`a
b`
0x18
XOR
256位的异或计算
a,b
a^b
0x19
NOT
256位的位取反计算
a
~a
3
0x1a
BYTE
返回(u)int256 x
从最高字节开始的第i
字节:y=(x>>(248-i*8)) &0xFF
i,x
y
3
0x20
SHA3
从memory偏移offset
的位置加载size
的值作为入参,计算keccak256哈希
offset,size
hash
30+
0x21
- 0x2f
-
Unused
-
-
-
0x30
ADDRESS
获取当前执行合约的地址
-
address(this)
2
0x32
ORIGIN
获取交易发起方EOA的地址
-
tx.origin
2
0x33
CALLER
回去消息调用方地址
-
msg.sender
2
0x34
CALLVALUE
获取以wei为单位的消息调用携带金额
-
msg.value
0x35
CALLDATALOAD
读取calldata
(16进制表示的字节{偶数个})偏移i
字节,注意:若calldata不足32字节会右侧补0
i
calldata[i:]
3
0x36
CALLDATASIZE
返回以字节为单位的消息数据j长度
-
size(calldata)
2
0x37
CALLDATACOPY
拷贝calldata
偏移offset
字节的数据至偏移destOffset
的memory位置
destOffset,offset,size
-
3+
0x38
CODESIZE
返回以字节为单位的,当前环境执行合约 (context中的code区域,实际即总字节码长度) 的代码(字节码)长度
-
size(address(this).code)
2
0x39
CODECOPY
拷贝address(this).code
偏移offset
字节的size
大小的数据至偏移destOffset
的memory位置
destOffset,offset,size
-
3+
0x3a
GASPRICE
返回当前执行交易的单位gas价格,以wei为单位
-
tx.gasprice
2
0x3c
EXTCODECOPY
拷贝指定address
字节码偏移offset
字节的size
大小的数据至偏移destOffset
的memory位置,动态gas(依据access sets)
address,destOffset,offset,size
-
100+
0x3d
RETURNDATASIZE
返回最后一个外部调用(如call、delegatecall...)返回的数据(return data
有专门的return value区域,并非如普通返回值在stack中)的长度,以字节为单位。EIP 211
-
size(return data)
2
0x3e
RETURNDATACOPY
拷贝return data
偏移offset
字节的size
大小的数据至偏移destOffset
的memory位置。EIP 211
destOffset,offset,size
-
3+
0x3f
EXTCODEHASH
返回指定address
的code
字节码的哈希,EIP 1052,动态gas(依据access sets)
address
hash(address(this).code)
100+
0x40
BLOCKHASH
获得指定blockNumber
的哈希,仅适用于最近的256个区块且不包括当前区块
blockNumber
blockhash(blockNumber)
20
0x41
COINBASE
获取当前区块的矿工的地址
-
block.coinbase
2
0x42
TIMESTAMP
获取当前区块的UNIX时间戳,以秒为单位
-
block.timestamp
2
0x43
NUMBER
获取当前区块号
-
block.number
2
0x44
DIFFICULTY
获取当前区块难度
-
block.difficulty
2
0x45
GASLIMIT
获取当前区块GAS上限
-
block.gaslimit
2
0x47
SELFBALANCE
获取当前环境下,执行账户address
的余额,Wei为单位 (对比BALANCE消耗更少的GAS)
-
address(this).balance
5
0x49
- 0x4f
-
Unused
-
-
0x50
POP
弹出栈顶(u)int256值并丢弃
-
-
2
0x51
MLOAD
从memory偏移offset
个字节的位置读取一个(u)int256到stack(值前面的0会舍弃),动态gas(会触发memory expansion,根据其判断)
offset
value
3+
0x52
MSTORE
向memory偏移offset
个字节的位置,写入一个(u)int256,动态gas(依据memory expansion)
offset,value
-
3+
0x53
MSTORE8
向memory偏移offset
个字节的位置,写入一个(u)int8,动态gas(依据memory expansion)
offset,value
-
3+
0x56
JUMP
无条件跳转,即改变执行环境code中PC 至偏移counter
字节的位置,跳转地点必须对应为JUMPDEST指令
counter
-
8
0x57
JUMPI
条件跳转,如果b
不等于0,改变执行环境code中PC至偏移counter
字节的位置。否则PC按正常顺序线性增加,跳转地点必须对应为JUMPDEST指令
counter,b
-
10
0x58
PC
一个指向字节码中下一个操作码的指针,由 EVM 执行。它是一个非负整数,实际是字节码中的字节偏移数。获取的是 “执行当前指令增量之前” (即不包含此次PC指令)的PC值。
-
counter
2
0x59
MSIZE
获取当前合约执行环境下的current memory (因为存在memory expansion) 大小,以字节为单位
-
size
2
0x5a
GAS
返回当前剩余的GAS
-
gasleft()
2
0x5b
JUMPDEST
为跳转指令(JUMP/JUMPI)标记一个有效的目的地
-
-
1
0x5c
- 0x5f
Unused
-
0x60
PUSH1
将1字节的值压入栈顶,该系列指令后面紧跟待压入的数据如:PUSH1 FF
-
value
3
0x61
PUSH2
将2字节的值压入栈顶
-
value
3
0x62
PUSH3
将3字节的值压入栈顶
-
value
3
0x63
PUSH4
将4字节的值压入栈顶
-
value
3
0x64
PUSH5
将5字节的值压入栈顶
-
value
3
0x65
PUSH6
将6字节的值压入栈顶
-
value
3
0x66
PUSH7
将7字节的值压入栈顶
-
value
3
0x67
PUSH8
将8字节的值压入栈顶
-
value
3
0x68
PUSH9
将9字节的值压入栈顶
-
value
3
0x69
PUSH10
将10字节的值压入栈顶
-
value
3
0x6a
PUSH11
将11字节的值压入栈顶
-
value
3
0x6b
PUSH12
将12字节的值压入栈顶
-
value
3
0x6c
PUSH13
将13字节的值压入栈顶
-
value
3
0x6d
PUSH14
将14字节的值压入栈顶
-
value
3
0x6e
PUSH15
将15字节的值压入栈顶
-
value
3
0x6f
PUSH16
将16字节的值压入栈顶
-
value
3
0x70
PUSH17
将17字节的值压入栈顶
-
value
3
0x71
PUSH18
将18字节的值压入栈顶
-
value
3
0x72
PUSH19
将19字节的值压入栈顶
-
value
3
0x73
PUSH20
将20字节的值压入栈顶
-
value
3
0x74
PUSH21
将21字节的值压入栈顶
-
value
3
0x75
PUSH22
将22字节的值压入栈顶
-
value
3
0x76
PUSH23
将23字节的值压入栈顶
-
value
3
0x77
PUSH24
将24字节的值压入栈顶
-
value
3
0x78
PUSH25
将25字节的值压入栈顶
-
value
3
0x79
PUSH26
将25字节的值压入栈顶
-
value
3
0x7a
PUSH27
将27字节的值压入栈顶
-
value
3
0x7b
PUSH28
将28字节的值压入栈顶
-
value
3
0x7c
PUSH29
将29字节的值压入栈顶
-
value
3
0x7d
PUSH30
将30字节的值压入栈顶
-
value
3
0x7e
PUSH31
将31字节的值压入栈顶
-
value
3
0x7f
PUSH32
(full word) 将32字节的值压入栈顶
-
value
3
0x80
DUP1
取stack上的第1个值(1st 栈顶)并返回至栈顶
value
value,value
3
0x81
DUP2
忽略stack上的前1个值,复制stack上的第2个值,并粘贴至栈顶
a,b
b,a,b
3
0x82
DUP3
忽略stack上的前2个值,复制stack上的第3个值,并粘贴至栈顶
a,b,c
c,a,b,c
3
0x83
DUP4
忽略stack上的前3个值,复制stack上的第4个值,并粘贴至栈顶
a,b,c,d
d,a,b,c,d
3
0x84
DUP5
忽略stack上的前4 (=n-1)个值,复制stack上的第5 (=n)个值,并粘贴至栈顶
stack[0],...stack[n-1], stack[n]
stack[n],stack[0],... stack[n]
3
0x85
DUP6
忽略stack上的前5 (=n-1)个值,复制stack上的第6 (=n)个值,并粘贴至栈顶
stack[0],...stack[n-1], stack[n]
stack[n],stack[0],... stack[n]
3
0x86
DUP7
忽略stack上的前6 (=n-1)个值,复制stack上的第7(=n)个值,并粘贴至栈顶
stack[0],...stack[n-1], stack[n]
stack[n],stack[0],... stack[n]
3
0x87
DUP8
忽略stack上的前7 (=n-1)个值,复制stack上的第8 (=n)个值,并粘贴至栈顶
stack[0],...stack[n-1], stack[n]
stack[n],stack[0],... stack[n]
3
0x88
DUP9
忽略stack上的前8 (=n-1)个值,复制stack上的第9 (=n)个值,并粘贴至栈顶
stack[0],...stack[n-1], stack[n]
stack[n],stack[0],... stack[n]
3
0x89
DUP10
忽略stack上的前9 (=n-1)个值,复制stack上的第10 (=n)个值,并粘贴至栈顶
stack[0],...stack[n-1], stack[n]
stack[n],stack[0],... stack[n]
3
0x8a
DUP11
忽略stack上的前10 (=n-1)个值,复制stack上的第11 (=n)个值,并粘贴至栈顶
stack[0],...stack[n-1], stack[n]
stack[n],stack[0],... stack[n]
3
0x8b
DUP12
忽略stack上的前11 (=n-1)个值,复制stack上的第12 (=n)个值,并粘贴至栈顶
stack[0],...stack[n-1], stack[n]
stack[n],stack[0],... stack[n]
3
0x8c
DUP13
忽略stack上的前12 (=n-1)个值,复制stack上的第13 (=n)个值,并粘贴至栈顶
stack[0],...stack[n-1], stack[n]
stack[n],stack[0],... stack[n]
3
0x8d
DUP14
忽略stack上的前13 (=n-1)个值,复制stack上的第14 (=n)个值,并粘贴至栈顶
stack[0],...stack[n-1], stack[n]
stack[n],stack[0],... stack[n]
3
0x8e
DUP15
忽略stack上的前14 (=n-1)个值,复制stack上的第15 (=n)个值,并粘贴至栈顶
stack[0],...stack[n-1], stack[n]
stack[n],stack[0],... stack[n]
3
0x8f
DUP16
忽略stack上的前15 (=n-1)个值,复制stack上的第16 (=n)个值,并粘贴至栈顶
stack[0],...stack[n-1], stack[n]
stack[n],stack[0],... stack[n]
3
0x90
SWAP1
交换栈顶stack[0]与(栈上2nd)stack[1]的值
a,b
b,a
3
0x91
SWAP2
交换栈顶stack[0]与(栈上3rd)stack[2]的值
a,b,c
c,b,a
3
0x92
SWAP3
交换栈顶stack[0]与(栈上4th)stack[3]的值
a,...,b
b,...,a
3
0x93
SWAP4
交换栈顶stack[0]与(栈上5th)stack[4]的值
a,...,b
b,...,a
3
0x94
SWAP5
交换栈顶stack[0]与(栈上6th)stack[5]的值
a,...,b
b,...,a
3
0x95
SWAP6
交换栈顶stack[0]与(栈上7th)stack[6]的值
a,...,b
b,...,a
3
0x96
SWAP7
交换栈顶stack[0]与(栈上8th)stack[7]的值
a,...,b
b,...,a
3
0x97
SWAP8
交换栈顶stack[0]与(栈上9th)stack[8]的值
a,...,b
b,...,a
3
0x98
SWAP9
交换栈顶stack[0]与(栈上10th)stack[9]的值
a,...,b
b,...,a
3
0x99
SWAP10
交换栈顶stack[0]与(栈上11th)stack[10]的值
a,...,b
b,...,a
3
0x9a
SWAP11
交换栈顶stack[0]与(栈上12th)stack[11]的值
a,...,b
b,...,a
3
0x9b
SWAP12
交换栈顶stack[0]与(栈上13th)stack[12]的值
a,...,b
b,...,a
3
0x9c
SWAP13
交换栈顶stack[0]与(栈上14th)stack[13]的值
a,...,b
b,...,a
3
0x9d
SWAP14
交换栈顶stack[0]与(栈上15th)stack[14]的值
a,...,b
b,...,a
3
0x9e
SWAP15
交换栈顶stack[0]与(栈上16th)stack[15]的值
a,...,b
b,...,a
3
0x9f
SWAP16
交换栈顶stack[0]与(栈上17th)stack[16]的值
a,...,b
b,...,a
3
0xa0
LOG0
从memory偏移offset
个字节的位置读取一个size
大小作为data,无topic,输出日志
offset,size
-
375+
0xa1
LOG1
从memory偏移offset
个字节的位置读取一个size
大小作为data,1个topic(32byte),输出日志
offset,size,topic
-
750+
0xa2
LOG2
从memory偏移offset
个字节的位置读取一个size
大小作为data,2个topic,输出日志
offset,size,topic1,topic2
-
1125+
0xa3
LOG3
从memory偏移offset
个字节的位置读取一个size
大小作为data,3个topic,输出日志
offset,size,topic1,topic2,topic3
-
1500+
0xa4
LOG4
从memory偏移offset
个字节的位置读取一个size
大小作为data,4个topic,输出日志
offset,size,topic1,topic2,topic3,topic4
-
1875+
0xa5
-0xaf
-
Unused
-
-
-
0xb0
-0xbf
-
Unused
-
-
-
0xc0
-0xcf
-
Unused
-
-
-
0xd0
-0xdf
-
Unused
-
-
-
0xe0
-0xef
-
Unused
-
-
-
0xf0
CREATE
从memory偏移offset
个字节的位置读取一个size
大小作为initialisation code来创建account,并发送value
Wei,创建失败返回0
value,offset,size
address
32000+
0xf1
CALL
向账户address
发出消息调用。argsOffset/Size制定了calldata从memory中读取的位置和大小,retOffset/Size制定了返回值存储于memory的条件。gas可用额度最多当前环境剩余gas的1/64,revert将返回0(注意,没有按预期执行目标账户code并不会revert),成功返回1
gas,address,value,argsOffset,argsSize,retOffset,retSize
success
100+
0xf2
CALLCODE
改变的是调用发起方的storage,其余功能同CALL
gas,address,value,argsOffset,argsSize,retOffset,retSize
success
100+
0xf3
RETURN
停止执行并返回从memory偏移offset
个字节的位置读取一个size
大小的return data
offset,size
-
0+
0xf4
DELEGATECALL
改变的是调用发起方的storage,msg.sender/msg.value为调用本方法的account(实际上是对CALLCODE的bugfix),无法转账,其余功能同CALL
gas,address,argsOffset,argsSize,retOffset,retSize
success
100+
0xf5
CREATE2
通过加salt
的方式,以不同的计算方式,可以在account创建成功前得到地其address,其它同CREATE
value,offset,size,salt
address
32000+
0xfa
STATICCALL
只读方法,不可修改state包括转账,即只能允许view和pure类型的函数调用,其他功能同CALL
gas,address,argsOffset,argsSize,retOffset,retSize
success
100+
0xfd
REVERT
REVERT ERROR:停止执行并回滚此次执行所改变的世界状态,返还unused gas给caller,并返回memory偏移offset
个字节位置的size
大小的return data
offset,size
-
0+
0xfe
INVALID
特指的无效指令 (等效于任何未在此目录的指令,实际上不是一个操作码),等同于REVERT(0,0)指令的效果会回滚,不同的是将消耗掉所有remaining gas,EIP141
-
-
NaN
0xff
SELFDESTRUCT
停止执行并将当前账户标记为“待销毁”,将会在本次transaction最后执行,返回当前账户的balance至address
(该行为无法被阻止,也不会报错)
address
-
5000+
③EVM最小实现:模拟与Debug
尝试手动移植可参考这里
1)EVM Toolkit(ETK)
原文链接:https://quilt.github.io/etk/
ETK是一个EVM 工具包,到目前位置,可以方便的将用mnemonic写的伪代码转化成字节码输出,同时也可以将字节码解码为mnemonic指令,核心指令为:
Assembler:
eas
汇编程序命令,将人类可读的mnemonic形式(如上文提到的)转换为 EVM 解释器期望的原始字节,以十六进制编码,并且可以配合
label
很方便的使用或编写代码。手动计算跳转目的地地址将是一项非常无意义的任务,因此Assembler支持为代码中的特定位置分配特定的
label
,下例为一个无限循环mnemonic指令Input
参数(这里是input.etk
)是mnemonic程序集文件路径,output.hex
是输出的字节码指令文件路径,以十六进制编码。如果省略了输出路径,则将汇编的指令写入标准输出(stdout)。Disassembler:
disease
反汇编命令大致与汇编程序相反,它将一串EVM十六进制字节 (如
output.hex
)或其他格式的文件解析为mnemonic指令--code, or -c
对于简短的代码片段,可以解析直接在命令行上给出的十六进制字节码指令
--bin file, or -b
将指定的二进制文件解释为mnemonic
--hex file, or -x
将指定的EVM opcodes十六进制文件解释为mnemonic
2)hEVM
原文链接:https://hevm.dev/overview.html
hEVM 是以太虚拟机(EVM)的一个实现,该虚拟机专门用于智能合同的 symbolic execution、单元测试和调试EVM 字节码的操作,最初上作为 dapptools 项目的一部分。其以下功能有助于练习以太坊字节码的使用:
通过输入字节码执行智能合约并验证是否有错误
验证两组不同字节码是否等价
可视化调试任意的 evm 字节码的执行
通过 rpc 获取以太坊state
Last updated