教程区块链区块链技术ch077.4 EVM 执行模型与指令集

本页目录

EVM 不是一台物理计算机,而是为去中心化共识设计的一套确定性指令集。它没有随机数生成器,没有文件系统,没有网络调用,也没有系统时钟——它的状态转换输出对同一输入(交易+上下文+状态)是 100% 确定性的。本节走进 EVM 的五个核心组件:程序计数器、内存、堆栈、存储和调用环境。


7.4.1 EVM 的五大运行时组件

每个 EVM 实例在执行交易时维护五个独立区域:

组件类型大小/行为作用
PCuint256逐指令递增当前执行位置
Stack数组 (LIFO)最多 1024 个 256 位字操作数与中间结果
Memory字节数组线性扩展,字节寻址临时数据(函数调用、数组拷贝)
Storage键值映射 (uint256→uint256)永久持久化,跨交易保留合约永久状态
Environment上下文结构每交易固定msg.sender, msg.value, block.number
graph TD
    subgraph EVM 运行时
        A[PC<br>程序计数器] --> B[执行引擎]
        C[Stack<br>LIFO 1024槽] --> D[PUSH/POP/SWAP/DUP]
        E[Memory<br>线性字节数组] --> F[MLOAD/MSTORE]
        G[Storage<br>键值映射
        永久持久化] --> H[SLOAD/ SSTORE]
        I[环境上下文] --> J[区块/交易/ 调用元数据]
    end
    style C fill:#ffcccc
    style G fill:#ccffcc
    style E fill:#ffffcc

7.4.2 指令集:256 位栈式 RISC

EVM 定义了约 140+ 个操作码(opcode),每个都是单字节(0x00-0xFF)。核心分类:

类别典型操作Gas 范围说明
算术ADD, MUL, SUB, DIV, MOD3-5溢出回绕(uint256 无符号)
比较/位LT, GT, EQ, AND, OR, NOT3布尔/按位运算
栈操作PUSH1-PUSH32, DUP1-DUP16, SWAP1-SWAP16, POP3栈操作
内存MLOAD, MSTORE, MSTORE83+按字扩展计费
存储SLOAD, SSTORE100 / 20000最昂贵操作之一
环境ADDRESS, BALANCE, CALLER, TIMESTAMP2-100读取环境变量
控制流JUMP, JUMPI, PC, JUMPDEST8跳转目标必须先标记
调用CALL, STATICCALL, DELEGATECALL100+跨合约调用
系统CREATE, CREATE2, SELFDESTRUCT32000+合约创建与销毁
日志LOG0-LOG4375+事件输出(不可合约内读取)

关键约束

  • 溢出回绕ADD/MUL 在超过 2²⁵⁶ 时自动截断(不像 CPU 抛异常),开发者需手动用防溢出库(如 Solidity 0.8 后自动溢出检查);
  • 栈深度限制:1024,超深则 CALL 回滚;
  • 无跳转表JUMP 的目标必须是字面量或栈顶值,且目标地址必须是 JUMPDEST 指令——防止跳转到数据段。
ts
// evm-simple-vm.ts
// 纯内置:模拟极简 EVM 栈执行(ADD, PUSH, MUL)

class SimpleEVM {
  private stack: bigint[] = [];
  private pc = 0;
  private halted = false;
  private gas: bigint;

  constructor(gasLimit: bigint) { this.gas = gasLimit; }

  push(v: bigint) {
    if (this.stack.length >= 1024) throw new Error('Stack overflow');
    this.gas -= 3n;
    this.stack.push(v & ((1n << 256n) - 1n));
  }

  pop(): bigint {
    if (this.stack.length === 0) throw new Error('Stack underflow');
    this.gas -= 3n;
    return this.stack.pop()!;
  }

  add() {
    const a = this.pop();
    const b = this.pop();
    this.gas -= 3n;
    this.push((a + b) % (1n << 256n)); // 溢出回绕
  }

