EVM Opcodes 101

1. Hello Opcodes
2. Opcodes分类
3. 堆栈指令
4. 算数指令
5. 比较指令
6. 位级指令
7. 内存指令
8. 存储指令
9. 控制流指令
10. 区块信息指令
11. 堆栈指令2
12. SHA3指令
13. 账户指令
14. 交易指令
15. Log指令
7. 内存指令
内存指令

我最近在重新学以太坊opcodes,也写一个“WTF EVM Opcodes极简入门”,供小白们使用。

推特:@0xAA_Science

社区:Discord|微信群|官网 wtf.academy

所有代码和教程开源在github: github.com/WTFAcademy/WTF-Opcodes


在这一讲,我们将介绍EVM中用于内存(Memory)操作的4个指令,包括MSTORE,MSTORE8,MLOAD,和MSIZE。我们将在用Python写的极简版EVM中添加对这些操作的支持。

EVM中的内存

我们在第一讲介绍了EVM的内存,它是一个线性寻址存储器,类似一个动态的字节数组,可以根据需求动态扩展。它的另一个特点就是易失性,交易结束时所有数据都会被清零。它支持以8或256 bit写入(MSTORE8/MSTORE),但只支持以256 bit取(MLOAD)。

我们可以用Python内置的bytearray来代表内存:

def __init__(self, code):
    self.code = code
    self.pc = 0
    self.stack = []
    self.memory = bytearray()  # 内存初始化为空

内存的读写比存储(Storage)的读写要便宜的多,每次读写有固定费用3 gas,另外如果首次访问了新的内存位置(内存拓展),则需要付额外的费用(由当前偏移量和历史最大偏移量决定),计算方法见链接。

MSTORE (内存写)

MSTORE指令用于将一个256位(32字节)的值存储到内存中。它从堆栈中弹出两个元素,第一个元素为内存的地址(偏移量 offset),第二个元素为存储的值(value)。操作码是0x52,gas消耗根据实际内存使用情况计算(3+X)。

def mstore(self):
    if len(self.stack) < 2:
        raise Exception('Stack underflow')
    offset = self.stack.pop()
    value = self.stack.pop()
    while len(self.memory) < offset + 32:
        self.memory.append(0) # 内存扩展
    self.memory[offset:offset+32] = value.to_bytes(32, 'big')

我们在run()函数中添加对MSTORE指令的处理:

def run(self):
    while self.pc < len(self.code):
        op = self.next_instruction()

        # ... 其他指令的处理 ...

        elif op == MSTORE: # 处理MSTORE指令
            self.mstore()

现在,我们可以尝试运行一个包含MSTORE指令的字节码:0x6002602052(PUSH1 2 PUSH1 0x20 MSTORE)。这个字节码将2和0x20(32)推入堆栈,然后进行MSTORE,将2存到偏移量为0x20的地方。

# MSTORE
code = b"\x60\x02\x60\x20\x52"
evm = EVM(code)
evm.run()
print(evm.memory[0x20:0x40])  
# 输出: [0, 0, 0, ..., 0, 2]

MSTORE8 (内存8位写)

MSTORE8指令用于将一个8位(1字节)的值存储到内存中。与MSTORE类似,但只使用最低8位。操作码是0x53,gas消耗根据实际内存使用情况计算(3+X)。

def mstore8(self):
    if len(self.stack) < 2:
        raise Exception('Stack underflow')
    offset = self.stack.pop()
    value = self.stack.pop()
    while len(self.memory) < offset + 32:
        self.memory.append(0) # 内存扩展
    self.memory[offset] = value & 0xFF # 取最低有效字节

我们在run()函数中添加对MSTORE8指令的处理:

elif op == MSTORE8: # 处理MSTORE8指令
    self.mstore8()

现在,我们可以尝试运行一个包含MSTORE8指令的字节码:0x6002602053(PUSH1 2 PUSH1 0x20 MSTORE8)。这个字节码将2和0x20(32)推入堆栈,然后进行MSTORE8,将2存到偏移量为0x20的地方。

# MSTORE8
code = b"\x60\x02\x60\x20\x53"
evm = EVM(code)
evm.run()
print(evm.memory[0x20:0x40])  
# 输出: [2, 0, 0, ..., 0, 0]

MLOAD (内存读)

MLOAD指令从内存中加载一个256位的值并推入堆栈。它从堆栈中弹出一个元素,从该元素表示的内存地址中加载32字节,并将其推入堆栈。操作码是0x51,gas消耗根据实际内存使用情况计算(3+X)。

def mload(self):
    if len(self.stack) < 1:
        raise Exception('Stack underflow')
    offset = self.stack.pop()
    while len(self.memory) < offset + 32:
        self.memory.append(0) # 内存扩展
    value = int.from_bytes(self.memory[offset:offset+32], 'big')
    self.stack.append(value)

我们在run()函数中添加对MLOAD指令的处理:

elif op == MLOAD: 
    self.mload()

现在,我们可以尝试运行一个包含MLOAD指令的字节码:0x6002602052602051(PUSH1 2 PUSH1 0x20 MSTORE PUSH1 0x20 MLOAD)。这个字节码将2和0x20(32)推入堆栈,然后进行MSTORE,将2存到偏移量为0x20的地方;然后将0x20推入堆栈,然后进行MLOAD,将刚才存储在内存的值读取出来。

# MSTORE
code = b"\x60\x02\x60\x20\x52\x60\x20\x51"
evm = EVM(code)
evm.run()
print(evm.memory[0x20:0x40])  
# 输出: [0, 0, 0, ..., 0, 2]
print(evm.stack)  
# output: [2]

MSIZE (内存大小)

MSIZE指令将当前的内存大小(以字节为单位)压入堆栈。操作码是0x59,gas消耗为2。

def msize(self):
    self.stack.append(len(self.memory))

总结

这一讲,我们介绍了EVM中的内存操作指令,并在极简版EVM中添加了对它们的支持。这些操作允许我们在EVM的内存中存储和读取值,为更复杂的合约逻辑提供基础。

课后习题: 写出0x6002602053602051对应的指令形式,并给出运行后的堆栈和内存状态。

PreviousNext