三条指令给EVM装函数:CALLSUB、CALLDEST与RETURNSUB 图 1
三条指令给EVM装函数:CALLSUB、CALLDEST与RETURNSUB · 图 1

EVM的字节码里一直有个结构性缺口:JUMP能跳进去,却跳不回来。合约里复用一段逻辑,要么靠内联复制让代码变肥,要么付出一次跨合约调用的全部开销——哪怕那段逻辑就在同一个合约身上。EIP-7979在2025年12月提出,用三条新操作码填这个洞:CALLSUB负责调用、CALLDEST标记入口、RETURNSUB负责回家。它刻意不走EOF容器格式那条重路线,是给现有字节码打的最小补丁。

三条指令各管一段

CALLSUB从数据栈顶取一个数字作为跳转目标,把控制权交过去,同时把返回地址压进一条专门的返回栈。注意目标来自数据栈而不是指令里的立即数——这意味着子程序的地址可以是运行时算出来的,也意味着它和JUMP一样属于动态跳转。CALLDEST只做一件事:标记这里是子程序入口。它和JUMPDEST是近亲,本身是空操作,执行流从它头顶直接走过去;区别在于它是CALLSUB唯一合法的落点,目标不是CALLDEST的调用直接触发异常停机。RETURNSUB则从返回栈弹出之前记下的地址,把控制权交回去。

返回栈是这三条指令的心脏,容量被限制在一千零二十四条。栈满还要再调用、或者返回栈空了还要RETURNSUB,都属于例外停机的情形——提案的测试用例专门列了这两个失败场景。还有一条容易被忽略的规则:CALLDEST同时也是合法的JUMP和JUMPI目标,从JUMP进去不会往返回栈记账,在这段子程序里走到RETURNSUB就会撞上空栈异常。设计者选择宁可让这种误用立刻炸掉,也不给它一个含糊的兜底行为。

Gas上提案走极简路线:费用表里CALLSUB被放在中档(原文标注mid),三条指令加起来的开销和一次内存操作同量级,和动辄几千Gas的跨合约CALL相比是零头。

为什么不动容器格式

以太坊历史上给函数问题开过好几张药方。2019年的EIP-2315想给JUMP配一条返回栈,最终被撤回,理由是等EOF的函数机制;后来的EIP-4750想把函数装进独立的代码段;EIP-4200则走静态相对跳转,至今停在停滞状态。这些路线的共同前提是给合约换一种容器格式,改动横跨编译器和节点。EIP-7979反其道而行:不引入代码段、不带立即数、不碰容器,只在现有扁平字节码上加三条指令和一条栈。提案的问答小节把这称为克制的代价——动态跳转无法做静态验证,换来的是几乎为零的迁移成本,以及ZK-rollup一侧对更规整控制流的期待。

一个直觉账:一个被调用一千次的辅助函数,内联复制让部署代码多长将近千倍于原长度,跨合约调用每次再叠上各自固定的开销;换成CALLSUB,代价只是每多维护一条返回地址。

快速问答

问:对普通用户能感知到什么变化? 答:短期没有。这是编译器层面的变化,受益的是合约体积与内部调用Gas,用户看到的只是同样功能合约可能部署更便宜、执行更省一点。

问:它和EIP-4200是竞争关系吗? 答:互补里带竞争。4200解决跳远问题(静态偏移跳转),7979解决有去有回问题;两者可以叠加,但都指向同一个目标——给EVM的控制流补基本功。

问:怎么判断这条提案走到哪一步了? 答:操作码编号至今写成占位形式,说明仍在早期设计阶段;以官网页面状态与讨论链接为准。

一次失败的旅程

按提案自己列的测试用例,把控制权交给一个数字上看起来合理、其实指向普通指令中间的地址,CALLSUB会当场停机;跳过调用直接执行RETURNSUB,同样停机。这种宁可炸掉不可含糊的取向和EVM一贯的确定执行哲学一致:每一种失败都必须是可预期、可复现的,节点之间永远不会因为实现差异给出两种结果。对编译器作者来说,这是好消息——错误代码在部署前测试网阶段就能以异常形式暴露出来。

一条判断线

EVM的升级史反复证明:越小的改动越容易先落地,越大的愿景往往被切成一串小步。CALLSUB三条指令就是这种切法的最新样本——函数这个老概念,用最原始的手段先装回去一半。

风险提示:本文内容为协议机制科普,不构成任何投资建议;提案参数与状态可能变化,请以官方规格文档为准。