本文介绍了在EOF格式的字节码中实现多个代码段的能力,主要是通过引入新的操作码CALLF
和RETF
来实现对函数的调用和返回。该EIP旨在消除动态跳转的需要,并通过编码输入和输出数量来提高代码分析的机会,同时限制每个函数的栈隔离。
引入在 EOF 格式的 (EIP-3540) 字节码中具有多个代码部分的能力,每个代码部分表示一个独立的子程序/函数。引入两个新指令,CALLF
和 RETF
,分别用于调用和返回此类函数。动态跳转指令是不允许的。
当前,在 EVM 中,一切都是动态跳转。像 Solidity 这样的语言以静态方式生成大多数跳转(即,目标在跳转前推入栈中,PUSHn .. JUMP
)。然而,不幸的是,大多数 EVM 解释器无法使用此方法,因为需要额外的验证/分析。这也限制了它们进行优化并可能降低跳转成本的能力。
EIP-4200 引入静态跳转指令,消除了对 大多数 动态跳转用例的需求,但并非所有情况都能通过它们解决。
本 EIP 的目标是消除需求并禁止动态跳转,因为它提供了这些跳转所用于的最重要的特性:调用和返回函数。
此外,它旨在通过对每个给定函数的输入和输出数量进行编码并隔离每个函数的栈(即,一个函数无法读取调用者/被调用者的栈),来改善分析机会。
EOF 容器的类型部分必须遵循以下要求:
n * 4
字节,其中 n
是代码部分的数量。inputs
、一个 uint8 outputs
和一个 uint16 max_stack_height
。注意: 这暗示输入和输出的栈有 255 的限制。进一步限制为 127 的栈项,因为输入和输出字节的高位保留供将来使用(outputs == 0x80
在 EOF1 中已用于表示不返回的函数,如在单独的 EIP 中介绍)。max_stack_height
在 EIP-5450 中进一步定义。参见 EIP-3540 以查看完整的良构 EOF 字节码结构。
引入了一个返回栈,与操作数栈分开。它是一个项目栈,代表函数执行结束后将返回的执行状态。每个项目由代码部分索引和代码部分中的偏移量(PC 值)组成。
注意:实现可以自由选择栈项的特定编码。在以下规范中,我们假设表示为两个无符号整数:code_section_index
、offset
。
返回栈的最大限制为 1024 项。
此外,EVM 会跟踪当前正在执行的部分的索引 - current_section_index
。
我们引入两个新指令:
CALLF
(0xe3
) - 调用一个函数RETF
(0xe4
) - 从一个函数返回如果代码是传统字节码,任何这些指令将导致 异常暂停。(注意:这意味着行为没有变化。)
首先我们定义几个辅助值:
type[i].inputs = type_section_contents[i * 4]
- 第 i 个代码部分的输入数量type[i].outputs = type_section_contents[i * 4 + 1]
- 第 i 个代码部分的输出数量type[i].max_stack_height = type_section_contents[i * 4 + 2:i * 4 + 4]
- 第 i 个代码部分的最大操作数栈高度如果代码是有效的 EOF1,适用以下执行规则:
CALLF
target_section_index
,编码为 16 位无符号大端值。1024 - type[target_section_index].max_stack_height + type[target_section_index].inputs
(即,如果被调用的函数可能超过全局栈高度限制),执行将导致异常暂停。这也保证了调用后的栈高度在限制内。1024
项,执行将导致异常暂停。向返回栈推送一个项:
(code_section_index = current_section_index,
offset = PC_post_instruction)
在 PC_post_instruction
下,我们指的是 CALLF
的整个立即参数之后的 PC 位置。
注意: EOF 验证 EIP-5450 保证在 CALLF
之后总是有一条指令(因终止指令或无条件跳转要求为该部分的最后一条指令),因此 PC_post_instruction
总是指向部分范围内的指令。
current_section_index
设置为 target_section_index
并将 PC
设置为 0
,然后在被调用的部分继续执行。RETF
current_section_index
和 PC
设置为此项中的值。注意: 对于第 0 个代码部分不返回的验证要求(单独 EIP 中介绍的不返回部分)保证在 RETF
之前返回栈不能是空的。
除了上述容器格式验证规则外,我们扩展了代码部分验证规则(如 EIP-3670 中定义)。
CALLF
的立即参数大于或等于代码部分的总数,则该代码部分无效。RJUMP
、RJUMPI
和 RJUMPV
立即参数值(跳转目的地相对偏移)验证:
CALLF
指令的两个字节之一,则代码部分无效。CALLF
/ JUMPF
(JUMPF
在单独的 EIP 中介绍)指令从第 0 个代码部分访问(第 0 个代码部分总是可以访问)。动态跳转指令 JUMP
(0x56
) 和 JUMPI
(0x57
) 是无效的,其操作码未定义。
JUMPDEST
(0x5b
) 指令被重新命名为 NOP
(“无操作”),且其行为未改变:它不会弹出任何内容,也不会向操作数栈推送任何内容,除了 PC 增加和收取 1 个 gas 外没有其他效果。
PC
(0x58
) 指令变为无效,其操作码未定义。
注意: 此更改意味着不再需要对 EOF 代码进行 JUMPDEST 分析。
CALLF
时,如上所述。RETF
结束执行 VS 异常暂停 VS 在验证期间不被允许顶层帧中 RETF
的替代逻辑可能是允许在代码验证期间使用它,并使其:
RETF
被清空而结束执行,或RETF
之前为空,则异常暂停。这已被顶层帧(第 0 个代码部分)为不返回部分的验证规则所取代(不返回的部分在单独的 EIP 中介绍),因为验证函数的不返回状态本身出于其他原因是有价值的。因此,在顶层帧中,RETF
的所有运行时行为考虑已过时。
让我们考虑一个只有单个指令 RETF
的简单函数。
这样的函数具有“最小”类型,inputs = 0, outputs = 0
。
然而,任何其他类型如 inputs = k, outputs = k
对于这样的函数也是有效的。
曾考虑强制所有函数使用“最小”类型。
这需要额外的验证规则,以检查函数中的任何指令是否访问底部栈操作数。
此规则可以被编译器遵守,但会造成相当大的烦恼。
另一方面,它对 EVM 实现几乎没有好处。
最后,决定不强制遵守。
代码部分的数量限制为 1024。这要求 CALLF
需要 2 字节的立即数,并留有将来增加限制的空间。讨论了 256 的限制(1 字节的立即数),并提出了可能不足的担忧。
NOP
指令我们不是废除 JUMPDEST
,而是将其重新赋予 NOP
指令,因为 JUMPDEST
实际上是一个“无操作”指令,已经在各种上下文中被用作这样的用途。这对某些链下工具可能很有用,例如基准测试 EVM 实现(NOP
指令的性能是 EVM 解释器循环的性能),作为强制代码对齐的填充,作为动态代码组合中的占位符。
JUMPDEST
分析的目的是在代码中找到不恰好位于 PUSH
立即数中的有效 JUMPDEST
字节。只有动态跳转指令(JUMP
、JUMPI
)要求目的地为 JUMPDEST
指令。相对静态跳转(RJUMP
和 RJUMPI
)不具有此要求,并在部署时 EOP 指令验证中验证。因此,在没有动态跳转指令的情况下,不再需要 JUMPDEST 分析。
此更改对向后兼容性没有风险,因为它只针对 EOF1 合约引入,部署未定义指令是不允许的,因此没有现有合约使用这些指令。新指令未针对传统字节码引入(未格式化为 EOF 的代码)。
新的执行状态和多部分控制流程对向后兼容性没有风险,因为它是执行单个代码部分的广义化。执行现有合约(无论是传统合约还是 EOF1 合约)没有用户可观察的变化。
待补充
通过 CC0 放弃版权及相关权益。
- 原文链接: github.com/ethereum/EIPs...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!