1. 引言
本文主要源自Macro团队的Gilbert在ETHNewYork 2022分享 Demystifying EVM Opcodes,同时结合evm.codes来理解。
下图摘自Evolution of the EVM Pt. 1:
一个很赞的理解Opcodes的资料见EVM opcodes谜题:
- Learning Ethereum Virtual Machine Opcodes With EVM Puzzles(https://github.com/fvictorio/evm-puzzles javascript)
学习EVM Opcodes,可成为更好的Solidity工程师。
更好的Solidity工程师,意味着:
- 1)理解Solidity的设计原理。
- 2)更好的为low-level code做准备。
- 3)更深入的理解通用设计模式。
- 4)更深入的理解智能合约在EVM中的运行机制。
2. 何为虚拟机?
在计算机科学中,“bytecode”为一种由源代码编译而来的计算机语言,其运行在虚拟机中。bytecode不是human-readable的,但确实computer-readable的。基本的流程为:
源代码-》bytecode-》Machine code
[Flow]
Source Code -> Bytecode -> Machine Code
Source Code: File written in programming language such as Java, Solidity.
Bytecode: Compiled from source code and run on Virtual Machine such as JVM, EVM
Machine Code: Code that only operating system can read. Bytecode is converted to Machine Code and finally executed.
3. EVM介绍
3.1 EVM中的Opcode
与其它虚拟机采用二进制表示Opcode不同,为便于记忆和可读,EVM的所有Opcode都以单个字节来表示,并附加了人类可读名字。
EVM Opcode的基本语法为:
3.2 EVM中的Stack
EVM是Stack-Based的,执行完下图前三个指令后,相应stack中的内容见下图左侧:
SWAP2指令是指将stack中的“a,b,c” 转换为 “c,b,a”:【即“0x03,0x04,0x09” -> “0x09,0x04,0x03”】
ADD指令是指将stack中的top 2 值pop出来,相加后的结果再push回stack中:【0x04+0x03=0x07】
CALLER指令是指将 the 20-byte address of the caller account 推送到stack中。该账号为 the account that did the last call (except delegate call)。
stack中每个元素最多为32字节。当想要操作大于32字节的数据时,使用stack将非常复杂,此时可以考虑使用memory。
3.3 EVM中的Memory
Memory为在EVM中可访问的另一种数据结构,其是一个非常长的数组,其长度最低为0,最长可为任意值,不过事实上不会是任意长,因随着运行最终会out of gas。不过从技术上来说,未对Memory的长度做限制。
以MSTORE指令(向Memory写入数据)为例,首先往stack中推入某些数据:
MSTORE指令是指取stack中的top 2值,依次为offset和value值,在memory偏移offset个字节中存入相应的value值。上图中,0x20为offset(32个字节),0x03为value值(32字节):
MSTORE会从stack中pop出top2的2个值,然后值更新到memory中相应的位置:
MLOAD指令(从Memory中读取数据)是指取stack的top1为offset,从memory中相应的offset位置开始读取32字节:
MLOAD指令会从stack中pop顶端值为offset,然后再将从memory中对应offset读取的32字节推入stack中:
Memory很便宜,但其仅存在于单笔交易中,若需要跨多笔交易存储,此时需要使用Storage。
3.4 EVM中的Storage
storage操作方式与memory类似,memory很便宜,区块链上的storage非常昂贵,如:
- 单个SSTORE操作需约2900~20000 gas
- 单个MSTORE操作仅需约3+ gas
因此非必要不使用storage存储,因其非常昂贵。
memory像一个巨大的数组,而storage像key-value数据库。
4. 更简单的Trim语法表示
Trim为小众语言,但具有更易读特性。
Trim的S-Expressions为:
5. Solidity Opcodes表示
如Solidity中的原语与Opcode的对应关系类似有:
- msg.sender->CALLER
- msg.value->CALLVALUE
- block.timestamp->TIMESTAMP
- tx.origin->ORIGIN
5.1 Solidity payable关键字
payable为Solidity的feature,若想get paid from a function,需为该函数添加payable关键字。payable不是an evm related concept,而是a solidity related concept。若函数为标记payable关键字,solidity可借此来decide to block your functions from receiving Ether,因此,对应non-payble函数,存在一些粗略等价的opcodes。首先获取CALLVALUE值,判断其是否为0;若CALLVALUE不为0,则跳转到某处来revert该hashtag syntax,相应的hashtag synax在Trim中称为label,label为bytecode中的某个位置,因此很容易跳转到指定位置的代码,而不需要数字节数 或 手工输入相应的数字:
对于每个未标记payable的函数,都会有以上代码生成。因此,对于标记了payable的函数,不会生成以上代码,从而实际上可减少合约编译出来的code size:
5.2 Solidity Storage变量
Solidity Storage变量:Storage变量仅在使用时才编译,而不是在定义时就编译。因此,可定义任意多的Storage变量,但事实上并不会给编译的合约增加任何code。原因在于,Storage是key-value数据库,在使用之前,无需对key进行初始化。若想读某个之前从未用过的key,返回相应的值为0,并不为抛错误。
Solidity源代码中的变量为index spaced,与变量名无关。
当做合约升级的时候,应注意不要对合约中的storage变量重新排序,否则影响历史数据与变量的对应关系:
5.3 Solidity Compact Storage变量
若有多个连续storage变量,其size均小于256 bit(对应32byte,为EVM的word size)。Solidity会将这些小于256 bit的Storage变量压缩存储在同一storage slot中:
这样的压缩会带来一定的好处,如下图中,在同一storage slot 0中同时有
x
,
y
x,y
x,y变量,因此可用bit mask来选择指定的变量。
采用Compact Storage的主要好处在于:【第一次SLOAD某slot为cold SLOAD,后续对同一slot的SLOAD均为Hot SLOAD。】
- Cold SLOAD需2100 gas
- Hot SLOAD需100 gas
5.4 Solidity If Statement
Solidity中的If条件判断:
不过有趣的一点在于,在0.8.x Solidity版本时,x = x + 7;
这样的引用并未优化,会对应有一次Hot SLOAD,若想避免这种情况,可引入本地变量来优化:uint tmp=x + 7; x = tmp;
。
5.5 Solidity External和Internal Function Calls
EVM中的外部调用 与 内部调用非常不同。事实上,EVM并没有内部调用的概念。所谓内部调用和外部调用,是Solidity层面的概念。
外部调用示例:【从FnCalls合约中外部调用token合约的transfer函数】
内部调用示例为:
EVM中的内部调用是指跳转到bytecode中的指定位置执行完成后,再跳转回去。上图为内部调用的伪代码。
下图与上图不同之处在于,内部调用的函数具有参数,即意味着在jump之前,需将函数参数push到stack中:
参考资料
[1] ETHNewYork 2022分享 Demystifying EVM Opcodes
[2] Explaining core system Ethereum Virtual Machine