EVM 工作原理完全指南

Devin
Web3

EVM 工作原理完全指南

EVM 工作原理完全指南

目录


EVM 基础概念

1. 什么是 EVM?

Preparing diagram...

定义:

EVM = Ethereum Virtual Machine(以太坊虚拟机)
    = 去中心化的计算引擎
    = 智能合约的执行环境
    = 全球同步的状态机

2. EVM 类比理解

Preparing diagram...
概念传统计算机以太坊
硬件物理 CPUEVM(虚拟)
操作系统Windows/Linux以太坊协议
应用程序.exe 文件智能合约
编程语言C/Java/PythonSolidity/Vyper
运行位置本地机器全球数千节点
执行结果可能不同完全相同

3. EVM 的核心特点

Preparing diagram...

特点详解

✅ 图灵完备(Turing Complete)

可以执行:
- 条件判断(if/else)
- 循环(while/for)
- 递归调用
- 任意复杂的逻辑

限制:
- Gas限制防止无限循环
- 区块Gas限制防止DOS

✅ 确定性(Deterministic)

相同的输入 → 必定产生相同的输出

例子:
函数 add(a, b) 在任何节点执行
输入 (3, 5) → 输出永远是 8

这保证了:
- 所有节点达成共识
- 可以验证计算结果
- 网络状态一致

✅ 隔离性(Sandboxed)

EVM运行在隔离环境中:

不能:
❌ 访问文件系统
❌ 发起网络请求
❌ 获取真随机数
❌ 访问其他进程

只能:
✅ 读取区块链数据
✅ 调用其他合约
✅ 修改自己的存储
✅ 触发事件(Event)

✅ 全球同步(Globally Synchronized)

当你部署合约:
    ↓
全球数千个节点同时执行
    ↓
每个节点独立计算
    ↓
所有节点得到相同结果
    ↓
达成共识

4. EVM 的限制

Preparing diagram...

EVM 架构详解

1. EVM 组件总览

Preparing diagram...

2. Stack(堆栈)

堆栈特性

Preparing diagram...

堆栈规格:

类型:LIFO(Last In First Out,后进先出)
最大深度:1024个元素
元素大小:256位(32字节)
访问方式:只能操作栈顶部分
溢出:超过1024会失败
下溢:空栈弹出会失败

堆栈操作演示

示例 1:简单计算(3 + 5)

Preparing diagram...

对应字节码:

60 03        PUSH1 3     (压入3)
60 05        PUSH1 5     (压入5)
01           ADD         (相加)

结果:Stack = [8]

示例 2:复杂表达式((a + b) * c)

假设:a=2, b=3, c=4

步骤1: PUSH1 0x02    Stack: [2]
步骤2: PUSH1 0x03    Stack: [2, 3]
步骤3: ADD           Stack: [5]        // 2+3
步骤4: PUSH1 0x04    Stack: [5, 4]
步骤5: MUL           Stack: [20]       // 5*4

最终结果:20

堆栈操作指令

Preparing diagram...

指令说明:

PUSH1-PUSH32: 压入1到32字节的数据
  例: PUSH1 0x05  → Stack: [5]

POP: 弹出并丢弃栈顶元素
  例: POP  → 移除栈顶

DUP1-DUP16: 复制栈顶第N个元素到栈顶
  例: Stack: [A, B, C]
      DUP1 → Stack: [A, A, B, C]  (复制栈顶)
      DUP2 → Stack: [B, A, B, C]  (复制第2个)

SWAP1-SWAP16: 交换栈顶和第N+1个元素
  例: Stack: [A, B, C]
      SWAP1 → Stack: [B, A, C]  (交换栈顶两个)
      SWAP2 → Stack: [C, B, A]  (交换栈顶和第3个)

3. Memory(内存)

内存特性

Preparing diagram...

内存规格:

类型:线性字节数组
访问:按32字节(256位)字对齐
生命周期:仅在交易执行期间
清除:交易结束后释放
初始大小:0字节
扩展:按需扩展(付费)

内存操作

Preparing diagram...

内存操作指令:

