为什么分析一段字节码会走到指数爆炸:EVM 控制流之困的根因与路线之争 图 1
为什么分析一段字节码会走到指数爆炸:EVM 控制流之困的根因与路线之争 · 图 1

任何要”读懂”一段合约的工具——反编译器、安全扫描器、即时编译器——都得先画出一张程序可能走的路线地图,术语叫控制流图:节点是一块块只有单入口单出口的代码,边是每次跳转可能去的方向。地图画得全不全、代价多大,取决于跳转目的地是写死的还是现场算的。目的地写死的叫静态跳转,现场从栈上取数的叫动态跳转。Java 字节码、WebAssembly 与 .NET 的公共语言运行时有一致立场:代码加载时校验、线性时间编译,所以要么禁止动态跳转要么严格限制。EVM 站在对面:它唯一的跳转指令从栈顶取目的地,也没给函数调用与返回任何专用设施,编译器只能用普通数据把返回地址压栈、到时候跳回去。EIP-8173 由 Greg Colvin 在 2026 年 2 月提交,是一篇不提案改任何东西的资料性文件,专门把这件事的账算给你看,现标 Draft。

从分析代价说起

账分三层。第一层,画地图:既然任何跳转都可能落在任何跳转标号上,边数就是跳转数乘以标号数——平方量级,代码长一倍、边可能多四倍。提案给出一族可以任意拉长的示例程序,三行一组的标号、取剩余燃料、跳转,长度增加时边的缠绕比程序变长更快。第二层,走路线:画完地图还要顺着每种可能的执行路径走一遍的工具,每遇一个未知目的地就分叉一次,分叉复合起来的路径数按论文原话是指数乃至阶乘增长——二十个内部函数调用就是百万量级的遍历,文献里的名字叫路径爆炸。第三层,也是最疼的:这不只是离线研究难题。部署合约时做代码校验、运行时做即时编译,都属于在线静态分析——动态跳转等于给每个分析器埋一个拒绝服务漏洞。反汇编领域的学术文献攒了几十年复杂而不完整的解法,最新的神经网络方案也只能反汇编大部分 Solidity 程序,论文摘要在提案里被逐条引用。

为什么分析一段字节码会走到指数爆炸:EVM 控制流之困的根因与路线之争 图 2
为什么分析一段字节码会走到指数爆炸:EVM 控制流之困的根因与路线之争 · 图 2

历史为什么走到这一步

资料的另一半是来路。蒸汽时代的分析机就用卡片实现条件跳转,程序即卡片顺序本身;1946 年图灵在自动计算引擎方案里写下调用与返回的原型——离开主程序时做个记号,从副程序第一条指令继续,回来后按记号接续,他的机器专设一条返回栈,与数据隔开。此后八十年,主流机器的返回地址从代码自修改、到链接寄存器、再到与数据同栈的栈帧——最后一种被 C 语言普及,也被栈溢出攻击 滥用了数十年。虚拟机的答案分两路:Java 与 WebAssembly 干脆把返回做成结构化指令;Forth 则像图灵的机器一样保留密封的返回栈。EVM 两条都没选,它把函数调用留给编译器用动态跳转自行合成——于是每一条已部署合约里的每个内部函数调用,都是地图上一条分析工具无法预知的边。

谱系与取舍

提案把历次改进串成一条线:早年的子程序与静态跳转提案、容器格式与类型化函数、更晚的显式调用与返回指令——每一步都在把动态边改写成静态边,也都撞上同一堵兼容性墙:已部署字节码不能失效,新设施只能进新格式容器。资料性提案的全部论点是:与其在编译器、扫描器里继续发明绕路的解法,不如让虚拟机的控制流结构本身配得上它的资产规模。能否说服路线图还得再看,这份文件至少把问题从各说各话的行话里打捞出来,摊在图论、编译与安全三方都认得的坐标上。

快速问答

问:为什么说”跳转目标来自栈”比”目标随机”更准确? 答:运行时它往往就是编译器埋的合法标号,并不随机;难在分析器无法先验地证明这一点,必须把机器允许的所有可能性都当真。

问:Solidity 源码读者需要关心这些吗? 答:间接的——工具对你编译产物的分析精度、误报率与校验耗时,都由这份控制流结构决定,而非源码语言本身。

风险提示:本文为协议研究科普,涉及提案状态以提案页面为准;不构成投资建议。