跳转到内容

理解以太坊虚拟机(EVM)

理解以太坊虚拟机(EVM)

以太坊虚拟机(Ethereum Virtual Machine,EVM)是以太坊的核心组件,是一个图灵完备的状态机,负责执行所有智能合约代码。

什么是 EVM

EVM 是一个基于栈的虚拟机,运行在所有以太坊节点上。它提供了一个完全隔离的沙盒环境,智能合约在其中执行,无法访问网络、文件系统或其他进程。

EVM 的关键特性

  • 确定性:相同的输入在任何节点上都产生相同的输出
  • 沙盒隔离:合约代码无法访问外部资源
  • Gas 计量:每个操作都有对应的 Gas 消耗,防止无限循环
  • 图灵完备:理论上可以执行任意计算(受 Gas 限制)

EVM 架构

内存模型

EVM 有三种数据存储方式:

1. 栈(Stack)

  • 最大深度 1024
  • 每个元素 32 字节(256 位)
  • 后进先出(LIFO)
  • 最便宜的存储方式

2. 内存(Memory)

  • 字节数组,可动态扩展
  • 每次调用时重置
  • 按字(32 字节)访问
  • 扩展内存消耗 Gas

3. 存储(Storage)

  • 持久化键值存储
  • 每个合约有独立的 256 位到 256 位的映射
  • 最昂贵的存储方式
  • 状态改变写入区块链
+------------------+
| Stack (1024) | ← 操作数,32字节/槽位
+------------------+
| Memory | ← 临时数据,按字节寻址
+------------------+
| Storage | ← 持久化数据,键值对
+------------------+

程序计数器(PC)

程序计数器跟踪当前执行的字节码位置。EVM 按顺序执行指令,除非遇到跳转指令(JUMP/JUMPI)。

EVM 字节码与操作码

Solidity 代码编译后生成 EVM 字节码,由一系列操作码(Opcode)组成。

常见操作码分类

算术操作

操作码Gas说明
ADD3加法
MUL5乘法
SUB3减法
DIV5整数除法
MOD5取模
EXP10+指数运算

比较操作

操作码Gas说明
LT3小于
GT3大于
EQ3等于
ISZERO3是否为零

存储操作

操作码Gas说明
SLOAD2100从 storage 读取
SSTORE20000写入 storage(新值)
MLOAD3从 memory 读取
MSTORE3写入 memory

环境信息

操作码Gas说明
ADDRESS2当前合约地址
CALLER2调用者地址
CALLVALUE2发送的 ETH 数量
TIMESTAMP2当前区块时间戳
NUMBER2当前区块号

字节码示例

下面是一个简单 Solidity 函数对应的字节码:

// Solidity 源码
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}

对应的 EVM 操作码序列(简化):

PUSH1 0x04 // 推入参数偏移
CALLDATALOAD // 读取第一个参数
PUSH1 0x24 // 推入第二个参数偏移
CALLDATALOAD // 读取第二个参数
ADD // 相加
PUSH1 0x00 // 内存偏移
MSTORE // 存入内存
PUSH1 0x20 // 返回数据长度
PUSH1 0x00 // 内存偏移
RETURN // 返回

Gas 机制

Gas 是 EVM 中衡量计算工作量的单位,防止恶意代码通过无限循环耗尽节点资源。

Gas 计算

交易费用 = Gas Used × Gas Price
  • Gas Limit:用户愿意为交易支付的最大 Gas 量
  • Gas Price:每单位 Gas 的价格(Gwei)
  • Base Fee:EIP-1559 后引入,按网络拥堵自动调整
  • Priority Fee(Tip):给矿工/验证者的小费

影响 Gas 消耗的因素

  1. 操作码类型:不同操作码 Gas 成本不同(见上表)
  2. 存储操作:SSTORE 最昂贵(首次写入 20000 Gas)
  3. 内存扩展:内存使用量增加时 Gas 成本非线性增长
  4. 调用深度:每次 CALL 都消耗额外 Gas
  5. 数据大小:calldata 每字节消耗 Gas(零字节 4 Gas,非零字节 16 Gas)

