Opcodes再进阶
Opcodes再进阶
这一篇的指令相比之前更加抽象,不过单列一篇来记更大的原因是我觉得写在一起太挤了。
Return类指令
RETURN
从指定的内存位置提取数据,存储到returnData
中,并终止当前的操作。此指令需要从堆栈中取出两个参数:内存的起始位置mem_offset
和数据的长度length
,操作码:0xF3
。当需要将数据返回给外部函数或交易时调:
PUSH1 0xff
PUSH1 0x00
MSTORE8
// 此时内存ff0000000000...
PUSH1 0x01
PUSH1 0x00
RETURN
// 取出第一个内存字节返回,Return value: 0xff
PUSH1 0x01 // 不会执行,一旦用了RETURN,程序将中止
RETURNDATASIZE
将returnData
的大小推入堆栈,操作码:0x3D
,用来获得上一个调用返回的数据。注意,这个只能由外部solidity合约调用,自己内部一旦RETURN了就啥也执行不下去了。
RETURNDATACOPPY
将returnData
中的某段数据复制到内存中。此指令需要从堆栈中取出三个参数:内存的起始位置mem_offset
,返回数据的起始位置return_offset
,和数据的长度length
,操作码:0x3E
。
REVERT类指令
这类指令用于处理异常,当它们被触发时,交易会回滚。下面的这两个指令是Solidity中的require
,error
和assert
关键字的基础。
REVERT
REVERT
指令会终止交易的执行,返回一个错误消息,并且所有状态更改(例如资金转移、存储值的更改等)都不会生效。它会从堆栈中弹出两个参数:内存中错误消息的起始位置mem_offset
和错误消息的长度length
。它的操作码为0xFD
。
PUSH1 0xaa
PUSH1 0x00
MSTORE
PUSH1 0x01
PUSH1 0x1f
REVERT
// [Error] revert,同时返回0xaa
INVALID
INVALID
是EVM中用来表示无效操作的指令。当EVM遇到无法识别的操作码时,或者在故意触发异常的情境下,它会执行INVALID
指令,导致所有状态更改都不会生效,并且消耗掉所有的gas。它确保了当合约试图执行未定义的操作时,不会无所作为或产生不可预测的行为,而是会安全地停止执行,它的操作码为0xFE
。
EVM会跟踪交易的状态success
,默认为True
,当交易失败回滚时变为False
,只有当success
为True
时继续执行opcodes,否则结束交易。而INVALID指令的原理非常暴力,就是直接把交易状态设成false
。
Call指令
CALL
指令会创建一个子环境来执行其他合约的部分代码,发送ETH
,并返回数据。返回数据可以使用RETURNDATASIZE
和RETURNDATACOPY
获取。若执行成功,会将1
压入堆栈;否则,则压入0
。如果目标合约没有代码,仍将1
压入堆栈(视为成功)。如果账户ETH
余额小于要发送的ETH
数量,调用失败,但当前交易不会回滚。
它从堆栈中弹出7个参数,依次为:
gas
:为这次调用分配的gas量;
to
:被调用合约的地址;
value
:要发送的ETH数量(从msg.sender
扣除);
mem_in_start
:输入数据(calldata
,调用的目标合约具体函数和参数)在内存的起始位置;
mem_in_size
:输入数据的长度;
mem_out_start
:返回数据(returnData
)在内存的起始位置,这是预留来接收目标合约的返回值的,留出空间就行不用写东西。
mem_out_size
:返回数据的长度。
它的操作码为0xF1
。
一个简单的调用:
PUSH1 0x01
PUSH1 0x1f
PUSH0
PUSH0
PUSH1 0x01
PUSH20 0x1000000000000000000000000000000000000c42
PUSH0
CALL
PUSH0
MLOAD
DelegateCall指令
DELEGATECALL
它从堆栈中弹出6个参数,与CALL
不同,它不包括value
,因为ETH不会被发送,DELEGATECALL
不会更改msg.sender
和msg.value
,且DELEGATECALL
改变的存储(storage)是原始合约的存储。
gas
:为这次调用分配的gas量;
to
:被调用合约的地址;
mem_in_start
:输入数据(calldata
)在内存的起始位置;
mem_in_size
:输入数据的长度;
mem_out_start
:返回数据(returnData
)在内存的起始位置;
mem_out_size
:返回数据的长度。
StaticCall指令
它和CALL
指令类似,允许合约执行其他合约的代码,但是不能改变合约状态。它是Solidity中pure
和view
关键字的基础。
STATICCALL
STATICCALL
指令会创建一个子环境来执行其他合约的部分代码,并返回数据。返回数据可以使用RETURNDATASIZE
和RETURNDATACOPY
获取。若执行成功,会将1
压入堆栈;否则,则压入0
。如果目标合约没有代码,仍将1
压入堆栈(视为成功)。
由于不能改变合约的状态,它不允许子环境执行的代码中包含以下指令:
CREATE
, CREATE2
, SELFDESTRUCT
;
LOG0
- LOG4;
SSTORE
;
value
不为0的CALL
。
它从堆栈中弹出6个参数,依次为:
gas
:为这次调用分配的gas量;
to
:被调用合约的地址;
mem_in_start
:输入数据(calldata
)在内存的起始位置;
mem_in_size
:输入数据的长度;
mem_out_start
:返回数据(returnData
)在内存的起始位置;
mem_out_size
:返回数据的长度。
它的操作码为0xFA
。
Create指令
它可以让合约创建新的合约。
以太坊有两种交易,一种是合约调用,而另一种是合约创建。在合约创建的交易中,to
字段设为空,而data
字段应填写为合约的初始代码(initcode
)。initcode
也是字节码,但它只在合约创建时执行一次,目的是为新合约设置必要的状态和返回最终的合约字节码(contract code
)。
一个简单的initcode
:
PUSH4 ffffffff // 存进内存
PUSH1 00
MSTORE
PUSH1 04
PUSH1 1c
RETURN // 把ffffffff返回,新合约的字节码被设为此
CREATE
执行流程:
从堆栈中弹出value
(向新合约发送的ETH)、mem_offset
和length
(新合约的initcode
在内存中的初始位置和长度);
计算新合约的地址,更新ETH余额;
初始化新的EVM上下文evm_create
,用于执行initcode
;
在evm_create
中执行initcode
;
如果执行成功,则更新创建的账户状态:
更新balance
,将nonce
初始化为0
,将code
字段设为evm_create
的返回数据,将storage
字段设置为evm_create
的storage
;
如果成功,则将新合约地址推入堆栈;若失败,将0
推入堆栈。
CREATE2
CREATE2
则提供了一种新的计算方法,使我们可以在合约部署之前预知它的地址:
address = keccak256( 0xff + sender_address + salt + keccak256(init_code))[12:]
执行流程:
从堆栈中弹出value
(向新合约发送的ETH)、mem_offset
、length
(新合约的initcode
在内存中的初始位置和长度)以及salt
;
计算新地址;
之后同CREATE
。
SelfDestruct指令
EVM中的SELFDESTRUCT
指令可以让合约自行销毁,并将账户中的ETH余额发送到指定地址。使用SELFDESTRUCT
指令时,当前合约会被标记为待销毁。但实际的销毁操作会在整个交易完成后进行。发送余额的过程一定成功,如果指定的地址是一个合约,那么该合约的代码不会被执行,即不会像平常的ETH
转账执行目标地址的fallback
方法。如果指定的地址不存在,则会为其创建一个新的账户,并存储这些ETH。
合约一旦销毁就无法再恢复。
执行流程:
从堆栈中弹出接收ETH
的指定地址;
将当前合约的余额转移到指定地址;
销毁合约。
Gas指令
EVM中的GAS
指令会将当前交易的剩余Gas
压入堆栈。它的操作码为0x5A
。