EVM学习笔记(二):理解EVM执行

深入解析 EVM 执行机制:以最小 Bank 合约为例

本文将通过一个精简的 Bank 合约实现,系统性地剖析 EVM(以太坊虚拟机)的执行流程。不同于常规的代码编写教学,本文聚焦于 Solidity 代码与 EVM 底层操作的映射关系,建立完整的虚拟机执行认知框架。

一、核心学习目标

通过本案例分析,读者将彻底掌握以下关键问题:

二、最小 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. 交易初始化阶段

当用户发起存款交易时,交易数据包含:

EVM首先创建新的执行上下文,初始化栈(Stack)、内存(Memory)和存储(Storage)环境。

2. 条件检查执行

require(msg.value > 0);

EVM操作序列

  1. 从交易上下文读取msg.value
  2. 执行比较操作(栈操作)
  3. 条件不满足时触发REVERT操作码
    • 回滚所有状态修改
    • 保留已消耗Gas(与STOP不同)

3. 余额更新操作

balances[msg.sender] += msg.value;

底层执行流程

  1. 计算存储槽位置:
    • slot = keccak256(abi.encodePacked(msg.sender, mapping初始槽))
  2. 执行SLOAD读取当前值
  3. 在栈上执行加法运算
  4. 执行STORE写入新值

成本分析

4. 事件触发机制

emit Deposit(...);

日志记录过程

  1. 创建LOG条目(不修改存储)
  2. 消耗Gas(约375 Gas + 每个topic 375 Gas)
  3. 数据存储在区块链收据中(可通过事件监听获取)

四、取款操作(withdraw)的 EVM 执行解析

1. 余额验证阶段

require(balances[msg.sender] >= amount);

安全设计要点

2. 状态更新顺序

balances[msg.sender] -= amount;

重入防护机制

3. 外部调用执行

msg.sender.call{value: amount}("");

CALL操作细节

  1. 创建新的调用帧(Call Frame)
  2. 传递控制权至目标地址
  3. 提供2300 Gas基础值(可调整)
  4. 返回执行结果(bool值)

安全风险

五、关键技术概念映射表

高级概念 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写入,而取款包含:

  1. 两次SLOAD读取
  2. 一次STORE写入
  3. 外部CALL操作(约700 Gas)
  4. 额外的条件检查逻辑

Q3:如何优化这个合约的Gas使用?
A:优化建议包括:

  1. 使用unchecked块减少算术检查Gas
  2. 对频繁访问的存储变量使用缓存模式
  3. 考虑使用assembly优化关键路径
  4. 批量处理存储更新减少STORE次数

通过这个最小Bank合约的深度剖析,可以建立从高级语言到虚拟机指令的完整认知链条。这种底层理解对于编写安全高效的智能合约、进行Gas优化以及通过安全审计都至关重要。建议读者结合实际代码调试,加深对EVM执行模型的理解。