深入浅出:以太坊虚拟机(EVM)存储模型设计与权衡

核心思想:分层存储

EVM采用分层存储架构,基本原则是:数据的持久性与访问成本成正比。即越持久的数据,操作它所需的Gas(费用)越高。这种设计是为了让开发者谨慎使用区块链的全局状态。

在以太坊虚拟机(EVM)中,数据存储根据可变性生命周期 分为几个不同的区域,上图将其分为了 "易变数据区""不易变数据区"

一、易变数据区

这部分数据在交易执行期间存在,交易结束后被清除,不永久保存到区块链状态。

  1. stack(栈)
    • 大小为 32 字节 × 256 位(即 256 个 32 字节的槽位)。
    • 用于 EVM 指令执行时的临时数据存储,比如局部变量、计算中间值。
    • 访问速度极快,但大小有限,只能通过 push/pop 方式操作。
    • 功能 :EVM是一个栈式虚拟机 ,它的指令(如ADD, MUL, SWAP)主要从栈顶获取操作数并压入结果。它用于存储局部变量、函数参数和计算中间值
    • 特点
      • 速度极快,Gas成本几乎为零
      • 大小严格限制为1024个元素(256位深 x 32字节宽)。这是为了防止无限递归调用等攻击,也是许多"栈溢出"漏洞的根源。
      • 只能通过PUSHPOPDUPSWAP等特定指令访问,无法随机访问。
    • 类比:CPU的L1缓存。容量最小,但速度最快。
  2. calldata(调用数据区)
    • 只读区域,存储本次交易或调用的输入数据(例如函数参数)。
    • 在外部调用时传入,比如用户调用合约函数时附带的 data 字段。
    • 功能 :一个只读 的字节区域,存储触发本次合约执行的原始输入数据 。例如,当你调用一个函数transfer(address to, uint amount)时,函数选择器和参数就编码在这里。
    • 特点
      • 完全只读,无法修改。
      • Gas成本模型特殊:在普通调用中,读取calldata不消耗Gas;但在合约创建(CREATE)时,calldata是合约的初始化代码,其字节数会影响部署成本。
      • 对于external函数的数组结构体参数,显式声明为calldata类型可以节省大量Gas,因为它避免了将数据复制到内存的开销。
    • 类比:只读的消息报文或网络请求体。
  3. memory(内存)
    • 临时、可读写、线性的字节数组,以 32 字节或 1 字节为单位存取。
    • 用于存储函数内的复杂数据结构(如数组、字符串),生命周期仅限于本次合约执行。
    • 功能 :一个可动态增长的字节数组 ,用于存储函数内的复杂临时数据,如数组、字符串、结构体,或作为调用外部合约前的数据准备区。
    • 特点易失性 ,交易结束即释放。可读可写 ,支持以字节或字(32字节)为单位进行精细访问。Gas成本适中。首次扩展(扩容)时需一次性支付Gas,后续访问成本很低。每次调用都从零开始,是线性的、连续的空间。
    • 类比:电脑的RAM。程序运行时的主工作内存。

二、不易变数据区

这部分数据会持久化保存到区块链状态中,在交易之间保持存在。

  1. code(代码区)
    • 只读区域,存储合约的字节码,部署后不可更改(除非使用 delegatecall 调用其他合约代码)。
    • 通过 EXTCODECOPY 等指令可读取其他合约的代码。
    • 功能 :存储合约的运行时字节码。这是合约部署后,其代码的只读副本。
    • 特点
      • 完全只读,不可变 (通过CREATE2和自毁可以变相实现代码更新,但原地址代码不变)。
      • 可以通过EXTCODECOPY等指令读取其他合约的代码,用于实现代理模式或合约验证。
      • 部署时,代码大小直接影响一次性部署成本。
    • 类比:存储在ROM中的可执行程序文件。
  2. storage(存储区)
    • 持久化的键值存储(256 位 → 256 位),合约状态变量存放于此。
    • 读写成本高(gas 费昂贵),因为会修改区块链全局状态。
    • 功能 :合约的永久存储 ,所有状态变量 (非常量、常量)都存储在这里。它是一个巨大的(2²⁵⁶ 槽位)mapping,每个槽位为32字节。
    • 特点
      • 持久化:写入storage即修改了区块链的全球状态。
      • Gas极其昂贵
        • SSTORE(写入)首次写入一个非零值约20,000 Gas,修改现有值约5,000 Gas。
        • SLOAD(读取)约800 Gas。
      • 优化Storage使用是合约Gas优化的重中之重(如使用更小的数据类型打包、使用映射代替数组)。
    • 类比:硬盘数据库。每一次写入都像在做一个永久的、不可篡改的数据库提交。

