EVM 字节码反编译终局:从三地址码到抽象语法树(AST)与 Solidity 源码重构

发布时间:2026/10/20 10:17:09
EVM 字节码反编译终局:从三地址码到抽象语法树(AST)与 Solidity 源码重构 在智能合约安全审计、闭源协议逆向分析以及黑客链上资产追踪过程中安全工程师面对的往往只是一串十六进制的 EVM 运行时字节码Runtime Bytecode。由于以太坊虚拟机EVM采用了准图灵完备的基于栈Stack-based架构没有通用寄存器所有变量计算均通过入栈PUSH、出栈POP、复制DUP与交换SWAP完成这使得直接阅读反汇编指令极其晦涩繁琐。要实现从死板的操作码Opcodes到高可读性的高级 Solidity 源码还原必须经历编译器逆向流水线操作码切分 $\rightarrow$ 基本块与控制流图CFG构建 $\rightarrow$ 栈平衡分析与三地址码TAC生成 $\rightarrow$ 数据流与变量定值使用链UD-Chains推演 $\rightarrow$ 抽象语法树AST重构。本文将系统拆解 EVM 反编译器的底层理论架构并使用 Python 完整实现一套从原始字节码恢复三地址码与表达式树的核心逆向引擎。EVM 反编译的核心技术管线1. 控制流图与基本块划分Basic Blocks CFG基本块是指一段连续执行、只有一个入口和一个出口的指令序列。在 EVM 字节码中基本块的边界由以下指令界定终结指令JUMP、JUMPI、STOP、RETURN、REVERT、SELFDESTRUCT、INVALID。入口指令紧随终结指令之后的第一条指令或者任何作为有效跳转目标的指令必须以JUMPDEST为开头。通过遍历字节码将线性序列切分为由有向边连接的基本块网络就形成了控制流图CFG。2. 栈模拟与三地址码转换Stack Simulation to TACEVM 的核心痛点是栈深度动态变化。例如代码序列PUSH1 0x04 CALLDATALOAD PUSH1 0x20 ADD在栈式架构中中间结果没有名字。三地址码TAC通过引入虚拟寄存器如 $v_0, v_1, \dots$消除对栈的物理依赖v0 CALLDATALOAD(0x04) v1 ADD(v0, 0x20)为了实现这一步反编译器在遍历基本块内的指令时维护一个虚拟符号栈Symbolic Stack。每当遇到计算指令从符号栈弹出对应数量的操作数赋予一个全新临时变量并将该临时变量重新压入符号栈。3. 数据流分析与抽象语法树AST折叠三地址码虽然消除了栈但仍然包含大量冗余的单步临时变量。通过定值-使用链分析Def-Use Analysis若变量 $v_0$ 仅被使用一次且使用点与定义点之间没有发生状态或内存副作用即可将其向上内嵌Inlining并合成为表达式树$$v_{target} \text{Add}(\text{CalldataLoad}(0x04), 0x20)$$进一步对控制流图中的循环结构天然形成的强连通分量和条件分支JUMPI进行模式识别即可重构出高级的if-else、while循环与函数选择器调度分发器Dispatcher。核心逆向引擎实现从字节码到 TAC 与语法树以下是用 Python 实现的高性能 EVM 字节码符号反编译核心演示了如何将字节码转换为结构化的三地址码并折叠为高阶表达式from typing import List, Dict, Tuple, Any # 基础操作码定义表 (操作码 - (助记符, 弹出栈参数数, 压入栈结果数)) OPCODES { 0x00: (STOP, 0, 0), 0x01: (ADD, 2, 1), 0x02: (MUL, 2, 1), 0x03: (SUB, 2, 1), 0x04: (DIV, 2, 1), 0x10: (LT, 2, 1), 0x11: (GT, 2, 1), 0x14: (EQ, 2, 1), 0x15: (ISZERO, 1, 1), 0x16: (AND, 2, 1), 0x17: (OR, 2, 1), 0x20: (SHA3, 2, 1), 0x35: (CALLDATALOAD, 1, 1), 0x50: (POP, 1, 0), 0x54: (SLOAD, 1, 1), 0x55: (SSTORE, 2, 0), 0x56: (JUMP, 1, 0), 0x57: (JUMPI, 2, 0), 0x5b: (JUMPDEST, 0, 0), 0xf3: (RETURN, 2, 0), 0xfd: (REVERT, 2, 0), } class Instruction: def __init__(self, pc: int, opcode: int, mnemonic: str, operand: bytes b): self.pc pc self.opcode opcode self.mnemonic mnemonic self.operand operand def __repr__(self): if self.operand: return f{hex(self.pc)}: {self.mnemonic} 0x{self.operand.hex()} return f{hex(self.pc)}: {self.mnemonic} class EVMDecompiler: def __init__(self, hex_code: str): if hex_code.startswith(0x): hex_code hex_code[2:] self.bytecode bytes.fromhex(hex_code) self.instructions: List[Instruction] [] self.var_counter 0 def disassemble(self) - List[Instruction]: 将原始字节码解码为指令序列 pc 0 length len(self.bytecode) while pc length: op self.bytecode[pc] if 0x60 op 0x7f: # PUSH1 - PUSH32 push_bytes op - 0x5f operand self.bytecode[pc 1 : pc 1 push_bytes] self.instructions.append(Instruction(pc, op, fPUSH{push_bytes}, operand)) pc 1 push_bytes elif 0x80 op 0x8f: # DUP1 - DUP16 self.instructions.append(Instruction(pc, op, fDUP{op - 0x7f})) pc 1 elif 0x90 op 0x9f: # SWAP1 - SWAP16 self.instructions.append(Instruction(pc, op, fSWAP{op - 0x8f})) pc 1 elif op in OPCODES: name OPCODES[op][0] self.instructions.append(Instruction(pc, op, name)) pc 1 else: self.instructions.append(Instruction(pc, op, fUNKNOWN_{hex(op)})) pc 1 return self.instructions def generate_tac(self) - List[str]: 基于符号栈推导三地址码TAC stack: List[str] [] tac_statements: List[str] [] for inst in self.instructions: if inst.mnemonic.startswith(PUSH): val f0x{inst.operand.hex()} stack.append(val) elif inst.mnemonic.startswith(DUP): idx int(inst.mnemonic[3:]) if len(stack) idx: val stack[-idx] stack.append(val) elif inst.mnemonic.startswith(SWAP): idx int(inst.mnemonic[4:]) if len(stack) idx: stack[-1], stack[-idx - 1] stack[-idx - 1], stack[-1] elif inst.opcode in OPCODES: name, pop_cnt, push_cnt OPCODES[inst.opcode] if len(stack) pop_cnt: # 栈下溢注入符号占位符 operands [farg_{i} for i in range(pop_cnt)] else: operands [stack.pop() for _ in range(pop_cnt)] if push_cnt 1: var_name fv{self.var_counter} self.var_counter 1 stmt f{var_name} {name}({, .join(reversed(operands))}) tac_statements.append(stmt) stack.append(var_name) else: if name in (SSTORE, JUMPI, RETURN, REVERT): stmt f{name}({, .join(reversed(operands))}) tac_statements.append(stmt) elif name POP: pass return tac_statements if __name__ __main__: # 示例测试字节码读取 calldata(4) 并将对应 Storage[0] 加上该值 # PUSH1 0x04, CALLDATALOAD, PUSH1 0x00, SLOAD, ADD, PUSH1 0x00, SSTORE, STOP sample_bytecode 6004356000540160005500 decompiler EVMDecompiler(sample_bytecode) instructions decompiler.disassemble() print( 反汇编指令流 ) for inst in instructions: print(inst) tac decompiler.generate_tac() print(\n 生成的三地址码 (TAC) ) for line in tac: print(line)运行上述代码控制台将输出清晰的三地址码序列 反汇编指令流 0x0: PUSH1 0x04 0x2: CALLDATALOAD 0x3: PUSH1 0x00 0x5: SLOAD 0x6: ADD 0x7: PUSH1 0x00 0x9: SSTORE 0xa: STOP 生成的三地址码 (TAC) v0 CALLDATALOAD(0x04) v1 SLOAD(0x00) v2 ADD(v1, v0) SSTORE(0x00, v2)终局重构从 TAC 走向高阶 Solidity 源码当三地址码被完整提取后反编译器的最终阶段是类型推断Type Inference与语法树匹配存储布局映射Storage Layout Mapping三地址码中的SLOAD(0x00)和SSTORE(0x00, ...)表明存储槽slot 0正在被更新。若槽位是通过SHA3(key, slot)计算而来反编译器可立即推断出该状态变量是一个mapping(key value)映射。函数选择器识别Dispatcher Inversion在字节码前部通常会出现形如SHR(0xe0, CALLDATALOAD(0x00))与EQ(0xa9059cbb)的结构。反编译器通过匹配这一结构能够精准重构出函数入口及其 4 字节函数选择器如transfer(address,uint256)。高阶控制流折叠Control Flow Structuring针对JUMPI(dest, condition)若dest分支在汇编执行后重新汇合于后续基本块反编译器即可将其直接折叠为结构化的if (condition) { // 分支块指令折叠 }EVM 字节码反编译不是玄学而是一套严谨的编译器前端与中间表示逆向工程。掌握了从 Opcodes 到 TAC 再到 AST 的推导闭环任何未经开源的暗池合约、黑客攻击合约都将在安全研究员的屏幕上显露其原始的全貌。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询