跳到主要内容

WTF Opcodes极简入门: 9. 控制流指令

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

推特:@0xAA_Science

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

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


在这一讲,我们将介绍EVM中用于控制流的5个指令,包括STOPJUMPJUMPIJUMPDEST,和PC。我们将在用Python写的极简版EVM中添加对这些操作的支持。

EVM中的控制流

我们在第三讲介绍了程序计数器(Program Counter,PC),而EVM的控制流是由跳转指令(JUMPJUMPIJUMPDEST)控制PC指向新的指令位置而实现的,这允许合约进行条件执行和循环执行。

STOP(停止)

STOP是EVM的停止指令,它的作用是停止当前上下文的执行,并成功退出。它的操作码是0x00,gas消耗为0。

STOP操作作码设为0x00有一个好处:当一个调用被执行到一个没有代码的地址(EOA),并且EVM尝试读取代码数据时,系统会返回一个默认值0,这个默认值对应的就是STOP指令,程序就会停止执行。

下面,让我们在run()函数中添加对STOP指令的处理:

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

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

elif op == STOP: # 处理STOP指令
print('Program has been stopped')
break # 停止执行

现在,我们可以尝试运行一个包含STOP指令的字节码:

# STOP
code = b"\x00"
evm = EVM(code)
evm.run()
# output: Program has been stopped

JUMPDEST(跳转目标)

JUMPDEST指令标记一个有效的跳转目标位置,不然无法使用JUMPJUMPI进行跳转。它的操作码是0x5b,gas消耗为1。

但是0x5b有时会作为PUSH的参数(详情可看黄皮书中的9.4.3. Jump Destination Validity),所以需要在运行代码前,筛选字节码中有效的JUMPDEST指令,使用ValidJumpDest 来存储有效的JUMPDEST指令所在位置。

def findValidJumpDestinations(self):
pc = 0

while pc < len(self.code):
op = self.code[pc]
if op == JUMPDEST:
self.validJumpDest[pc] = True
elif op >= PUSH1 and op <= PUSH32:
pc += op - PUSH1 + 1
pc += 1
def jumpdest(self):
pass

JUMP(跳转)

JUMP指令用于无条件跳转到一个新的程序计数器位置。它从堆栈中弹出一个元素,将这个元素设定为新的程序计数器(pc)的值。操作码是0x56,gas消耗为8。

def jump(self):
if len(self.stack) < 1:
raise Exception('Stack underflow')
destination = self.stack.pop()
if destination not in self.validJumpDest:
raise Exception('Invalid jump destination')
else: self.pc = destination

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

elif op == JUMP: 
self.jump()
elif op == JUMPDEST:
self.jumpdest()

现在,我们可以尝试运行一个包含JUMPJUMPDEST指令的字节码:0x600456005B(PUSH1 4 JUMP STOP JUMPDEST)。这段字节码将4推入堆栈,然后进行JUMP,跳转到pc = 4的位置,该位置正好是JUMPDEST指令,跳转成功,程序没有被STOP指令中断。

# JUMP
code = b"\x60\x04\x56\x00\x5b"
evm = EVM(code)
evm.run()
print(evm.pc)
# output: 5

JUMPI(条件跳转)

JUMPI指令用于条件跳转,它从堆栈中弹出两个元素,如果第二个元素(条件,condition)不为0,那么将第一个元素(目标,destination)设定为新的pc的值。操作码是0x57,gas消耗为10。

def jumpi(self):
if len(self.stack) < 2:
raise Exception('Stack underflow')
destination = self.stack.pop()
condition = self.stack.pop()
if condition != 0:
if destination not in self.validJumpDest:
raise Exception('Invalid jump destination')
else: self.pc = destination

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

elif op == JUMPI: 
self.jumpi()

现在,我们可以尝试运行一个包含JUMPIJUMPDEST指令的字节码:0x6001600657005B(PUSH1 01 PUSH1 6 JUMPI STOP JUMPDEST)。这个字节码将16推入堆栈,然后进行JUMPI,由于条件不为0,执行跳转到pc = 6的位置,该位置正好是JUMPDEST指令,跳转成功,程序没有被STOP指令中断。

# JUMPI
code = b"\x60\x01\x60\x06\x57\x00\x5b"
evm = EVM(code)
evm.run()
print(evm.pc)
# output: 7 程序没有被中断

PC(程序计数器)

PC指令将当前的程序计数器(pc)的值压入堆栈。操作码为0x58,gas消耗为2。

def pc(self):
self.stack.append(self.pc)

总结

这一讲,我们介绍了EVM中的控制流程指令,并在极简版EVM中添加了对它们的支持。这些操作为合约提供了控制流程的能力,为编写更复杂的合约逻辑提供了可能。

课后习题: 写出0x5F600557005B对应的指令形式,并给出运行后的堆栈和程序计数器状态。