EVM指令集
EVM中的代码是合约编译过来的字节码。其中构成字节码的一个EVM指令集大小为一个字节(8bit),所以EVM总指令集最多由256个,目前已存在100多个指令(5C-5E 没有使用)。
EVM执行从EVM Code中读取到的指令集,stack能从memory、calldata或storage一次加载一个字大小的数据(4byte),将指令使用的值放入栈内进行操作得到结果后将其值返回。
下面会从solidity汇编常用的指令去进行代码展示。EVM指令集详细细节查询:EVM Codes - An Ethereum Virtual Machine Opcodes Interactive Reference
EVM指令提供了以下几个功能:
- 算术和位逻辑运算
- 执行上下文查询
- 堆栈、内存和存储访问
- 控制流操作
- Logging, calling和其他操作符
算术运算
1 2 3 4 5 6 7 8 9 10 11 12
| ADD MUL SUB DIV SDIV MOD SMOD ADDMOD MULMOD EXP SIGNEXTEND SHA3
|
堆栈操作
1 2 3 4 5 6 7 8 9 10
| POP MLOAD MSTORE MSTORE8 SLOAD SSTORE MSIZE PUSHx DUPx SWAPx
|
流程控制操作码
1 2 3 4 5
| STOP JUMP JUMPI PC JUMPDEST
|
系统操作
1 2 3 4 5 6 7 8 9 10
| LOGx CREATE CALL CALLCODE RETURN DELEGATECALL STATICCALL REVERT INVALID SELFDESTRUCT
|
逻辑运算
1 2 3 4 5 6 7 8 9 10 11
| LT GT SLT SGT EQ ISZERO AND OR XOR NOT BYTE
|
环境操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| GAS ADDRESS BALANCE ORIGIN CALLER CALLVALUE CALLDATALOAD CALLDATASIZE CALLDATACOPY CODESIZE CODECOPY GASPRICE EXTCODESIZE EXTCODECOPY RETURNDATASIZE RETURNDATACOPY
|
区块操作码
1 2 3 4 5 6
| BLOCKHASH COINBASE TIMESTAMP NUMBER DIFFICULTY GASLIMIT
|
编译合约
工具准备
笔者编译合约使用以下工具
编译合约
选用remix的代码案例进行分析。下面是一个存储合约,代码函数由两部分组成。
两个函数的选择器
- store(uint256):6057361d
- retrieve():2e64cec1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| // SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
/** * @title Storage * @dev Store & retrieve value in a variable * @custom:dev-run-script ./scripts/deploy_with_ethers.ts */ contract Storage {
uint256 number;
/** * @dev Store value in variable * @param num value to store */ function store(uint256 num) public { number = num; }
/** * @dev Return value * @return value of 'number' */ function retrieve() public view returns (uint256){ return number; } }
|
字节码分析
该部分字节码通过remix获得。object部分对应合约字节码,opcode部分对应字节码的操作码。
1 2 3 4
| { "object": "608060405234801561000f575f80fd5b506101438061001d5f395ff3fe608060405234801561000f575f80fd5b5060043610610034575f3560e01c80632e64cec1146100385780636057361d14610056575b5f80fd5b610040610072565b60405161004d919061009b565b60405180910390f35b610070600480360381019061006b91906100e2565b61007a565b005b5f8054905090565b805f8190555050565b5f819050919050565b61009581610083565b82525050565b5f6020820190506100ae5f83018461008c565b92915050565b5f80fd5b6100c181610083565b81146100cb575f80fd5b50565b5f813590506100dc816100b8565b92915050565b5f602082840312156100f7576100f66100b4565b5b5f610104848285016100ce565b9150509291505056fea26469706673582212205b47cee55f559ebab97cf0c7dc7a6cef1aba6f07a98a0a25c74b22ba6701722364736f6c63430008160033", "opcodes": "PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0xF JUMPI PUSH0 DUP1 REVERT JUMPDEST POP PUSH2 0x143 DUP1 PUSH2 0x1D PUSH0 CODECOPY PUSH0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH2 0xF JUMPI PUSH0 DUP1 REVERT JUMPDEST POP PUSH1 0x4 CALLDATASIZE LT PUSH2 0x34 JUMPI PUSH0 CALLDATALOAD PUSH1 0xE0 SHR DUP1 PUSH4 0x2E64CEC1 EQ PUSH2 0x38 JUMPI DUP1 PUSH4 0x6057361D EQ PUSH2 0x56 JUMPI JUMPDEST PUSH0 DUP1 REVERT JUMPDEST PUSH2 0x40 PUSH2 0x72 JUMP JUMPDEST PUSH1 0x40 MLOAD PUSH2 0x4D SWAP2 SWAP1 PUSH2 0x9B JUMP JUMPDEST PUSH1 0x40 MLOAD DUP1 SWAP2 SUB SWAP1 RETURN JUMPDEST PUSH2 0x70 PUSH1 0x4 DUP1 CALLDATASIZE SUB DUP2 ADD SWAP1 PUSH2 0x6B SWAP2 SWAP1 PUSH2 0xE2 JUMP JUMPDEST PUSH2 0x7A JUMP JUMPDEST STOP JUMPDEST PUSH0 DUP1 SLOAD SWAP1 POP SWAP1 JUMP JUMPDEST DUP1 PUSH0 DUP2 SWAP1 SSTORE POP POP JUMP JUMPDEST PUSH0 DUP2 SWAP1 POP SWAP2 SWAP1 POP JUMP JUMPDEST PUSH2 0x95 DUP2 PUSH2 0x83 JUMP JUMPDEST DUP3 MSTORE POP POP JUMP JUMPDEST PUSH0 PUSH1 0x20 DUP3 ADD SWAP1 POP PUSH2 0xAE PUSH0 DUP4 ADD DUP5 PUSH2 0x8C JUMP JUMPDEST SWAP3 SWAP2 POP POP JUMP JUMPDEST PUSH0 DUP1 REVERT JUMPDEST PUSH2 0xC1 DUP2 PUSH2 0x83 JUMP JUMPDEST DUP2 EQ PUSH2 0xCB JUMPI PUSH0 DUP1 REVERT JUMPDEST POP JUMP JUMPDEST PUSH0 DUP2 CALLDATALOAD SWAP1 POP PUSH2 0xDC DUP2 PUSH2 0xB8 JUMP JUMPDEST SWAP3 SWAP2 POP POP JUMP JUMPDEST PUSH0 PUSH1 0x20 DUP3 DUP5 SUB SLT ISZERO PUSH2 0xF7 JUMPI PUSH2 0xF6 PUSH2 0xB4 JUMP JUMPDEST JUMPDEST PUSH0 PUSH2 0x104 DUP5 DUP3 DUP6 ADD PUSH2 0xCE JUMP JUMPDEST SWAP2 POP POP SWAP3 SWAP2 POP POP JUMP INVALID LOG2 PUSH5 0x6970667358 0x22 SLT KECCAK256 JUMPDEST SELFBALANCE 0xCE 0xE5 PUSH0 SSTORE SWAP15 0xBA 0xB9 PUSH29 0xF0C7DC7A6CEF1ABA6F07A98A0A25C74B22BA6701722364736F6C634300 ADDMOD AND STOP CALLER ", }
|
合约部署
使用remix的调试器对合约部署交易进行分析。合约部署时,EVM会执行到字节码第27字节时结束。下面开始分析合约部署时,opcode的逻辑。
合约部署相关操作码主要发生在第15-27字节之间。我们可以先通过前四个指令开始研究EVM指令集是怎样工作的。
首先push1是将1字节压入栈中,可以使用push32指令可以压入32字节到栈。指令进行了两次push1操作
1
| PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE
|
然后使用了mstore(offset,vaule)将0x08存入内存中。
下一个操作码是CALLVALUE,这是一个环境操作码,它将启动此执行的消息调用发送的以太数量(以wei为单位)推到堆栈的顶部。
合约部署时会通过codecopy将合约操作码copy到内存中,在代码执行到jump时,会跳转到0xf位置,即下面的JUMPDEST。
1
| JUMPDEST POP PUSH2 0x143 DUP1 PUSH2 0x1D PUSH0 CODECOPY PUSH0 RETURN
|
此时栈的状态如下:
让我们跳到最重要的一步:codecopy(destOffset, offset, size),该指令会将当前环境下运行的代码复制到内存中,其中destOffset是复制到内存的偏移量,offset是复制代码中的字节偏移量,size是复制的代码字节大小。
codecopy从栈顶拿到三个值,复制到代码0x00位置,从当前环境(calldata)中的1d位置开始,大小为143字节。可以看到memory中复制的字节码。
执行函数
使用remix调用store函数,存入5到合约中。交易的calldata如下:
1
| "0x6057361d0000000000000000000000000000000000000000000000000000000000000005"
|
执行store函数时从第26字节开始,第26字节位置使用了calldataload(i)。calldataload是从calldata指定偏移i读取32字节到栈中。然后通过shr(shift, value)将value向右偏移0xE0位
1
| CALLDATALOAD PUSH1 0xE0 SHR
|
calldata读到的是store的函数选择器,指令会比较函数选择器跳转到相应位置。下面两行是两个函数选择器,根据操作码逻辑,先使用dup对栈中0的值进行复制,然后push4 向栈中推入retrieve()的选择器进行比较,eq会返回0。
1
| DUP1 PUSH4 0x2E64CEC1 EQ PUSH2 0x38 JUMPI
|
jumpI(counter, b)能跳转到其他指令,其中counter是将继续执行的部署代码中的字节偏移量(跳到的指令必须是JUMPDEST),b当该值不等于0是才会跳转。在下图中,栈顶的两个值为0x38,0x00,由于b等于0,jumpI不会跳转。
同上,但是jumpI操作码能够跳转到0x56位置,因为calldata传入的选择器与栈顶顶的值一致。(注意remix使用十进制对应指令位置,即跳转到86)
1
| DUP1 PUSH4 0x6057361D EQ PUSH2 0x56 JUMPI
|
现在我们直接跳到即将结束的一步,EVM会使用sstore(key,value)将给定的参数5存储在对应合约地址的key-0:value-5的位置。
使用sstore后storage变化
使用内联汇编减少gas消耗
上面我们学习了指令集,不仅调试的时候可以使用它进行分析,我们还能够使用它在solidity中写内联汇编。可以通过assembly块开始一段代码。
下面进行一个简单的演示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
contract assTest{
function add1(uint256 a, uint256 b) public pure returns (uint) { return a + b; }
function add2(uint x, uint y) public pure returns (uint) { assembly { let result := add(x, y) mstore(0x0, result) return(0x0, 32) } } }
|
从remix的获得的两个函数消耗gas进行比较(可能因环境不同有差异)
- 前者使用:949 gas
- 后者使用:561 gas
引用:
1.ethereumbook/13evm.asciidoc at develop · ethereumbook/ethereumbook (github.com)
2.EVM Codes - An Ethereum Virtual Machine Opcodes Interactive Reference