MSTORE(offset, value):
  将32字节值写入内存
  例: MSTORE(0x80, 0x42)
      → Memory[0x80-0x9F] = 0x42...

MLOAD(offset):
  从内存读取32字节
  例: MLOAD(0x80)
      → Stack: [Memory[0x80-0x9F]]

MSTORE8(offset, value):
  将1字节值写入内存
  例: MSTORE8(0x80, 0x42)
      → Memory[0x80] = 0x42

内存 Gas 成本

Preparing diagram...

Gas 计算公式:

memory_cost = (memory_size_word² / 512) + (3 × memory_size_word)

例子:
使用64字节(2 words):
  cost = (2² / 512) + (3 × 2) = 0.0078 + 6 ≈ 6 gas

使用1024字节(32 words):
  cost = (32² / 512) + (3 × 32) = 2 + 96 = 98 gas

使用10KB:
  cost = 1000+ gas(快速增长)

4. Storage(存储)

存储特性

Preparing diagram...

存储规格:

类型:键值对映射(Key-Value)
键大小:32字节(256位)
值大小:32字节(256位)
持久化:永久保存在区块链上
每个合约:独立的存储空间
初始值:所有槽位默认为0

Storage vs Memory vs Stack

Preparing diagram...

Storage Gas 成本

最昂贵的操作!

Preparing diagram...

Gas 成本详解:

SLOAD(读取Storage):
  - 冷访问(首次):2100 gas
  - 热访问(同交易内再次):100 gas

SSTORE(写入Storage):
  - 零 → 非零(新建):20000 gas
  - 非零 → 非零(修改):5000 gas
  - 非零 → 零(清除):5000 gas + 退款15000 gas

示例:
  uint256 value;  // 默认为0

  value = 100;    // 零→非零:20000 gas
  value = 200;    // 非零→非零:5000 gas
  value = 0;      // 非零→零:5000 gas - 15000退款 = -10000

Storage 优化技巧

技巧 1:缓存到内存

// ❌ 低效:多次读取Storage
function inefficient() public view returns (uint) {
    return value + value + value;  // 读3次storage
    // 成本:2100 + 100 + 100 = 2300 gas
}

// ✅ 高效:缓存到内存
function efficient() public view returns (uint) {
    uint temp = value;  // 读1次storage
    return temp + temp + temp;  // 内存操作
    // 成本:2100 gas
}

技巧 2:打包变量

// ❌ 低效:占用多个槽
contract Inefficient {
    uint128 a;  // slot 0
    uint256 b;  // slot 1
    uint128 c;  // slot 2
}
// 读取a+c: 2次SLOAD = 2200 gas

// ✅ 高效:打包到同一槽
contract Efficient {
    uint128 a;  // slot 0前半部分
    uint128 c;  // slot 0后半部分
    uint256 b;  // slot 1
}
// 读取a+c: 1次SLOAD = 2100 gas(如果都在同一交易)

技巧 3:使用映射而非数组(某些情况)

// 数组:需要存储长度
uint[] public arr;  // slot 0: length, slot hash(0): elements

// 映射:不存储长度,按需访问
mapping(uint => uint) public map;  // 只在使用时才有gas成本

5. Calldata(调用数据)

Preparing diagram...

Calldata 特性:

类型:只读字节数组
来源:交易的data字段
访问:CALLDATALOAD, CALLDATASIZE, CALLDATACOPY
生命周期:仅当前调用
成本:非零字节16 gas,零字节4 gas

Calldata 示例:

// 调用: transfer(address to, uint256 amount)
// to: 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb
// amount: 1000000000000000000 (1 ETH in Wei)

Calldata:
0xa9059cbb  // transfer(address,uint256)的函数选择器
000000000000000000000000742d35Cc6634C0532925a3b844Bc9e7595f0bEb  // to地址
0000000000000000000000000000000000000000000000000de0b6b3a7640000  // amount

总长度:4 + 32 + 32 = 68字节

6. Program Counter(程序计数器)

Preparing diagram...

PC 工作原理:

字节码:60 03 60 05 01 ...
位置:   0  1  2  3  4  5

