1. 引言
本文主要源自Macro团队的Gilbert在ETHNewYork 2022分享 Demystifying EVM Opcodes,同时结合evm.codes来理解。
学习EVM Opcodes,可成为更好的Solidity工程师。
更好的Solidity工程师,意味着:
- 1)理解Solidity的设计原理。
- 2)更好的为low-level code做准备。
- 3)更深入的理解通用设计模式。
- 4)更深入的理解智能合约在EVM中的运行机制。
2. 何为虚拟机?
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
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:
参考资料
[1] ETHNewYork 2022分享 Demystifying EVM Opcodes