WTF Opcodes极简入门: 9. 控制流指令
我最近在重新学以太坊opcodes,也写一个“WTF EVM Opcodes极简入门”,供小白们使用。
所有代码和教程开源在github: github.com/WTFAcademy/WTF-Opcodes
在这一讲,我们将介绍EVM中用于控制流的5个指令,包括STOP
,JUMP
,JUMPI
,JUMPDEST
,和PC
。我们将在用Python写的极简版EVM中添加对这些操作的支持。
EVM中的控制流
我们在第三讲介绍了程序计数器(Program Counter,PC),而EVM的控制流是由跳转指令(JUMP
,JUMPI
,JUMPDEST
)控制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
指令标记一个有效的跳转目标位置,不然无法使用JUMP
和JUMPI
进行跳转。它的操作码是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()
函数中添加对JUMP
和JUMPDEST
指令的处理:
elif op == JUMP:
self.jump()
elif op == JUMPDEST:
self.jumpdest()
现在,我们可以尝试运行一个包含JUMP
和JUMPDEST
指令的字节码: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()
现在,我们可以尝试运行一个包含JUMPI
和JUMPDEST
指令的字节码:0x6001600657005B
(PUSH1 01 PUSH1 6 JUMPI STOP JUMPDEST)。这个字节码将1
和6
推入堆栈,然后进行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
对应的指令形式,并给出运行后的堆栈和程序计数器状态。