EVM学习笔记(二):理解EVM执行
深入解析 EVM 执行机制:以最小 Bank 合约为例
本文将通过一个精简的 Bank 合约实现,系统性地剖析 EVM(以太坊虚拟机)的执行流程。不同于常规的代码编写教学,本文聚焦于 Solidity 代码与 EVM 底层操作的映射关系,建立完整的虚拟机执行认知框架。
一、核心学习目标
通过本案例分析,读者将彻底掌握以下关键问题:
deposit()函数执行时:- EVM 修改了哪些存储状态
- Gas 消耗的具体分布
withdraw()函数执行时:- 外部调用(CALL)的底层机制
- 重入攻击的防范原理
- 高级语言特性:
- mapping 存储结构的成本构成
- require 语句的回滚实现机制
二、最小 Bank 合约实现
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract SimpleBank {
// 存储用户余额的mapping结构
mapping(address => uint256) public balances;
// 事件日志(不占用存储空间)
event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);
// 存款函数
function deposit() external payable {
require(msg.value > 0, "zero value");
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
// 取款函数
function withdraw(uint256 amount) external {
require(balances[msg.sender] >= amount, "not enough");
balances[msg.sender] -= amount;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
emit Withdraw(msg.sender, amount);
}
}
三、存款操作(deposit)的 EVM 执行解析
1. 交易初始化阶段
当用户发起存款交易时,交易数据包含:
to:合约地址value:转账金额(如1 ETH)data:函数选择器(deposit()的4字节标识)gasLimit:最大Gas消耗限制
EVM首先创建新的执行上下文,初始化栈(Stack)、内存(Memory)和存储(Storage)环境。
2. 条件检查执行
require(msg.value > 0);
EVM操作序列:
- 从交易上下文读取
msg.value - 执行比较操作(栈操作)
- 条件不满足时触发
REVERT操作码- 回滚所有状态修改
- 保留已消耗Gas(与
STOP不同)
3. 余额更新操作
balances[msg.sender] += msg.value;
底层执行流程:
- 计算存储槽位置:
slot = keccak256(abi.encodePacked(msg.sender, mapping初始槽))
- 执行
SLOAD读取当前值 - 在栈上执行加法运算
- 执行
STORE写入新值
成本分析:
STORE操作成本:- 首次设置非零值:20,000 Gas
- 修改非零值:5,000 Gas
- 清零存储槽:2,900 Gas(含2,000 Gas退款)
4. 事件触发机制
emit Deposit(...);
日志记录过程:
- 创建LOG条目(不修改存储)
- 消耗Gas(约375 Gas + 每个topic 375 Gas)
- 数据存储在区块链收据中(可通过事件监听获取)
四、取款操作(withdraw)的 EVM 执行解析
1. 余额验证阶段
require(balances[msg.sender] >= amount);
安全设计要点:
- 先验证后修改的原子性
- 使用
SLOAD读取存储值 - 验证失败触发
REVERT
2. 状态更新顺序
balances[msg.sender] -= amount;
重入防护机制:
- 采用"Checks-Effects-Interactions"模式
- 先更新本地状态
- 再执行外部调用
- 防止恶意合约通过回调重复取款
3. 外部调用执行
msg.sender.call{value: amount}("");
CALL操作细节:
- 创建新的调用帧(Call Frame)
- 传递控制权至目标地址
- 提供2300 Gas基础值(可调整)
- 返回执行结果(bool值)
安全风险:
- 目标合约可能执行恶意回调
- 低Gas传递可能导致转账失败
- 建议添加Gas限制或使用
transfer(但需注意其2300 Gas限制)
五、关键技术概念映射表
| 高级概念 | EVM实现机制 | 性能特征 |
|---|---|---|
| 状态存储 | Storage(持久化键值存储) | 高成本(SSTORE) |
| 临时计算 | Stack(256位LIFO栈) | 超高速(3 Gas/操作) |
| 动态数据 | Memory(线性可扩展内存) | 中等成本(扩展成本递增) |
| 外部交互 | CALL操作码 | 创建新执行上下文 |
| 异常处理 | REVERT/STOP操作码 | REVERT保留Gas消耗 |
六、Q&A
Q1:为什么mapping无法直接遍历?
A:EVM的Storage设计为哈希映射结构,仅支持通过keccak256(key, slot)计算访问。没有内置方法获取所有键列表,遍历需要额外维护索引结构。
Q2:为什么存款操作比取款操作Gas消耗低?
A:存款主要消耗在STORE写入,而取款包含:
- 两次
SLOAD读取 - 一次
STORE写入 - 外部CALL操作(约700 Gas)
- 额外的条件检查逻辑
Q3:如何优化这个合约的Gas使用?
A:优化建议包括:
- 使用
unchecked块减少算术检查Gas - 对频繁访问的存储变量使用缓存模式
- 考虑使用
assembly优化关键路径 - 批量处理存储更新减少
STORE次数
通过这个最小Bank合约的深度剖析,可以建立从高级语言到虚拟机指令的完整认知链条。这种底层理解对于编写安全高效的智能合约、进行Gas优化以及通过安全审计都至关重要。建议读者结合实际代码调试,加深对EVM执行模型的理解。