Gas 优化技巧

// 不好的写法 - 每次循环都读写 storage
contract BadExample {
uint256 public count;
function incrementMany(uint256 n) external {
for (uint256 i = 0; i < n; i++) {
count++; // 每次循环 SLOAD + SSTORE
}
}
}
// 好的写法 - 使用内存变量
contract GoodExample {
uint256 public count;
function incrementMany(uint256 n) external {
uint256 _count = count; // 一次 SLOAD
for (uint256 i = 0; i < n; i++) {
_count++; // 仅栈操作
}
count = _count; // 一次 SSTORE
}
}

合约调用机制

CALL vs DELEGATECALL vs STATICCALL

类型执行上下文msg.sendermsg.value存储修改
CALL被调用合约调用者可传递 ETH被调用合约
DELEGATECALL当前合约原始 msg.sender保持不变当前合约
STATICCALL被调用合约调用者不可传递禁止修改状态
// CALL - 在目标合约的上下文中执行
(bool success, bytes memory data) = target.call{value: amount}(
abi.encodeWithSignature("someFunction(uint256)", param)
);
// DELEGATECALL - 在当前合约的上下文中执行目标代码
(bool success, bytes memory data) = target.delegatecall(
abi.encodeWithSignature("someFunction(uint256)", param)
);

调用深度限制

EVM 最大调用深度为 1024。超过此限制会导致调用失败。攻击者可以利用此限制实施”调用深度攻击”。

合约创建

当部署新合约时,EVM 执行以下步骤:

  1. 创建新账户,分配地址
  2. 执行构造函数字节码(init code
  3. 返回的字节码作为合约的运行时代码(runtime code)存储
交易(包含 init code)→ EVM 执行 init code → 返回 runtime code → 存储到账户

事件(Events)与日志

合约可以发出事件,这些事件以日志的形式存储在交易收据中,不存储在合约 storage 中(更便宜)。

// 定义事件
event Transfer(address indexed from, address indexed to, uint256 value);
// 发出事件
emit Transfer(msg.sender, recipient, amount);

日志由以下部分组成:

  • topics:最多 4 个(第一个是事件签名哈希,其余是 indexed 参数)
  • data:非 indexed 的参数,可以是任意长度

ABI(应用二进制接口)

ABI 定义了如何与合约交互的接口规范。

函数选择器

函数选择器是函数签名的 Keccak-256 哈希的前 4 字节:

// 函数签名:transfer(address,uint256)
const selector = ethers.id("transfer(address,uint256)").slice(0, 10);
// 结果:0xa9059cbb

ABI 编码

const abiCoder = new ethers.AbiCoder();
// 编码函数参数
const encoded = abiCoder.encode(
['address', 'uint256'],
['0xRecipient', ethers.parseEther('1.0')]
);
// 解码返回值
const decoded = abiCoder.decode(
['uint256'],
returnData
);

EVM 兼容链

由于 EVM 的开源性,许多其他区块链实现了 EVM 兼容性:

说明
Polygon以太坊侧链,低手续费
BNB Chain币安开发的 EVM 兼容链
Avalanche C-Chain高性能 EVM 链
Arbitrum以太坊 Layer 2(Optimistic Rollup)
Optimism以太坊 Layer 2(Optimistic Rollup)
BaseCoinbase 开发的 Layer 2

调试 EVM 执行

使用 Remix 调试器

Remix IDE 内置 EVM 调试器,可以逐步查看操作码执行过程。

使用 Tenderly

Tenderly 提供在线交易调试和监控工具,可以模拟交易执行并查看详细的 Gas 消耗。

Hardhat Console

hardhat.config.js
require("hardhat-gas-reporter");
// 测试时自动报告 Gas 消耗

总结

EVM 是以太坊生态的核心引擎:

  • 基于栈的虚拟机,具有确定性和沙盒隔离特性
  • 通过 Gas 机制防止资源滥用
  • 支持复杂的合约调用模式(CALL、DELEGATECALL、STATICCALL)
  • ABI 定义了合约交互的标准接口
  • 已成为整个行业的标准,众多链实现了 EVM 兼容