EVM Opcodes(2)
原文章:https://www.wtf.academy/evm-opcodes-101
控制流指令
EVM 中的控制流
EVM 的控制流是由跳转指令(JUMP
、JUMPI
、JUMPDEST
)控制 PC (程序计数器)指向新的指令位置而实现的,这允许合约进行条件执行和循环执行。
STOP(停止)
- 操作码:
0x00
- gas 消耗:
0
STOP
是 EVM 的停止指令,它的作用是停止当前上下文的执行。并成功退出
将STOP
操作码设置为0x00
有一个好处:当一个调用被执行到一个没有代码的地址(EOA),并且 EVM 尝试读取代码数据时,系统会返回一个默认值 0,这个默认值对应的指令就是STOP
指令,程序就会停止执行
JUMPDEST(跳转目标)
- 操作码:
0x5b
- gas 消耗:
1
JUMPDEST
指令标记一个有效的跳转目标位置,不然无法使用JUMP
和JUMPI
进行跳转。
但是0x5b
有时候会作为PUSH
的参数(详情可看黄皮书中的9.4.3. Jump Destination Validity),所以需要在运行代码前,筛选字节码中有效的JUMPDEST
指令,使用ValidJumpDest
来存储有效的JUMPDEST
指令所在位置。
JUMP(跳转)
- 操作码:
0x56
- gas 消耗:
8
JUMP
指令用于无条件跳转到一个新的程序计数器位置。他从堆栈中弹出一个元素,将这个元素设定为新的程序计数器(pc
)的值。
举例:
0x600456005B
(PUSH1 4 JUMP STOP JUMPDEST)。这段字节码将4
推入堆栈,然后进行JUMP
,跳转到pc = 4
的位置,该位置正好是JUMPDEST
指令,跳转成功,程序没有被STOP
指令中断。
JUMPI(条件跳转)
- 操作码:
0x57
- gas 消耗:
10
JUMPI
指令用于条件跳转,他从堆栈中弹出两个元素,如果第二个元素(条件,condition
)不为 0,那么将第一个元素(目标,destination
)设定位新的pc
值
举例:
0x6001600657005B
(PUSH1 1 PUSH1 6 JUMPI STOP JUMPDEST)。这个字节码将1
和6
推入堆栈,然后进行JUMPI
,由于条件不为0
。执行跳转到pc = 6
的位置,该位置正好是JUMPDEST
指令,跳转成功,程序没有被STOP
指令中断。
PC(程序计数器)
- 操作码:
0x58
- gas 消耗:
2
PC
指令将当前的程序计数器(PC
)的值压入到堆栈。
区块信息指令
区块信息
在编写智能合约时,经常会用到区块链信息,比如生成伪随机数时常用的blockhash
,block.number
,block.timestamp
。
EVM 提供了一系列指令让智能合约访问当前或历史区块信息,包括区块哈希、时间戳、coinbase等。
区块信息指令
BLOCKHASH
:- 操作码:
0x40
- gas 消耗:
20
查询特定区块(最近的 256 个区块,不包括当前区块)的 hash。他从堆栈中弹出一个值作为区块高度(block number),然后将该区块的 hash 压入堆栈。(如果不属于最近的 256 个区块,则返回 0。)(可用使用
NUMBER
指令来查询当前区块高度)- 操作码:
COINBASE
:- 操作码:
0x41
- gas 消耗:
2
将当前区块的coinbase(矿工/受益人)地址压入堆栈。
- 操作码:
TIMESTAMP
:- 操作码:
0x42
- gas 消耗:
2
将当前区块的时间戳压入堆栈
- 操作码:
NUMBER
:- 操作码:
0x43
- gas 消耗:
2
将当前区块高度压入堆栈
- 操作码:
PREVRANDAO
:- 操作码:
0x44
- gas 消耗:
2
替代了原先的
DIFFICULTY
(0x44)操作码,其返回值为 beacon 链随机性信标的输出。此变更允许智能合约在以太坊转向权益证明(PoS)后继续从原本的DIFFICULTY
操作码处获得随机性。- 操作码:
GASLIMIT
:- 操作码:
0x45
- gas 消耗:
2
将当前区块的 gas 限制压入堆栈。
- 操作码:
CHAINID
:- 操作码:
0x46
- gas 消耗:
2
将当前的链ID压入堆栈
- 操作码:
SELFBALANCE
:- 操作码:
0x47
- gas 消耗:
2
将合约的当前余额压入堆栈
- 操作码:
BASEFEE
:- 操作码:
0x48
- gas 消耗:
2
将当前区块的基础费(base fee)压入堆栈。
- 操作码:
SHA3 指令
- 操作码:
0x20
- gas 消耗:
30 + 6 * 数据的字节长度 + 拓展内存成本
SHA3(offset, size)
指令从堆栈中取出两个参数,起始位置offset
和长度size
(以字节为单位),然后它从内存中读取起始位置offset
开始的size
长度的数据,计算这段数据的Keccak-256哈希,并将结果(一个32字节的值)压入堆栈。
示例:
0x5F5F20
(PUSH0 PUSH0 SHA3)。这个字节码将两个0
推入堆栈,然后用SHA3
指令计算0
的哈希。
账户指令
以太坊账户结构
以太坊上的账户分为两类:
- 外部账户(Externally Owned Accounts,EOA)
- 以太坊网络上的代表,他们可以拥有ETH,发送交易并与合约互动
- 合约账户(Contract Account)
- 存储和执行智能合约代码的实体,它们也可以拥有和发送 ETH,但不能主动发起交易
以太坊上的账户结构非常简单,可以理解为地址到账户状态的映射。 账户地址是 20 字节(160 位)的数据,可以用 40 位的 16 进制表示,比如0x9bbfed6889322e016e0a02ee459d306fc19545d8
。而账户的状态(State)具有 4 中属性:
- Balance:这是账户持有的原生代币(Native Token)数量,用 Wei 表示(1 ETH = 10 ^ 18 Wei)
- Nonce:对于 EOA 账户,这是该账户发送的交易数。对于合约账户,它是该账户创建的合约数量。
- Storage:每个合约账户都有与之关联的存储空间,其中包含状态变量的值
- Code:合约账户的字节码
也就是说,只有合约账户拥有Storage
和Code
,EOA 没有。
BALANCE
- 操作码:
0x31
- gas 消耗:
2600
(cold address)或100
(warm address)
BALANCE
指令用于返回某个账户的余额。他从堆栈中弹出一个地址,然后查询该地址的余额并压入堆栈。
示例:
739bbfed6889322e016e0a02ee459d306fc19545d831
(PUSH20 9bbfed6889322e016e0a02ee459d306fc19545d8 BALANCE)。这个字节码使用PUSH20
将一个地址推入堆栈,然后使用BALANCE
指令查询该地址的余额。
EXTCODESIZE
- 操作码:
0x3B
- gas 消耗:
2600
(cold address) 或100
(warm address)
EXTCODESIZE
指令用于返回某个账户的代码长度(以字节为单位)。他从堆栈弹出一个地址,然后查询该地址的代码长度并压入堆栈。如果该账户不存在或者没有代码,则返回 0 。
示例:
739bbfed6889322e016e0a02ee459d306fc19545d83B
(PUSH20 9bbfed6889322e016e0a02ee459d306fc19545d8 EXTCODESIZE)。这个字节码使用PUSH20
将一个地址压入堆栈,然后使用EXTCODESIZE
指令查询该地址的代码长度。
EXTCODECOPY
- 操作码:
0x3C
- gas 消耗:由读取代码长度、内存拓展成本、地址是否为 cold 三部分决定
EXTCODECOPY
指令用于将某个账户的代码部分复制到 EVM 内存中。他从堆栈中弹出 4 个参数(addr, mem_offset, code_offset, length),分别对应要查询的地址,写到内存的偏移量,读取代码的偏移量和长度。
示例:
60045F5F739bbfed6889322e016e0a02ee459d306fc19545d83C
(PUSH1 4 PUSH0 PUSH0 PUSH20 9bbfed6889322e016e0a02ee459d306fc19545d8 EXTCODECOPY)。这个字节码将 4 (length
),0(code_offset
),0(mem_offset
),地址(addr
)分别推入堆栈,然后使用EXTCODECOPY
指令将代码复制到内存中。
EXTCODEHASH
- 操作码:
0x3F
- gas 消耗:
2600
(cold address) or100
(warm address)
EXTCODEHASH
指令返回某个账户的代码的 Keccak256 哈希值。他从堆栈中弹出一个地址,然后查询该地址代码的哈希并压入堆栈。
示例:
739bbfed6889322e016e0a02ee459d306fc19545d83F
(PUSH20 9bbfed6889322e016e0a02ee459d306fc19545d8 EXTCODEHASH)。这个字节码使用PUSH20
将一个地址推入堆栈,然后使用EXTCODEHASH
指令查询该地址的代码哈希。
交易指令
交易的基本结构
再深入学习这些指令之前,我们先了解以太坊交易的基本结构。每一笔以太坊交易都包含以下属性:
nonce
:一个与发送者账户相关的数字,表示该账户已发送的交易数gasPrice
:交易发送者愿意支付的单位 gas 价格。gaslimit
:交易发送者为这次交易分配的最大 gas 数量to
:交易的接收者地址。当交易为合约创建时,这一字段为空value
:以 wei 为单位的发送金额data
:附带的数据,通常为合约调用的输入数据(calldata)和新合约的初始代码(initcode)。
v, r, s
:与交易签名相关的三个值
交易指令
ADDRESS
- 操作码:
0x30
- gas 消耗:
2
- 功能:将当前执行合约的地址压入堆栈。
- 使用场景:当合约需要知道自己的地址时使用
ORIGIN
- 操作码:
0x32
- gas 消耗:
2
- 功能:将交易的原始发送者(即签名者)地址压入堆栈。
- 使用场景:区分合约调用者与交易发起者
CALLER
- 操作码:
0x33
- gas 消耗:
2
- 功能:将直接调用当前合约的地址压入堆栈
- 使用场景:当合约需要知道是谁调用了它的时候
CALLVALUE
- 操作码:
0x34
- gas 消耗:
2
- 功能:将发送给合约的 ether 的数量(以 wei 为单位)压入堆栈
- 使用场景:当合约需要知道有多少以太币被发送时使用
CALLDATALOAD
- 操作码:
0x35
- gas 消耗:
3
- 功能:从交易或合约调用的
data
字段加载数据。他从堆栈中弹出 calldata 的偏移量(offset
),然后从 calldata 的offset
位置读取 32 字节的数据并压入堆栈。如果 calldata 剩余不足 32 字节,则补 0。 - 使用场景:读取传入的数据。
CALLDATASIZE
- 操作码:
0x36
- gas 消耗:
2
- 功能:获取交易的合约调用的
data
字段的字节长度,并压入堆栈。 - 使用场景:在读取数据之前检查大小
CALLDATACOPY
- 操作码:
0x37
- gas 消耗:
3 + 3 * 数据长度 + 内存拓展成本
- 功能:将
data
中的数据复制到内存中。他会从堆栈中弹出三个参数(mem_offset, calldata_offset, length),分别对应写到内存的偏移量,读取 calldata 的偏移量和长度。 - 使用场景:将输入数据复制到内存
CODESIZE
- 操作码:
0x38
- gas 消耗:
2
- 功能:获取当前合约代码的字节长度,然后压入堆栈
- 使用场景:当合约需要访问自己的字节码时使用
CODECOPY
- 操作码:
0x39
- gas 消耗:
3 + 3 * 数据长度 + 内存拓展成本
- 功能:复制合约的代码到 EVM 的内存中。他从堆栈中弹出三个参数:目标内存的开始偏移量(
mem_offset
)、代码的开始偏移量(code_offset
)以及要复制的长度(length
) - 使用场景:当合约需要读取自己的部分字节码时使用
GASPRICE
- 操作码:
0x4A
- gas 消耗:
2
- 功能:获取交易的 gas 价格,并压入堆栈。
- 使用场景:当合约需要知道当前的 gas 价格时使用
Log 指令
EVM 中的日志和事件
在 Solidity 中,我们常常使用event
来定义和触发事件。当这些事件被触发时,他们会生成日志,将数据永远存储在区块链上。日志分为主题(topic
)和数据(data
)。第一个主题通常是事件签名的哈希值,后面的主题是由indexed
修饰的事件参数。
EVM 中的LOG
指令用于创建这些日志。指令从LOG0
到LOG4
的区别在于它们包含的主题数量。例如,LOG0
没有主题,而LOG4
有四个主题。
LOG 指令
- 操作码:
A0
到A4
- gas 消耗:
gas = 375 + 375 * topic 数量 + 内存拓展成本
EVM 中有五个 LOG
指令:LOG0
、LOG1
、LOG2
、LOG3
、LOG4
。他们的主要区别在于携带的主题数(topics
):LOG0
没有主题,LOG4
有四个主题。
LOG
指令从堆栈中弹出 2 + n 个元素。其中前两个参数是内存开始位置(mem_offset
)和数据长度length
,n 是主题的数量(取决于具体的LOG
指令)。所有对于LOG1
,我们会从堆栈中弹出 3 个元素:内存开始位置,数据长度,和一个主题。需要mem_offset
的原因是日志的数据(data
)数据存储在内存中,gas 消耗低,而主题(topic
)部分直接存储在堆栈上。
示例:
**测试
LOG0
**:运行一个包含LOG0
的字节码:60aa6000526001601fa0
(PUSH1 aa PUSH1 0 MSTORE PUSH1 1 PUSH1 1f LOG0)。这个字节码将aa
存在内存中,然后使用LOG0
指令将aa
输出到日志的数据部分。测试
LOG1
:运行一个包含LOG1
指令的字节码:60aa60005260116001601fa1
(PUSH1 aa PUSH1 0 MSTORE PUSH 11 PUSH1 1 PUSH1 1f LOG1)。这个字节码将aa
存在内存,然后将11
压入堆栈,最后使用LOG1
指令将aa
输出到日志的数据部分,将11
输出到日志的主题部分。