跳到主要内容

WTF Opcodes极简入门: 3. 堆栈指令

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

推特:@0xAA_Science

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

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


这一讲,我们介绍EVM中的程序计数器(Program Counter)和堆栈指令,同时用Python实现一个简化版的EVM,可以执行PUSHPOP指令。

程序计数器

在EVM中,程序计数器(通常缩写为 PC)是一个用于跟踪当前执行指令位置的寄存器。每执行一条指令(opcode),程序计数器的值会自动增加,以指向下一个待执行的指令。但是,这个过程并不总是线性的,在执行跳转指令(JUMPJUMPI)时,程序计数器会被设置为新的值。

下面我们使用Python创建一个简单的EVM程序计数器:

class EVM:
# 初始化
def __init__(self, code):
self.code = code # 初始化字节码,bytes对象
self.pc = 0 # 初始化程序计数器为0
self.stack = [] # 堆栈初始为空

# 获取当前指令
def next_instruction(self):
op = self.code[self.pc] # 获取当前指令
self.pc += 1 # 递增
return op

def run(self):
while self.pc < len(self.code):
op = self.next_instruction() # 获取当前指令

上面的示例代码很简单,它的功能就是利用程序计数器遍历字节码中的opcode,在接下来的部分,我们将为它添加更多的功能。

code = b"\x01\x02\x03"
evm = EVM(code)
evm.run()

堆栈指令

EVM是基于堆栈的,堆栈遵循 LIFO(后入先出)原则,最后一个被放入堆栈的元素将是第一个被取出的元素。PUSH和POP指令就是用来操作堆栈的。

PUSH

在EVM中,PUSH是一系列操作符,共有32个(在以太坊上海升级前),从PUSH1PUSH2,一直到PUSH32,操作码范围为0x600x7F。它们将一个字节大小为1到32字节的值从字节码压入堆栈(堆栈中每个元素的长度为32字节),每种指令的gas消耗都是3。

PUSH1为例,它的操作码为0x60,它会将字节码中的下一个字节压入堆栈。例如,字节码0x6001就表示把0x01压入堆栈。PUSH2就是将字节码中的下两个字节压入堆栈,例如,0x610101就是把0x0101压入堆栈。其他的PUSH指令类似。

以太坊上海升级新加入了PUSH0,操作码为0x5F(即0x60的前一位),用于将0压入堆栈,gas消耗为2,比其他的PUSH指令更省gas。

下面我们用python实现PUSH0PUSH32,主要逻辑见push()run()函数:

PUSH0 = 0x5F
PUSH1 = 0x60
PUSH32 = 0x7F

class EVM:
def __init__(self, code):
self.code = code # 初始化字节码,bytes对象
self.pc = 0 # 初始化程序计数器为0
self.stack = [] # 堆栈初始为空

def next_instruction(self):
op = self.code[self.pc] # 获取当前指令
self.pc += 1 # 递增
return op

def push(self, size):
data = self.code[self.pc:self.pc + size] # 按照size从code中获取数据
value = int.from_bytes(data, 'big') # 将bytes转换为int
self.stack.append(value) # 压入堆栈
self.pc += size # pc增加size单位

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

if PUSH1 <= op <= PUSH32:
size = op - PUSH1 + 1
self.push(size)
elif op == PUSH0:
self.stack.append(0)

字节码0x60016001(PUSH1 1 PUSH1 1)会将两个1压入堆栈,下面我们执行一下:

code = b"\x60\x01\x60\x01"
evm = EVM(code)
evm.run()
print(evm.stack)
# output: [1, 1]

你也可以在evm.codes上验证(注意要把字节码开头的0x去掉):

POP

在EVM中,POP指令(操作码0x50,gas消耗2)用于移除栈顶元素;如果当前堆栈为空,就抛出一个异常。

下面我们将POP指令加入到之前的代码中,主要逻辑见pop()run()函数:

PUSH0 = 0x5F
PUSH1 = 0x60
PUSH32 = 0x7F
POP = 0x50

class EVM:
def __init__(self, code):
self.code = code # 初始化字节码,bytes对象
self.pc = 0 # 初始化程序计数器为0
self.stack = [] # 堆栈初始为空

def next_instruction(self):
op = self.code[self.pc] # 获取当前指令
self.pc += 1 # 递增
return op

def push(self, size):
data = self.code[self.pc:self.pc + size] # 按照size从code中获取数据
value = int.from_bytes(data, 'big') # 将bytes转换为int
self.stack.append(value) # 压入堆栈
self.pc += size # pc增加size单位

def pop(self):
if len(self.stack) == 0:
raise Exception('Stack underflow')
return self.stack.pop() # 弹出堆栈

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

if PUSH1 <= op <= PUSH32: # 如果为PUSH1-PUSH32
size = op - PUSH1 + 1
self.push(size)
elif op == PUSH0: # 如果为PUSH0
self.stack.append(0)
self.pc += size
elif op == POP: # 如果为POP
self.pop()

字节码0x6001600150(PUSH1 1 PUSH1 1 POP)会将两个1压入堆栈,然后再弹出一个1。下面我们执行一下:

code = b"\x60\x01\x60\x01\x50"
evm = EVM(code)
evm.run()
evm.stack
# output: [1]

你也可以在evm.codes上验证(注意要把字节码开头的0x去掉):

总结

这一讲,我们主要介绍了EVM中的程序计数器和堆栈指令,特别是PUSHPOP指令。并且参考evm-from-scratch,我们使用Python实现了一个简化版的EVM,能够处理PUSHPOP指令。在后续的教程中,我们将继续探索更多的opcodes,从而进一步完善我们的EVM实现。