EIP-4200: EOF - 静态相对跳转
带有有符号立即数的 `RJUMP`、`RJUMPI` 和 `RJUMPV` 指令,用于编码跳转目标
Authors | Alex Beregszaszi (@axic), Andrei Maiboroda (@gumb0), Paweł Bylica (@chfast) |
---|---|
Created | 2021-07-16 |
Requires | EIP-3540, EIP-3670 |
Table of Contents
摘要
引入了三个新的 EVM 跳转指令(RJUMP
、RJUMPI
和 RJUMPV
),它们将目标编码为有符号立即数。 这些指令在大多数(但不是全部)用例中都很有用,并且可以降低成本。
动机
一个反复出现讨论的话题是 EVM 只有一种用于动态跳转的机制。 它们提供了一个非常灵活的架构,只有 2 (!) 条指令。 然而,这种灵活性是有代价的:它使代码分析更加复杂,并且(部分地)导致了需要 JUMPDEST
标记。
在很多情况下,控制流实际上是静态的,不需要任何动态行为,尽管并非所有用例都可以通过静态跳转来解决。
有多种方法可以减少对动态跳转的需求,一些例子:
- 通过对函数/子程序的原生支持
- “返回调用者”指令
- 具有动态索引的“switch-case”表
此更改并未试图解决这些问题,而是引入了一个最小的功能集,以允许编译器决定哪个选项最适合给定的用例。 预计编译器将几乎专门使用 RJUMP
/ RJUMPI
,但返回调用者的情况将继续使用 JUMP
。
此功能并不妨碍 EVM 以后引入其他形式的控制流。 RJUMP
/ RJUMPI
可以有效地与更高级别的函数声明共存,其中静态相对跳转应用于函数内控制流。
这些指令的主要好处是降低了 gas 成本(在部署和执行时)并改善了分析特性。
规范
我们在激活 EIP-3540 的同一区块号上引入了三个新指令:
RJUMP
(0xe0) - 相对跳转RJUMPI
(0xe1) - 条件相对跳转RJUMPV
(0xe2) - 通过跳转表相对跳转
如果代码是旧版字节码,则所有这些指令都会导致异常停止。 (注意:这意味着行为没有改变。)
如果代码是有效的 EOF1:
RJUMP relative_offset
将PC
设置为PC_post_instruction + relative_offset
。RJUMPI relative_offset
从堆栈中弹出一个值(condition
),并将PC
设置为PC_post_instruction + ((condition == 0) ? 0 : relative_offset)
。RJUMPV max_index relative_offset+
从堆栈中弹出一个值(case
),并将PC
设置为PC_post_instruction + ((case > max_index) ? 0 : relative_offset[case])
。
立即数参数 relative_offset
被编码为 16 位有符号(二进制补码)大端值。 根据 PC_post_instruction
,我们指的是整个立即数值之后的 PC
位置。
RJUMPV
的立即数编码更加特殊:无符号 8 位 max_index
值确定跳转表中的最大索引。 后面的 relative_offset
值的数量是 max_index+1
。 这允许最大 256 的表大小。RJUMPV
的编码必须至少有一个 relative_offset
,因此它将至少占用 4 个字节。 此外,case > max_index
条件的失败意味着在许多用例中,人们会将 默认 路径放置在 RJUMPV
指令之后。 一个有趣的特性是 RJUMPV 0 relative_offset
是一个反转的 RJUMPI
,在许多情况下可以使用它来代替 ISZERO RJUMPI relative_offset
。
我们还扩展了 EIP-3670 的验证算法,以验证每个 RJUMP
/ RJUMPI
/ RJUMPV
都有一个指向指令的 relative_offset
。 这意味着它不能指向 PUSHn
/ RJUMP
/ RJUMPI
/ RJUMPV
的立即数数据。 它不能指向代码边界之外。 允许指向 JUMPDEST
,但不是必需的。
因为目标是预先验证的,所以这些指令的成本低于它们的动态对应指令:RJUMP
应该花费 2,而 RJUMPI
和 RJUMPV
应该花费 4。
原理
相对寻址
我们选择相对寻址是为了支持可重定位的代码。 这也意味着可以注入代码片段。 在此 EIP 之前,已经看到一种用于实现相同目标的技术是注入诸如 PUSHn PC ADD JUMPI
之类的代码。
我们看不到相对寻址有任何明显的缺点,它也允许我们弃用 PC
指令。
立即数大小
有符号 16 位立即数意味着可能的最大跳转距离是 32767。如果 PC=0
处的字节码以 RJUMP
开头,则可以跳转到 PC=32770
。
给定 MAX_CODE_SIZE = 24576
(在 EIP-170 中)和 MAX_INITCODE_SIZE = 49152
(在 EIP-3860 中),我们认为 16 位立即数足够大。
具有 8 位立即数的版本仅允许将 PC
向后移动 125 个字节或向前移动 127 个字节。 虽然对于许多 for 循环来说,这似乎是一个足够好的距离,但对于跨函数跳转来说,它可能不够好。 另外,由于 16 位立即数与动态跳转在此类情况下的占用空间大小相同(3 字节:JUMP PUSH1 n
),因此我们认为指令越少越好。
如果需要具有其他大小(例如 8 位、24 位或 32 位)的立即数编码,则可以引入新的操作码,类似于存在多个 PUSH
指令的方式。
PUSHn JUMP
序列
如果我们选择绝对寻址,那么 RJUMP
可以被视为类似于序列 PUSHn JUMP
(并且 RJUMPI
类似于 PUSHn JUMPI
)。 在这种情况下,有人可能会争辩说,应该对这些序列进行折扣,而不是引入新指令,因为 EVM 可以优化它们。
我们认为这是一个糟糕的方向:
- 它进一步复杂化了已经复杂的 gas 计算规则。
- 并且它要么需要共识定义的 EVM 代码内部表示,要么强制 EVM 实现自行进行优化。
这两者都存在风险。 此外,我们认为 EVM 实现应该可以自由选择它们应用的优化,并且节省的成本不需要以任何代价传递下去。
此外,它需要对当前实现进行潜在的重大更改,这些实现依赖于没有前瞻的逐个流式执行。
与动态跳转的关系
目标不是完全取代 EVM 当前的控制流系统,而是对其进行扩充。 在许多情况下,动态跳转很有用,例如返回调用者。
可以引入一种新机制,用于拥有预定义的有效跳转目标表,并动态提供此表中的索引,以完成某种形式的动态跳转。 这对于有效编码某种形式的“switch-case”语句非常有用。 它也可以用于“返回调用者”的情况,但这可能效率低下或者很尴尬。
缺少 JUMPDEST
JUMPDEST
有两个目的:
- 为了有效地划分代码——这对于预先计算给定块(即
JUMPDEST
之间的指令)的总 gas 使用量以及 JIT/AOT 转换非常有用。 - 为了明确显示有效位置(否则任何非数据位置都将有效)。
静态跳转不需要此功能,因为分析器可以在 jumpdest 分析期间轻松地区分静态跳转立即数中的目标。
这里有两个好处:
- 不浪费一个字节用于
JUMPDEST
也意味着每次跳转目标在部署期间节省 200 gas。 - 鉴于
JUMPDEST
本身花费 1 gas 并且在跳转期间被“执行”,因此每次跳转在执行期间节省额外的 1 gas。
RJUMPV
回退情况
如果在 RJUMPV
指令执行中未找到匹配项(即默认情况),执行将继续而不会分支。 这允许在参数中填充 0
,并允许程序员选择实现方式。 替代选项包括在没有匹配项时异常中止。
向后兼容性
此更改不会对向后兼容性构成任何风险,因为它是在 EIP-3540 的同时引入的。 新指令未针对旧版字节码(非 EOF 格式的代码)引入。
测试用例
验证
有效情况
- 以
JUMPDEST
作为目标的RJUMP
/RJUMPI
/RJUMPV
relative_offset
为正/负/0
- 以
JUMPDEST
以外的指令作为目标的RJUMP
/RJUMPI
/RJUMPV
relative_offset
为正/负/0
RJUMPV
具有从 1 到 256 的各种有效表大小RJUMP
作为代码段中的最后一条指令
无效情况
- 具有截断立即数的
RJUMP
/RJUMPI
/RJUMPV
RJUMPI
/RJUMPV
作为代码段中的最后一条指令RJUMP
/RJUMPI
/RJUMPV
目标超出代码段边界RJUMP
/RJUMPI
/RJUMPV
目标推送数据RJUMP
/RJUMPI
/RJUMPV
目标另一个RJUMP
/RJUMPI
/RJUMPV
立即数参数
执行
- 旧代码中的
RJUMP
/RJUMPI
/RJUMPV
中止执行 RJUMP
relative_offset
为正/负/0
RJUMPI
relative_offset
为正/负/0
condition
等于0
condition
不等于0
RJUMPV 0 relative_offset
case
等于0
case
不等于0
RJUMPV
具有包含正、负、0
偏移的表case
等于0
case
不等于0
case
超出表边界 (case > max_index
, 回退情况)case
> 255
安全考虑
实现 EOF 容器验证算法时,应仔细考虑添加带有立即数参数的新指令。
静态相对跳转执行不需要运行时检查跳转目标。 它大大降低了执行成本。 因此,新指令的 gas 成本也可以大大降低。
RJUMPV
指令相对偏移表最多可以有 256 个单字节条目,因此读取偏移量不可能是潜在的攻击面。
版权
在 CC0 下放弃版权及相关权利。
Citation
Please cite this document as:
Alex Beregszaszi (@axic), Andrei Maiboroda (@gumb0), Paweł Bylica (@chfast), "EIP-4200: EOF - 静态相对跳转 [DRAFT]," Ethereum Improvement Proposals, no. 4200, July 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4200.