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,同时返回0xaaINVALID
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
MLOADDelegateCall指令
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。