三、关系总结

区域 可变性 生命周期 Gas 成本
stack 可变 指令执行期间 免费(几乎)
calldata 只读 本次交易 较低(创建交易时付费)
memory 可变 本次合约执行 动态(扩展时收费)
code 只读 合约生命周期 仅部署时付费
storage 可变 永久(除非修改) 很高

四、交互流程与Gas影响分析

  1. 一次典型的函数调用
    • 用户交易携带 calldata 进入EVM。
    • EVM从 code 区加载合约字节码并开始执行。
    • 执行指令在 stack 上进行算术和逻辑运算。
    • 如果需要复杂临时数据(如解析calldata中的数组),则在 memory 中分配空间进行操作。
    • 如果函数需要更新合约状态(如更新余额),则执行昂贵的 storage 写入操作。
  2. Gas优化的关键启发
    • 最小化Storage操作 :这是节省Gas的黄金法则。使用事件(event)记录而非存储,使用内存变量进行计算,最后再一次性写入Storage。
    • 善用Calldata和Memory :对于外部函数,大数组应使用calldata传入,避免复制到memory
    • 警惕栈深度:复杂的函数调用链和递归可能导致栈溢出(超过1024深度),使交易失败。
    • 代码大小:庞大的合约代码会增加部署成本和调用成本(因为需要加载更多代码)。

安全视角

  • 栈溢出:恶意递归调用可能耗光栈空间。
  • Storage冲突:在代理合约或可升级合约中,Storage布局的不兼容会导致严重漏洞。
  • 内存扩展攻击:攻击者可能诱导合约操作一个巨大的内存数组,导致Gas消耗激增而拒绝服务(DoS)。

总结

你提供的这张图精炼地概括了EVM的存储层次结构

  • 栈和内存"工作车间" ,快但临时。
  • Calldata"输入原料" ,只读。
  • Storage"成品仓库" ,改造成本高。
  • Code"生产线图纸" ,一旦部署,不易更改。
相关推荐
~木雨1 小时前
String 底层原理与常量池全解析:byte []+coder、intern 陷阱、substring 内存泄漏,一篇讲透
java·jvm·内存优化·string·字符串常量池
cfm_29142 小时前
ThreadLocal 内存泄漏
java·jvm
liangbo74 小时前
JVM规范第 6 章:Java 虚拟机指令集
java·jvm
会周易的程序员15 小时前
给 PLC 写一个字节码虚拟机:STVM 虚拟机架构设计
开发语言·c++·虚拟机·软plc·iec61131·stvm
2501_9378609419 小时前
Java多线程初阶(下)—— synchronized、volatile、wait/notify与经典并发工具
java·开发语言·jvm
IvorySQL21 小时前
PostgreSQL 日报|内核多项缺陷修复与数据校验补丁推进(8 月 27 日)
数据库·postgresql·区块链
zhanghaha131421 小时前
Python进阶教程:19_量化交易 零基础超详细入门教程
区块链
二十雨辰1 天前
[Java]-JVM面试题
java·开发语言·jvm
IT_Octopus1 天前
G1 与 GC 日志入门课程(以本次离线菜单 Full GC 事故为教材)
java·jvm
mykj15511 天前
Web3开发系统不是炒币,是互联网的“对账本”逻辑
web3·区块链·rwa·钱包区块链系统开发