执行流程:
1. PC = 0: 读取0x60 (PUSH1)
2. PC = 1: 读取0x03 (数据)
3. PC = 2: 读取0x60 (PUSH1)
4. PC = 3: 读取0x05 (数据)
5. PC = 4: 读取0x01 (ADD)
6. PC = 5: 继续...

跳转:
- JUMP:直接修改PC
- JUMPI:条件修改PC
- JUMPDEST:跳转目标标记

字节码与操作码

1. 编译流程

Preparing diagram...

详细流程:

Preparing diagram...

示例:

// Solidity源代码
contract Simple {
    uint256 public value;

    function setValue(uint256 _value) public {
        value = _value;
    }
}

// 编译后的字节码(部分)
608060405234801561001057600080fd5b50...

// ABI(接口)
[
  {
    "inputs": [{"name": "_value", "type": "uint256"}],
    "name": "setValue",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  }
]

2. 操作码分类

Preparing diagram...

3. 常用操作码详解

算术操作(Gas: 3-10)

Preparing diagram...

操作码表:

操作码十六进制Gas说明栈变化
ADD0x013a + b[a,b] → [a+b]
MUL0x025a × b[a,b] → [a×b]
SUB0x033a - b[a,b] → [a-b]
DIV0x045a ÷ b[a,b] → [a÷b]
MOD0x065a % b[a,b] → [a%b]
ADDMOD0x088(a+b) % N[a,b,N] → [(a+b)%N]
MULMOD0x098(a×b) % N[a,b,N] → [(a×b)%N]
EXP0x0A10+a ^ b[a,b] → [a^b]

比较操作(Gas: 3)

操作码十六进制说明栈变化
LT0x10a < b[a,b] → [a<b?1
]
GT0x11a > b[a,b] → [a>b?1
]
EQ0x14a == b[a,b] → [a==b?1
]
ISZERO0x15a == 0[a] → [a==0?1
]

位运算(Gas: 3)

操作码十六进制说明栈变化
AND0x16按位与[a,b] → [a&b]
OR0x17按位或[a,b] → [a|b]
XOR0x18按位异或[a,b] → [a^b]
NOT0x19按位非[a] → [~a]
SHL0x1B左移[shift,value] → [value<<shift]
SHR0x1C右移[shift,value] → [value>>shift]

环境信息

操作码十六进制Gas说明
ADDRESS0x302当前合约地址
BALANCE0x31100账户余额
ORIGIN0x322交易发起者
CALLER0x332调用者地址
CALLVALUE0x342发送的 ETH
CALLDATALOAD0x353读取 calldata
CALLDATASIZE0x362calldata 大小
GASPRICE0x3A2Gas 价格

区块信息

操作码十六进制Gas说明
BLOCKHASH0x4020区块哈希
COINBASE0x412验证者地址
TIMESTAMP0x422区块时间戳
NUMBER0x432区块号
DIFFICULTY0x442难度(PoS=0)
GASLIMIT0x452Gas 限制
CHAINID0x462链 ID

4. 字节码分析实例

Solidity 函数:

function add(uint a, uint b) public pure returns (uint) {
    return a + b;
}

编译后的操作流程:

Preparing diagram...

对应字节码:

// 函数选择器检查
60 04           PUSH1 4
35              CALLDATALOAD        // 读取函数选择器
63 771602f7     PUSH4 0x771602f7    // add(uint,uint)的选择器
14              EQ
61 001a         PUSH2 0x001A
57              JUMPI               // 如果匹配,跳转到函数体

// 函数体
5b              JUMPDEST            // 跳转目标
60 04           PUSH1 4
35              CALLDATALOAD        // 读取参数a
60 24           PUSH1 36
35              CALLDATALOAD        // 读取参数b
01              ADD                 // a + b
60 00           PUSH1 0
52              MSTORE              // 存到内存
60 20           PUSH1 32
60 00           PUSH1 0
f3              RETURN              // 返回结果

智能合约执行流程

1. 完整执行流程

Preparing diagram...

2. EVM 执行详细步骤

Preparing diagram...

