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

核心思想:分层存储

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

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

一、易变数据区

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

  1. stack(栈)
    • 大小为 32 字节 × 256 位(即 256 个 32 字节的槽位)。
    • 用于 EVM 指令执行时的临时数据存储,比如局部变量、计算中间值。
    • 访问速度极快,但大小有限,只能通过 push/pop 方式操作。
    • 功能 :EVM是一个栈式虚拟机 ,它的指令(如ADD, MUL, SWAP)主要从栈顶获取操作数并压入结果。它用于存储局部变量、函数参数和计算中间值。
    • 特点 :
      • 速度极快,Gas成本几乎为零。
      • 大小严格限制为1024个元素(256位深 x 32字节宽)。这是为了防止无限递归调用等攻击,也是许多"栈溢出"漏洞的根源。
      • 只能通过PUSH、POP、DUP、SWAP等特定指令访问,无法随机访问。
    • 类比: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 是 "生产线图纸" ,一旦部署,不易更改。
相关推荐
晒太羊的猫10 小时前
jvm面试题
jvm
新新学长搞科研14 小时前
【会议征稿】第三届数字经济与计算机科学国际学术会议(DECS 2026)
区块链·计算机科学·数字经济
夜之眷属14 小时前
Core dump 崩溃排查:JVM 宕机后,那份 core 文件怎么用 gdb 还原现场
java·运维·服务器·jvm
用户0942485680315 小时前
第31章:JVM字节码解释器与模板解释器源码路径
java·jvm
阿俊-全栈开发17 小时前
LikeShop 版本升级与备份:从旧版平滑迁移的完整步骤
jvm·数据库·oracle·开源·likeshop·likeshop开源商城
Nebula_g19 小时前
JavaSE加强:线程Thread(线程安全重点)
java·开发语言·jvm·算法·javase·技术栈
JAVA面经实录91719 小时前
Java高级后端 · 全套面试通关手册(线上故障排查)
java·jvm·面试
梦帮科技19 小时前
【3.0】上线与运维:节点监控、水龙头、区块浏览器与安全清单
运维·安全·web安全·金融·区块链·密码学·安全架构
kjmkq20 小时前
区块链项目出海宣发,依托全球Web3垂直媒体快速建立行业背书
web3·区块链·媒体
sibylyue1 天前
SQLite 运行模式
java·jvm·sqlite