  mul() {
    const a = this.pop();
    const b = this.pop();
    this.gas -= 5n;
    this.push((a * b) % (1n << 256n));
  }

  run(bytecode: { op: 'PUSH' | 'ADD' | 'MUL'; val?: bigint }[]) {
    while (this.pc < bytecode.length && !this.halted && this.gas > 0n) {
      const inst = bytecode[this.pc++];
      if (inst.op === 'PUSH') this.push(inst.val!);
      else if (inst.op === 'ADD') this.add();
      else if (inst.op === 'MUL') this.mul();
    }
    return { stack: [...this.stack], gas: this.gas, pc: this.pc };
  }
}

// 执行: (3 + 4) * 5 = 35
const bytecode = [
  { op: 'PUSH' as const, val: 3n },
  { op: 'PUSH' as const, val: 4n },
  { op: 'ADD' as const },
  { op: 'PUSH' as const, val: 5n },
  { op: 'MUL' as const },
];
const evm = new SimpleEVM(100000n);
const result = evm.run(bytecode);
console.log(`栈顶: ${result.stack[0]} (期望 35)`);
console.log(`剩余 gas: ${result.gas}`);

7.4.3 跨合约调用:CALL, DELEGATECALL, STATICCALL

操作码上下文继承msg.sendermsg.value可修改存储?
CALL新上下文调用者(原调用者或当前合约)可发送
DELEGATECALL继承当前地址原始调用者继承当前 msg.value是(改的是当前合约的存储)
STATICCALL只读上下文调用者不可发送否(回滚)
sequenceDiagram
    participant U as User
    participant A as 合约 A
    participant B as 合约 B
    
    U->>A: 调用 f()
    A->>B: CALL(发送 ETH, 调用 g())
    Note over A,B: B 的 msg.sender = A, msg.value = 发送量
    A->>B: STATICCALL(调用 h())
    Note over A,B: B 无法修改任何存储,否则回滚
    A->>B: DELEGATECALL(调用 g())
    Note over A,B: B 看到的是 U 作为 msg.sender<br>且修改的是 A 的存储!

DELEGATECALL代理合约模式(如 OpenZeppelin 的 TransparentProxy、ERC-1967)的基础:一个轻量代理合约持有所有 state,通过 DELEGATECALL 将执行转发到可升级的实现合约,但态始终保存在代理上。


7.4.4 执行环境:区块上下文与交易上下文

EVM 运行时可访问的环境变量:

操作码含义是否可操控
BLOCKHASH(n)最近 256 个区块的哈希否(历史固定)
TIMESTAMP当前区块时间戳(秒)矿工可在小范围内操纵
NUMBER当前区块号
COINBASE矿工/验证者地址矿工决定
DIFFICULTY/PREVRANDAOPoW 难度 / PoS 随机信标不可操纵
GASLIMIT当前区块的 gas 上限协议参数
CALLER/ORIGIN直接调用者 / 交易发起者
CALLVALUE本次调用发送的 Wei否(由交易定)
CODESIZE当前执行代码的大小
RETURNDATASIZE上一个调用的返回数据长度

关键安全提示TIMESTAMPBLOCKHASH 可被矿工轻微操纵,因此不应用于要求真随机性的场景。合约开发者通常使用 Chainlink VRF 或 commit-reveal 协议获取可信随机数。


关键认知三:EVM 的图灵完备不是赠与,而是约束。1024 栈深度、Gas 上限、不可访问外部世界——这些约束将停机问题转化为"资源耗尽即停"。理解 EVM 的确定性本质(同一输入必出同一输出),才能理解为何以太坊的状态转换可以在全网数万节点上并行执行后得到完全一致的结果——这是共识的前提。


← 7.3 MPT 状态树 | 前往 → 7.5 Gas 计费机制与 EIP-1559

评论

0

评论加载中…

发表评论

0/2000