3. 函数调用过程

示例场景:DEX 交易

Preparing diagram...

调用栈变化:

初始状态:
Call Stack: [UserContract]

UserContract调用DEX:
Call Stack: [UserContract, DEXContract]

DEXContract调用Token:
Call Stack: [UserContract, DEXContract, TokenContract]

TokenContract返回:
Call Stack: [UserContract, DEXContract]

DEXContract返回:
Call Stack: [UserContract]

完成
Call Stack: []

4. 异常处理机制

Preparing diagram...

异常示例:

function divide(uint a, uint b) public pure returns (uint) {
    require(b != 0, "Division by zero");  // REVERT if b==0
    return a / b;
}

// 执行流程:
// 1. 检查 b != 0
// 2. 如果b=0,触发REVERT
// 3. 返回错误信息 "Division by zero"
// 4. 状态回滚
// 5. 剩余Gas退还给调用者

Gas 与 EVM 的关系

1. 为什么需要 Gas?

Preparing diagram...

2. Gas 成本设计原则

Preparing diagram...

3. 操作码 Gas 成本对照

Preparing diagram...

详细成本表:

级别Gas 范围操作示例用途
极便宜2-3PUSH, POP, ADD, SUB基础栈操作
便宜3-8MUL, DIV, AND, OR, XOR算术和逻辑
中等10-50SHA3, BYTE, SHL, SHR复杂计算
昂贵100-2100BALANCE, SLOAD, CALL状态访问
极昂贵5000-32000SSTORE, CREATE, CREATE2状态修改

4. Gas 优化策略

策略 1:使用短路评估

// ✅ 便宜的条件在前
if (cheapCheck() && expensiveCheck()) {
    // cheapCheck()为false时不执行expensiveCheck()
}

// ❌ 昂贵的条件在前
if (expensiveCheck() && cheapCheck()) {
    // 总是先执行expensiveCheck()
}

策略 2:批量操作

// ❌ 多次单独调用
for(uint i = 0; i < users.length; i++) {
    token.transfer(users[i], amounts[i]);  // 每次21000+ gas
}

// ✅ 批量转账
function batchTransfer(address[] memory users, uint[] memory amounts) public {
    for(uint i = 0; i < users.length; i++) {
        balances[users[i]] += amounts[i];  // 单次调用处理全部
    }
}

策略 3:使用事件代替存储

// ❌ 存储历史记录
uint[] public history;
function record(uint value) {
    history.push(value);  // ~20000 gas per entry
}

// ✅ 使用事件
event Recorded(uint value, uint timestamp);
function record(uint value) {
    emit Recorded(value, block.timestamp);  // ~375 gas
}

策略 4:变量打包

// ❌ 浪费存储槽
struct User {
    uint256 id;        // slot 0
    bool active;       // slot 1 (浪费31字节)
    uint256 balance;   // slot 2
}

// ✅ 优化打包
struct User {
    uint128 id;        // slot 0 前半部分
    uint128 balance;   // slot 0 后半部分
    bool active;       // slot 1 (仍然浪费,但减少了一个槽)
}

策略 5:使用 immutable 和 constant

// ❌ 普通状态变量
address public owner;  // 每次访问2100 gas

// ✅ immutable(部署时设置)
address public immutable owner;  // 编译时内联,几乎免费

// ✅ constant(编译时常量)
uint public constant FEE = 100;  // 完全免费

5. Gas 优化对比

Preparing diagram...

实践练习

练习 1:反汇编字节码

工具: https://etherscan.io/opcode-tool

步骤:

// 1. 编写简单合约
contract Add {
    function add(uint a, uint b) public pure returns (uint) {
        return a + b;
    }
}

// 2. 使用Remix编译
// 3. 复制字节码
// 4. 在opcode-tool中分析
// 5. 理解每个操作码的作用

练习 2:Gas 消耗分析

// 部署以下三个版本,对比gas消耗

// 版本A:未优化
contract VersionA {
    uint public counter;

    function increment() public {
        counter = counter + 1;
    }
}

// 版本B:使用++
contract VersionB {
    uint public counter;

    function increment() public {
        counter++;
    }
}

