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(被哈希的内容的)。
Address | Name | Description | Input(默认从左向右为:栈顶 => 向下) | Output | Gas |
---|---|---|---|---|---|
| ecRecover | 通过 |
|
| 3000 |
| SHA2-256 | SHA256哈希算法(Bitcoin使用,属于SHA2系列),产生256位输出,gas不足返回值为空,对应Solidity中 |
|
| 60+ |
| RIPEMD-160 | RIPEMD-160哈希算法 (Bitcoin使用),产生160位输出,gas不足返回值为空,对应Solidity中 |
|
| 600+ |
| identity | 返回输入的 |
|
| 15+ |
| modexp | 计算 $B^{E}\ mod \ M$ ( |
|
| 200+ |
| ecAdd | 椭圆曲线'alt_bn128'上两点 (x,y) 的相加(ADD),无穷远点的 x 和 y 均为0,入参无效或gas不足返回值为空 |
|
| 150+ |
| ecMul | BN128椭圆曲线上点与标量相乘(MUL),s为标量scalar,无穷远点的 x 和 y 均为0 |
|
| 6000+ |
| ecPairing | BN128椭圆曲线的双线性函数配对操作,将用于zk-SNARK验证,入参无效或gas不足返回值为空 |
|
| 45000+ |
| blake2f | 实现blake2哈希函数,并在 BLAKE2 哈希算法中使用压缩函数 F ,使用的次数 |
|
| 0+ |
定义类知识点:
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之前的状态
Opcode | Mnemonic | Description | Input(默认从左向右为:栈顶 => 向下) | Output | Gas |
---|---|---|---|---|---|
| STOP | 结束合约执行并退出 | - | - | 0 |
| ADD | (u)int256,取模$2^{256}$ |
|
| 3 |
| MUL | (u)int256,取模$2^{256}$ |
|
| 5 |
| SUB | (u)int256,取模$2^{256}$ |
|
| 3 |
| DIV | 整除,uint256除法 |
|
| 5 |
| SDIV | 整除,int256除法 |
|
| 5 |
| MOD | uint256,取模$2^{256}$ |
|
| 5 |
| SMOD | int256,取模$2^{256}$ |
|
| 5 |
| ADDMOD | (u)int256加法,取模N |
|
| 8 |
| MULMOD | (u)int256乘法,取模N |
|
| 8 |
| EXP | uint256指数结果,取模$2^{256}$ |
|
| 10+ |
| SIGNEXTEND | 把 |
|
| 5 |
| - | Unused | - | - | - |
| LT | uint256小于比较,满足返回1,不满足返回0 |
|
| 3 |
| GT | uint256大于比较,满足返回1,不满足返回0 |
|
| 3 |
| SLT | int256(补码)小于比较,满足返回1,不满足返回0 |
|
| 3 |
| SGT | int256(补码)小于比较,满足返回1,不满足返回0 |
|
| 3 |
| EQ | (u)int256相等比较,满足返回1,不满足返回0 |
|
| 3 |
| ISZERO | (u)int256零比较,满足返回1,不满足返回0 |
|
| 3 |
| AND | 256位的位与计算 |
|
| 3 |
| OR | 256位的位或计算 |
| `a | b` |
| XOR | 256位的异或计算 |
|
| |
| NOT | 256位的位取反计算 |
|
| 3 |
| BYTE | 返回(u)int256 |
|
| 3 |
| SHL | 256位左移位,新位置0:EIP145 |
|
| 3 |
| SHR | 256位右移,新位置0:EIP145 |
|
| 3 |
| SAR | 考虑符号位的256右移位,新位符号位保持,其他位置0:EIP145 |
|
| 3 |
| SHA3 | 从memory偏移 |
|
| 30+ |
| - | Unused | - | - | - |
| ADDRESS | 获取当前执行合约的地址 | - |
| 2 |
| BALANCE | 获取指定地址的余额,单位wei,地址不存在返回0,动态gas(依据access sets) |
|
| 100+ |
| ORIGIN | 获取交易发起方EOA的地址 | - |
| 2 |
| CALLER | 回去消息调用方地址 | - |
| 2 |
| CALLVALUE | 获取以wei为单位的消息调用携带金额 | - |
| |
| CALLDATALOAD | 读取 |
|
| 3 |
| CALLDATASIZE | 返回以字节为单位的消息数据j长度 | - |
| 2 |
| CALLDATACOPY | 拷贝 |
| - | 3+ |
| CODESIZE | 返回以字节为单位的,当前环境执行合约 (context中的code区域,实际即总字节码长度) 的代码(字节码)长度 | - |
| 2 |
| CODECOPY | 拷贝 |
| - | 3+ |
| GASPRICE | 返回当前执行交易的单位gas价格,以wei为单位 | - |
| 2 |
| EXTCODESIZE | 获取指定 |
|
| 100+ |
| EXTCODECOPY | 拷贝指定 |
| - | 100+ |
| RETURNDATASIZE | 返回最后一个外部调用(如call、delegatecall...)返回的数据( | - |
| 2 |
| RETURNDATACOPY | 拷贝 |
| - | 3+ |
| EXTCODEHASH | 返回指定 |
|
| 100+ |
| BLOCKHASH | 获得指定 |
|
| 20 |
| COINBASE | 获取当前区块的矿工的地址 | - |
| 2 |
| TIMESTAMP | 获取当前区块的UNIX时间戳,以秒为单位 | - |
| 2 |
| NUMBER | 获取当前区块号 | - |
| 2 |
| DIFFICULTY | 获取当前区块难度 | - |
| 2 |
| GASLIMIT | 获取当前区块GAS上限 | - |
| 2 |
| CHAINID | 获取当前区块的chainId,EIP 1344 | - |
| 2 |
| SELFBALANCE | 获取当前环境下,执行账户 | - |
| 5 |
| BASEFEE | 获取当前区块的基础gas fee,Wei为单位,EIP 3198 | - |
| 2 |
| - | Unused | - | - | |
| POP | 弹出栈顶(u)int256值并丢弃 | - | - | 2 |
| MLOAD | 从memory偏移 |
|
| 3+ |
| MSTORE | 向memory偏移 |
| - | 3+ |
| MSTORE8 | 向memory偏移 |
| - | 3+ |
| SLOAD | 从storage的 |
|
| 100+ |
| SSTORE | 向storage的 |
| - | 100+ |
| JUMP | 无条件跳转,即改变执行环境code中PC 至偏移 |
| - | 8 |
| JUMPI | 条件跳转,如果 |
| - | 10 |
| PC | 一个指向字节码中下一个操作码的指针,由 EVM 执行。它是一个非负整数,实际是字节码中的字节偏移数。获取的是 “执行当前指令增量之前” (即不包含此次PC指令)的PC值。 | - |
| 2 |
| MSIZE | 获取当前合约执行环境下的current memory (因为存在memory expansion) 大小,以字节为单位 | - |
| 2 |
| GAS | 返回当前剩余的GAS | - |
| 2 |
| JUMPDEST | 为跳转指令(JUMP/JUMPI)标记一个有效的目的地 | - | - | 1 |
| Unused | - | |||
| PUSH1 | 将1字节的值压入栈顶,该系列指令后面紧跟待压入的数据如: | - |
| 3 |
| PUSH2 | 将2字节的值压入栈顶 | - |
| 3 |
| PUSH3 | 将3字节的值压入栈顶 | - |
| 3 |
| PUSH4 | 将4字节的值压入栈顶 | - |
| 3 |
| PUSH5 | 将5字节的值压入栈顶 | - |
| 3 |
| PUSH6 | 将6字节的值压入栈顶 | - |
| 3 |
| PUSH7 | 将7字节的值压入栈顶 | - |
| 3 |
| PUSH8 | 将8字节的值压入栈顶 | - |
| 3 |
| PUSH9 | 将9字节的值压入栈顶 | - |
| 3 |
| PUSH10 | 将10字节的值压入栈顶 | - |
| 3 |
| PUSH11 | 将11字节的值压入栈顶 | - |
| 3 |
| PUSH12 | 将12字节的值压入栈顶 | - |
| 3 |
| PUSH13 | 将13字节的值压入栈顶 | - |
| 3 |
| PUSH14 | 将14字节的值压入栈顶 | - |
| 3 |
| PUSH15 | 将15字节的值压入栈顶 | - |
| 3 |
| PUSH16 | 将16字节的值压入栈顶 | - |
| 3 |
| PUSH17 | 将17字节的值压入栈顶 | - |
| 3 |
| PUSH18 | 将18字节的值压入栈顶 | - |
| 3 |
| PUSH19 | 将19字节的值压入栈顶 | - |
| 3 |
| PUSH20 | 将20字节的值压入栈顶 | - |
| 3 |
| PUSH21 | 将21字节的值压入栈顶 | - |
| 3 |
| PUSH22 | 将22字节的值压入栈顶 | - |
| 3 |
| PUSH23 | 将23字节的值压入栈顶 | - |
| 3 |
| PUSH24 | 将24字节的值压入栈顶 | - |
| 3 |
| PUSH25 | 将25字节的值压入栈顶 | - |
| 3 |
| PUSH26 | 将25字节的值压入栈顶 | - |
| 3 |
| PUSH27 | 将27字节的值压入栈顶 | - |
| 3 |
| PUSH28 | 将28字节的值压入栈顶 | - |
| 3 |
| PUSH29 | 将29字节的值压入栈顶 | - |
| 3 |
| PUSH30 | 将30字节的值压入栈顶 | - |
| 3 |
| PUSH31 | 将31字节的值压入栈顶 | - |
| 3 |
| PUSH32 | (full word) 将32字节的值压入栈顶 | - |
| 3 |
| DUP1 | 取stack上的第1个值(1st 栈顶)并返回至栈顶 |
|
| 3 |
| DUP2 | 忽略stack上的前1个值,复制stack上的第2个值,并粘贴至栈顶 |
|
| 3 |
| DUP3 | 忽略stack上的前2个值,复制stack上的第3个值,并粘贴至栈顶 |
|
| 3 |
| DUP4 | 忽略stack上的前3个值,复制stack上的第4个值,并粘贴至栈顶 |
|
| 3 |
| DUP5 | 忽略stack上的前4 (=n-1)个值,复制stack上的第5 (=n)个值,并粘贴至栈顶 |
|
| 3 |
| DUP6 | 忽略stack上的前5 (=n-1)个值,复制stack上的第6 (=n)个值,并粘贴至栈顶 |
|
| 3 |
| DUP7 | 忽略stack上的前6 (=n-1)个值,复制stack上的第7(=n)个值,并粘贴至栈顶 |
|
| 3 |
| DUP8 | 忽略stack上的前7 (=n-1)个值,复制stack上的第8 (=n)个值,并粘贴至栈顶 |
|
| 3 |
| DUP9 | 忽略stack上的前8 (=n-1)个值,复制stack上的第9 (=n)个值,并粘贴至栈顶 |
|
| 3 |
| DUP10 | 忽略stack上的前9 (=n-1)个值,复制stack上的第10 (=n)个值,并粘贴至栈顶 |
|
| 3 |
| DUP11 | 忽略stack上的前10 (=n-1)个值,复制stack上的第11 (=n)个值,并粘贴至栈顶 |
|
| 3 |
| DUP12 | 忽略stack上的前11 (=n-1)个值,复制stack上的第12 (=n)个值,并粘贴至栈顶 |
|
| 3 |
| DUP13 | 忽略stack上的前12 (=n-1)个值,复制stack上的第13 (=n)个值,并粘贴至栈顶 |
|
| 3 |
| DUP14 | 忽略stack上的前13 (=n-1)个值,复制stack上的第14 (=n)个值,并粘贴至栈顶 |
|
| 3 |
| DUP15 | 忽略stack上的前14 (=n-1)个值,复制stack上的第15 (=n)个值,并粘贴至栈顶 |
|
| 3 |
| DUP16 | 忽略stack上的前15 (=n-1)个值,复制stack上的第16 (=n)个值,并粘贴至栈顶 |
|
| 3 |
| SWAP1 | 交换栈顶stack[0]与(栈上2nd)stack[1]的值 |
|
| 3 |
| SWAP2 | 交换栈顶stack[0]与(栈上3rd)stack[2]的值 |
|
| 3 |
| SWAP3 | 交换栈顶stack[0]与(栈上4th)stack[3]的值 |
|
| 3 |
| SWAP4 | 交换栈顶stack[0]与(栈上5th)stack[4]的值 |
|
| 3 |
| SWAP5 | 交换栈顶stack[0]与(栈上6th)stack[5]的值 |
|
| 3 |
| SWAP6 | 交换栈顶stack[0]与(栈上7th)stack[6]的值 |
|
| 3 |
| SWAP7 | 交换栈顶stack[0]与(栈上8th)stack[7]的值 |
|
| 3 |
| SWAP8 | 交换栈顶stack[0]与(栈上9th)stack[8]的值 |
|
| 3 |
| SWAP9 | 交换栈顶stack[0]与(栈上10th)stack[9]的值 |
|
| 3 |
| SWAP10 | 交换栈顶stack[0]与(栈上11th)stack[10]的值 |
|
| 3 |
| SWAP11 | 交换栈顶stack[0]与(栈上12th)stack[11]的值 |
|
| 3 |
| SWAP12 | 交换栈顶stack[0]与(栈上13th)stack[12]的值 |
|
| 3 |
| SWAP13 | 交换栈顶stack[0]与(栈上14th)stack[13]的值 |
|
| 3 |
| SWAP14 | 交换栈顶stack[0]与(栈上15th)stack[14]的值 |
|
| 3 |
| SWAP15 | 交换栈顶stack[0]与(栈上16th)stack[15]的值 |
|
| 3 |
| SWAP16 | 交换栈顶stack[0]与(栈上17th)stack[16]的值 |
|
| 3 |
| LOG0 | 从memory偏移 |
| - | 375+ |
| LOG1 | 从memory偏移 |
| - | 750+ |
| LOG2 | 从memory偏移 |
| - | 1125+ |
| LOG3 | 从memory偏移 |
| - | 1500+ |
| LOG4 | 从memory偏移 |
| - | 1875+ |
| - | Unused | - | - | - |
| - | Unused | - | - | - |
| - | Unused | - | - | - |
| - | Unused | - | - | - |
| - | Unused | - | - | - |
| CREATE | 从memory偏移 |
|
| 32000+ |
| CALL | 向账户 |
|
| 100+ |
| CALLCODE | 改变的是调用发起方的storage,其余功能同CALL |
|
| 100+ |
| RETURN | 停止执行并返回从memory偏移 |
| - | 0+ |
| DELEGATECALL | 改变的是调用发起方的storage,msg.sender/msg.value为调用本方法的account(实际上是对CALLCODE的bugfix),无法转账,其余功能同CALL |
|
| 100+ |
| CREATE2 | 通过加 |
|
| 32000+ |
| STATICCALL | 只读方法,不可修改state包括转账,即只能允许view和pure类型的函数调用,其他功能同CALL |
|
| 100+ |
| REVERT | REVERT ERROR:停止执行并回滚此次执行所改变的世界状态,返还unused gas给caller,并返回memory偏移 |
| - | 0+ |
| INVALID | 特指的无效指令 (等效于任何未在此目录的指令,实际上不是一个操作码),等同于REVERT(0,0)指令的效果会回滚,不同的是将消耗掉所有remaining gas,EIP141 | - | - | NaN |
| SELFDESTRUCT | 停止执行并将当前账户标记为“待销毁”,将会在本次transaction最后执行,返回当前账户的balance至 |
| - | 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