EVM 的跳转指令 JUMP 与 JUMPI 有一条安静的安全约束:目标地址必须指向一个合法的 JUMPDEST 标记——字节值为 0x5b,且不能落在某条 PUSH 指令的立即数数据里。为了执行这条检查,客户端在第一次执行一段代码前要做一次线性扫描,标出「哪些 0x5b 是真正的跳转表、哪些只是碰巧长在数据里的字节」。EIP-7921 提议干脆取消这层区分:所有 0x5b 一律合法。提案 2025 年 3 月 26 日创建,作者 William Morriss,现在处于 Stagnant 状态。
这条校验为什么存在
字节码是一维数组,指令与数据只是同一段字节的两种读法。PUSH1 0x5b 会把一个 0x5b 字节塞进指令流,孤零零看这个字节,它与一条真正的 JUMPDEST 指令完全同值。若不检查歧义,一条计算出来的跳转可能落进「其实是数据」的字节、把后面的字节当指令继续执行——所谓「跳进数据区」的意外控制流。现行规则用一次预处理消灭这类意外:每个被执行的合约代码先扫描一遍,建立合法目标位图。代价被记录在提案动机里:其一,代码尺寸成为线性攻击面——每段新代码都要付一次与体积成正比的扫描成本;其二,任何带立即数的新操作码都要小心绕开这套分析假设;其三,CODECOPY 式把代码当数据搬运的写法会让位图「过期」,同一份字节在不同时刻有合法的跳转目标不同。
取消检查换来什么、失去什么
7921 的规格只有一句话:生效后,任何 0x5b 字节都是 JUMP/JUMPI 的合法目标。收益对应动机清单:不再需要位图预处理,代码体积与执行成本解耦,新操作码自由携带立即数。失去的是那层「意外必死」的保护:数据字节里的 0x5b 成为可落点,反汇编也不再只有一种读法——从偏移 3 读是 PUSH1 0x5b,从偏移 4 读可能是 JUMPDEST 开头的另一套指令。提案的 Backwards Compatibility 段承认,两段歧义读法通常几字节内收敛,但理论上的分歧长度无上界。编译器与审计工具因此要接受「多解字节码」的世界,这正是推进阻力所在。
一个思想实验
拿一段普通合约问两个问题:如果取消检查,它运行结果会变吗?不会——编译器生成的跳转本来就指向真正的 JUMPDEST,多出来的合法位没人去跳。变化只出现在边界:手写汇编的落点、把代码段当常数表搬运的合约、以及被刻意构造的恶意字节码。所以这份提案的争论从来不是「改不改坏现有合约」,而是「安全冗余该由协议兜底还是由工具链兜底」。历史上这条校验与 JUMPDEST 本身是同一枚硬币——2016 年的硬分叉给跳转加锚点,是为了堵住旧时代「跳到任意字节」的漏洞家族;7921 则论证锚点的检查方式比锚点本身更贵。硬币翻到哪一面,取决于下一位认领提案的人能否说服客户端团队。
一段前史的时间线
这条校验不是天经地义,2016 年的 Spurious Dragon 硬分叉才给 JUMP/JUMPI 加上「必须落在 JUMPDEST」的锚定——在那之前,跳转目标可以是任意字节,「滑进」一段精心构造的常量数据并继续执行,曾是真实攻击手法之一。当时采用的方案已经做了折中:只排除立即数内部的字节,保留其余 0x5b。7921 想做的是最后一公里的简化:连这点折中也不要。把三段历史摆在一起,能看清 EVM 安全模型的一次完整钟摆——从完全放开,到重手收紧,再到十年后论证「收紧的成本大于收益」。协议演化从来不是单行道,这条钟摆本身就是读懂 EVM 设计哲学的样本。
快速问答
问:现在的 EVM 允许跳到任意字节吗?
答:不允许。未标 JUMPDEST 或落在立即数里的字节都会让跳转失败并耗尽 gas。
问:取消检查会让已部署合约改变行为吗?
答:对按规范编译的合约无行为差异;变化集中在数据区含 0x5b 的边界写法与反汇编层面。
问:Stagnant 离上线多远? 答:无法用距离衡量——它未被排进任何升级,也未见客户端实现公告,当前只是提案档案里的一页。
风险提示
本文为虚拟机机制解读,不构成投资建议。字节码层面的规则变化可能影响合约行为,部署与审计请以当前 EVM 规范与编译器文档为准。
发表评论
还没有评论,来说两句吧。
评论区为展示样式,提交不会被处理。