EVM 不是一台物理计算机,而是为去中心化共识设计的一套确定性指令集。它没有随机数生成器,没有文件系统,没有网络调用,也没有系统时钟——它的状态转换输出对同一输入(交易+上下文+状态)是 100% 确定性的。本节走进 EVM 的五个核心组件:程序计数器、内存、堆栈、存储和调用环境。
7.4.1 EVM 的五大运行时组件
每个 EVM 实例在执行交易时维护五个独立区域:
| 组件 | 类型 | 大小/行为 | 作用 |
|---|---|---|---|
| PC | uint256 | 逐指令递增 | 当前执行位置 |
| 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, MOD | 3-5 | 溢出回绕(uint256 无符号) |
| 比较/位 | LT, GT, EQ, AND, OR, NOT | 3 | 布尔/按位运算 |
| 栈操作 | PUSH1-PUSH32, DUP1-DUP16, SWAP1-SWAP16, POP | 3 | 栈操作 |
| 内存 | MLOAD, MSTORE, MSTORE8 | 3+ | 按字扩展计费 |
| 存储 | SLOAD, SSTORE | 100 / 20000 | 最昂贵操作之一 |
| 环境 | ADDRESS, BALANCE, CALLER, TIMESTAMP | 2-100 | 读取环境变量 |
| 控制流 | JUMP, JUMPI, PC, JUMPDEST | 8 | 跳转目标必须先标记 |
| 调用 | CALL, STATICCALL, DELEGATECALL | 100+ | 跨合约调用 |
| 系统 | CREATE, CREATE2, SELFDESTRUCT | 32000+ | 合约创建与销毁 |
| 日志 | LOG0-LOG4 | 375+ | 事件输出(不可合约内读取) |
关键约束:
- 溢出回绕:
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.sender | msg.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/PREVRANDAO | PoW 难度 / PoS 随机信标 | 不可操纵 |
GASLIMIT | 当前区块的 gas 上限 | 协议参数 |
CALLER/ORIGIN | 直接调用者 / 交易发起者 | 否 |
CALLVALUE | 本次调用发送的 Wei | 否(由交易定) |
CODESIZE | 当前执行代码的大小 | 否 |
RETURNDATASIZE | 上一个调用的返回数据长度 | 否 |
关键安全提示:TIMESTAMP 和 BLOCKHASH 可被矿工轻微操纵,因此不应用于要求真随机性的场景。合约开发者通常使用 Chainlink VRF 或 commit-reveal 协议获取可信随机数。
关键认知三:EVM 的图灵完备不是赠与,而是约束。1024 栈深度、Gas 上限、不可访问外部世界——这些约束将停机问题转化为"资源耗尽即停"。理解 EVM 的确定性本质(同一输入必出同一输出),才能理解为何以太坊的状态转换可以在全网数万节点上并行执行后得到完全一致的结果——这是共识的前提。
← 7.3 MPT 状态树 | 前往 → 7.5 Gas 计费机制与 EIP-1559
评论
0评论加载中…