为 EVM 引入静态相对跳转与调用:EIP-8013(RJUMP / RJUMPI / RJUMPV / RJUMPSUB / RJUMPSUBV)完整解析
【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
EIP-8013 为以太坊虚拟机(EVM)新增五条以有符号立即数编码跳转目标地址的指令——RJUMP、RJUMPI、RJUMPV、RJUMPSUB、RJUMPSUBV,让编译器能够在绝大多数JUMP/JUMPI场景下使用静态跳转替代动态跳转,从而在部署与执行阶段降低 gas 消耗、提升执行性能并大幅改善静态分析的可处理性。读完本文,你将掌握这五条指令的完整语义、立即数编码规则、与 EIP-7979 子程序机制(return stack /ENTERSUB/RETURNSUB)的衔接方式、验证算法扩展、gas 成本建议,以及配套的验证与执行测试用例,并能在此基础上评估该提案对 EVM 控制流演进的影响。
背景与动机:为什么 EVM 需要静态跳转
EVM 目前只提供动态跳转机制:JUMP与JUMPI的跳转目标从数据栈(data stack)取值,指令本身不携带任何目标信息。这种设计只用两条指令就支撑了极其灵活的控制流,但代价是代码分析变得困难甚至不可解,也直接导致了JUMPDEST标记的诞生。
在绝大多数实际场景中,控制流本质上是静态的——跳转目标在字节码编译期就已确定,根本不需要动态行为。EIP-8013 的提案者总结了几类可以减少动态跳转需求的手段:
- 对函数 / 子程序的原生支持;
- 一种"返回到调用者"的指令——即子程序返回(subroutine return);
- 支持动态索引的"switch-case"跳转表。
本提案引入一个最小特性集(恰好涵盖以上三点),使编译器可以只使用RJUMP/RJUMPI/RJUMPSUB就完成全部内部控制流的表达。同时,该功能不排斥 EVM 未来引入其他形式的控制流,也不排斥存量(legacy)代码继续运行:RJUMP/RJUMPI可以与更高级的函数声明方式高效共存,用于函数内部的控制流。
新指令带来的核心收益有三点:部署与执行阶段的 gas 成本更低、执行性能更好、静态分析性质更优。
关于静态控制流更完整的历史脉络、技术基础及其对以太坊扩容路线图(ZK-Rollup、Optimistic Rollup、RISC-V 迁移)的影响,可参阅同仓库的姊妹文档 EIPS/eip-8173.md(Static Control Flow for the EVM,Informational 类型)。该文档还附带了一个可复现的实验 assets/eip-8173/walk-count/README.md:对同一程序分别用传统
jump约定与调用/返回指令编写,统计分析器必须执行的路径遍历次数——jump版本遍历次数按1 + K + K² + … + K^K增长(20 次共享调用约为 10²⁶ 次遍历),而调用/返回版本恒为 1 次。
规范:五条新指令
提案引入五条新指令,每条指令要么携带一个两字节相对偏移,要么携带一个偏移表作为立即数(immediate):
| 指令 | Opcode | 立即数 | 栈操作 | 语义 |
|---|---|---|---|---|
RJUMP | 0xe0 | relative_offset | 无 | 将PC设为PC_post_instruction + relative_offset |
RJUMPI | 0xe1 | relative_offset | 弹出condition | 将PC设为PC_post_instruction + ((condition == 0) ? 0 : relative_offset) |
RJUMPV | 0xe2 | max_index relative_offset+ | 弹出case | 将PC设为PC_post_instruction + ((case > max_index) ? 0 : relative_offset[case]) |
RJUMPSUB | 0xe3 | relative_offset | 无 | 将PC_post_instruction压入return stack,并将PC设为PC_post_instruction + relative_offset(一个ENTERSUB,等价于以CALLSUB调用) |
RJUMPSUBV | 0xe4 | max_index relative_offset+ | 弹出case | 将PC_post_instruction压入 return stack,并将PC设为PC_post_instruction + ((case > max_index) ? 0 : relative_offset[case])(一个ENTERSUB,等价于以CALLSUB调用) |
立即数编码规则
relative_offset是一个16 位有符号(二进制补码)大端序值;- 所谓
PC_post_instruction,指的是整个立即数之后的PC位置(即该指令占用完毕后 PC 所指向的下一条指令位置); RJUMP的跳转距离为有符号 16 位所能表示的范围,最大正向跳转距离为32767。若PC=0处的字节码以RJUMP开头,则最远可跳到PC=32770。
RJUMPV与RJUMPSUBV的特殊编码
RJUMPV与RJUMPSUBV的立即数编码更为特殊:
- 紧跟 opcode 的是一个无符号 8 位
max_index,决定跳转表的最大索引; - 随后跟有
max_index + 1个relative_offset值,即跳转表最多可容纳 256 个表项; RJUMPV的编码必须至少包含一个relative_offset,因此其最小长度为4 字节(1 字节 opcode + 1 字节max_index+ 至少 2 字节偏移);- 当
case > max_index(索引越界)时,控制流直接顺序落入下一条指令(fall-through)。这意味着在大多数使用场景下,程序员会把default路径紧跟在RJUMPV指令之后放置; - 一个值得注意的用法是:
RJUMPV 0 relative_offset等价于一个取反的RJUMPI,可在许多场合替代ISZERO RJUMPI relative_offset的组合。
与子程序机制的衔接(依赖 EIP-7979)
RJUMPSUB与RJUMPSUBV的目的地址MUST 是ENTERSUB(子程序入口标记),并且控制权通过RETURNSUB返回到发起调用的RJUMPSUB/RJUMPSUBV。这两条指令相当于直接访问了 EIP-7979 中CALLSUB指令底层的"返回调用者"机制。
EIP-7979 为 EVM 引入的机器状态包括:
- return stack(返回栈):仅由
CALLSUB压入、仅由RETURNSUB弹出,最深 1024 项,EVM 代码无法直接访问(不能读、改、移动),从而消除了代码自身篡改控制流的整类漏洞; CALLSUB:从数据栈弹出目标地址压入返回栈并跳转,目标必须是CALLDEST,gas 为mid(8);CALLDEST:子程序入口标记,功能类似JUMPDEST的空操作,gas 为jumpdest(1);RETURNSUB:从返回栈弹出地址写入PC,返回栈为空则异常停机,gas 为low(5)。
需要说明的是,EIP-8013 规范正文中统一使用ENTERSUB指代子程序入口目标("anENTERSUB, as if withCALLSUB"),其底层即 EIP-7979 所描述的调用目标标记机制。EIP-7979 的参考实现(以 EELS Python 执行规范形式给出,见 EIPS/eip-7979.md 第 333-401 行)展示了Evm状态如何新增return_stack字段、jumpdest 分析如何扩展出valid_call_destinations集合,以及三条指令的逐条实现,可作为理解本提案底层机制的直接参考。
验证算法扩展
本提案同时扩展了 EIP-7979 的验证算法,要求对每一条RJUMP/RJUMPI/RJUMPV/RJUMPSUB/RJUMPSUBV校验其relative_offset指向一条指令:
- 不能指向
PUSHn/RJUMP/RJUMPI/RJUMPV的立即数数据; - 不能指向代码边界之外;
- 进一步地,所有且仅有
RJUMPSUB与RJUMPSUBV的relative_offset必须指向ENTERSUB;而其余指令(RJUMP/RJUMPI/RJUMPV)允许指向JUMPDEST,但并不要求必须如此。
由于跳转目标在 jumpdest 分析阶段即被校验完毕,运行时无需再次检查,这正是静态跳转相比动态跳转在 gas 和执行开销上的优势来源。
Gas 成本
由于跳转目标在 jumpdest 分析阶段已一次性校验,运行时无需重复检查,因此这些指令的成本可以低于其动态对应物。提案建议:
| 指令 | 建议 gas |
|---|---|
RJUMP | 2 |
RJUMPI/RJUMPV | 4 |
RJUMPSUB/RJUMPSUBV | 5 |
作为对比,当前动态跳转JUMP的成本为mid(8)gas,JUMPDEST为 1 gas。
设计权衡(Rationale)
相对寻址(Relative addressing)
选择相对寻址是为了支持可重定位代码(relocatable code),这意味着代码片段可以被注入。在本提案之前,业界已有通过注入PUSHn PC ADD JUMPI来实现同样目标的技巧。相对寻址没有明显缺点,并且它使得PC指令有被废弃的可能。
立即数大小(Immediate size)
有符号 16 位立即数意味着最大跳转距离为 32767。结合 EIP-170 的MAX_CODE_SIZE = 24576与 EIP-3860 的MAX_INITCODE_SIZE = 49152,16 位立即数被认为足够使用。
若改用 8 位立即数,PC 最多只能向后移动 125 字节、向前移动 127 字节——对许多 for 循环足够,但不足以支撑跨函数跳转。而且 16 位立即数与动态跳转在此类场景下的占用完全一致(JUMP PUSH1 n恰好 3 字节),因此"指令更少"是更优选择。若未来确有其他尺寸(8 位、24 位、32 位)立即数的需求,可以像多条PUSH指令那样引入新的 opcode。
PUSHn JUMP序列的处理
如果选择绝对寻址,RJUMP可被视为PUSHn JUMP序列的等价物(RJUMPI类似PUSHn JUMPI)。那样的话,一种观点认为不必引入新指令,只需为这类序列提供 gas 折扣、由 EVM 实现自行优化即可。
提案认为这不是好方向,并将现有PUSHn JUMP序列的语义定义留给 EIP-7979,不在本提案中优化其 gas,理由如下:
- 会进一步复杂化本就繁琐的 gas 计算规则;
- 要么需要在共识层定义 EVM 代码的内部表示,要么强迫 EVM 实现各自做优化——两者都有风险;
- EVM 实现应当自由选择应用何种优化,优化收益并不必须全部让渡给用户;
- 还会要求当前依赖"流式逐字节执行、无前瞻"的实现做出重大改动。
无需JUMPDEST(Lack ofJUMPDEST)
JUMPDEST有两个用途:
- 高效划分代码块——可用于预先计算某个block(即两个
JUMPDEST之间的指令)的总 gas,以及 JIT/AOT 翻译; - 显式标记合法跳转位置(否则任何非数据位置都可能是跳转目标)。
对静态跳转而言,这两点都不需要:分析器在 jumpdest 分析阶段可以直接从静态立即数识别跳转目标。由此带来两个直接收益:
- 每个跳转目标省掉 1 字节的
JUMPDEST,部署时每个目标节省 200 gas; - 执行时每次跳转额外节省 1 gas(
JUMPDEST自身消耗 1 gas 且跳转时会"执行"到它)。
RJUMPV/RJUMPSUBV的默认分支(fallback case)
当RJUMPV或RJUMPSUBV的case在表中找不到匹配(即default情形)时,执行继续顺序进行、不分支。这种设计允许用0填充跳转表中的空隙,并让程序员自由选择实现方式。另一种备选方案是在无匹配时触发异常中止,但被本提案放弃。
向后兼容
本变更对向后兼容性不构成任何风险。新指令是全新引入的 opcode,对现有 EVM 代码的语义没有任何改动。
测试用例
提案给出了完整的验证与执行测试用例清单(详见 EIPS/eip-8013.md 的Test Cases一节),可作为实现与测试驱动开发(TDD)的直接输入。
验证(Validation)
有效用例(Valid cases):
RJUMP/RJUMPI/RJUMPV以JUMPDEST为目标,且relative_offset分别为正、负、0;RJUMP/RJUMPI/RJUMPV以非JUMPDEST指令为目标,且relative_offset分别为正、负、0;RJUMPV/RJUMPSUBV的各种合法表大小(1 到 256);RJUMP作为代码段中的最后一条指令。
无效用例(Invalid cases):
RJUMP/RJUMPI/RJUMPV立即数被截断(truncated);RJUMPI/RJUMPV作为代码段中的最后一条指令;RJUMPSUB/RJUMPSUBV的目标不是ENTERSUB;- 所有五条指令的目标超出代码段边界;
- 所有五条指令的目标指向 push 数据;
- 所有五条指令的目标指向另一条
RJUMP/RJUMPI/RJUMPV的立即数参数。
执行(Execution)
RJUMP:relative_offset分别为正、负、0;RJUMPI:relative_offset分别为正、负、0,且分别覆盖condition == 0与condition != 0;RJUMPV 0 relative_offset:case为0与非0;RJUMPV表内含正、负、0偏移:case为0、非0、越界(case > max_index落入 default)、case > 255;RJUMPSUB:relative_offset分别为正、负、0;RJUMPSUBV 0 relative_offset:case为0与非0;RJUMPSUBV表内含正、负、0偏移:case为0、非0、越界、case > 255。
安全考虑
- 验证算法是安全核心:新增带立即数的指令,实现验证算法时必须谨慎。静态相对跳转的执行不需要在运行时检查跳转目标,这大幅降低了执行成本,也因此允许显著降低新指令的 gas;
- DoS 面可控:
RJUMPV与RJUMPSUBV的相对偏移表最多包含 256 个表项,读取一个偏移量不可能成为潜在的 DoS 攻击面。
从更广的视角看,静态控制流把"跳转目标"从运行时数据(数据栈)搬移到了指令编码本身,使代码的调用结构对分析工具、编译器与审计工具显式可见。这一点与 EIP-7979 的 return stack 设计一脉相承:返回地址存放在 EVM 代码不可触及的独立返回栈中,从机制上杜绝了数据覆盖返回地址这类控制流破坏攻击。
在仓库中的延伸阅读
- 提案正文:EIPS/eip-8013.md;
- 底层调用/返回机制(return stack、
CALLSUB/CALLDEST/RETURNSUB及 EELS 参考实现):EIPS/eip-7979.md; - 控制流基础与扩容路线图(Informational 背景文档):EIPS/eip-8173.md,其 CFG 复杂度图示见 assets/eip-8173/control-flow.svg,路径遍历计数实验见 assets/eip-8173/walk-count/README.md(可用
python3 walker.py复现); - EIP-7979 配套的 RISC-V 基准测量(解释执行、寄存器 IR、AOT 编译三档对比):assets/eip-7979/riscv/README.md,源码位于 assets/eip-7979/riscv/(
aot.py、interp.c、ir.py等)与 Yul 编译器/验证器 assets/eip-7979/yul-compiler/; - 历史前身:EOF 容器中的静态相对跳转提案 EIPS/eip-4200.md(
RJUMP/RJUMPI/RJUMPV三指令的最早形态,仅限 EOF1 代码);EIP-8013 将其推广为不依赖 EOF 的通用指令,并新增子程序跳转形态RJUMPSUB/RJUMPSUBV。
小结
EIP-8013 以最小化的五条指令,把 EVM 的控制流从"只有动态跳转"推进到"静态为主、动态为辅"的形态:RJUMP/RJUMPI覆盖绝大部分条件与无条件分支,RJUMPV以跳转表实现 switch-case 与默认分支,RJUMPSUB/RJUMPSUBV借助 EIP-7979 的 return stack 机制实现可重入的子程序调用与返回。所有跳转目标在验证阶段一次性确认,运行时零检查,换来部署与执行阶段的双重 gas 节省、更优的静态分析性质,以及向 JIT/AOT 编译、RISC-V 迁移与 ZK 证明效率提升等方向展开的演进空间。对实现者而言,EIPS/eip-8013.md 中详尽的验证/执行测试用例与 EIPS/eip-7979.md 的 Python 参考实现,构成了从语义到落地的完整闭环。
【免费下载链接】EIPsThe Ethereum Improvement Proposal repository项目地址: https://gitcode.com/GitHub_Trending/ei/EIPs
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考