// 版本C:使用unchecked
contract VersionC {
    uint public counter;

    function increment() public {
        unchecked {
            ++counter;
        }
    }
}

// 测试:调用increment()10次,记录总gas消耗

练习 3:使用 Remix 调试器

步骤:

  1. 在 Remix 中部署合约
  2. 调用函数
  3. 点击"Debug"按钮
  4. 观察:
    • Stack 的变化
    • Memory 的内容
    • Storage 的更新
    • Gas 的消耗
  5. 单步执行每个操作码

练习 4:分析真实合约

使用 Etherscan:

  1. 访问 Uniswap 合约: 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D

  2. 查看"Contract" → "Code"

  3. 分析字节码:

    • 找到函数选择器
    • 识别主要操作码
    • 理解逻辑流程
  4. 在"Read Contract"中调用函数

    • 观察返回值
    • 理解函数功能

参考资料

官方文档

  1. Ethereum.org - EVM

  2. Ethereum Yellow Paper

  3. EVM Opcodes

  4. Solidity Documentation

深度学习

  1. Mastering Ethereum - EVM Chapter

  2. EVM Deep Dives

  3. Deconstructing a Solidity Contract

  4. Understanding EVM Stack

工具

  1. Remix IDE

  2. Tenderly

  3. Foundry

  4. Hardhat

Gas 优化

  1. Gas Optimization Guide

  2. Solidity Gas Optimization Tips

  3. Yul Documentation

进阶主题

  1. EVM Puzzles

  2. Huff Language

  3. EVM Bytecode Analyzer

视频教程

  1. Smart Contract Programmer - EVM

  2. Patrick Collins - Assembly

研究论文

  1. A Survey of Tools for Analyzing Ethereum

  2. Ethereum Smart Contract Security

社区资源

  1. Ethereum StackExchange

  2. EVM Security

  3. Week in Ethereum - Development


总结

核心要点

EVM 的本质:

✅ 基于栈的虚拟机
   - 256位字长
   - LIFO堆栈

✅ 三种存储
   - Stack: 临时计算(便宜)
   - Memory: 交易期间(中等)
   - Storage: 永久保存(昂贵)

✅ 字节码驱动
   - Solidity → 字节码
   - 操作码执行
   - Gas计量

✅ 确定性执行
   - 相同输入→相同输出
   - 全球节点一致
   - 可验证计算

EVM vs 传统 VM

特性EVMJVM/其他 VM
架构基于栈基于寄存器/栈
字长256 位32/64 位
确定性强制非强制
资源计量Gas
执行环境全球同步本地
持久化区块链数据库/文件

学习路径

初学者:

  1. 理解 Stack/Memory/Storage
  2. 学习基本操作码
  3. 使用 Remix 调试
  4. 分析简单合约

进阶:

  1. 深入字节码分析
  2. 学习 Gas 优化
  3. 理解调用机制
  4. 研究异常处理

高级:

  1. 学习 Yul/Assembly
  2. 研究 EVM 内部
  3. 贡献 EVM 实现
  4. 安全审计

关键技能

Preparing diagram...

最佳实践

开发时:

✅ 使用最新Solidity版本
✅ 启用编译器优化
✅ 测试gas消耗
✅ 审查字节码
✅ 使用静态分析工具

优化时:

✅ 缓存storage到memory
✅ 使用事件代替storage
✅ 打包变量节省槽位
✅ 使用immutable/constant
✅ 批量操作减少调用

安全时:

✅ 检查外部调用
✅ 使用checks-effects-interactions
✅ 防止重入攻击
✅ 限制gas使用
✅ 处理异常情况

恭喜完成学习! 🎉

你现在对 EVM 的工作原理有了深入的理解。结合之前的《以太坊架构》教程,你已经掌握了以太坊的完整技术栈!

下一步建议:

  • 实践编写和优化智能合约
  • 学习高级安全技术
  • 探索 Layer 2 和扩容方案
  • 参与开源项目贡献

最后更新:2025 年 11 月 祝你成为区块链技术专家!🚀