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

在以太坊虚拟机(EVM)中,数据存储根据可变性 和生命周期 分为几个不同的区域,上图将其分为了 "易变数据区" 和 "不易变数据区"。
一、易变数据区
这部分数据在交易执行期间存在,交易结束后被清除,不永久保存到区块链状态。
- stack(栈)
- 大小为 32 字节 × 256 位(即 256 个 32 字节的槽位)。
- 用于 EVM 指令执行时的临时数据存储,比如局部变量、计算中间值。
- 访问速度极快,但大小有限,只能通过 push/pop 方式操作。
- 功能 :EVM是一个栈式虚拟机 ,它的指令(如
ADD,MUL,SWAP)主要从栈顶获取操作数并压入结果。它用于存储局部变量、函数参数和计算中间值。 - 特点 :
- 速度极快,Gas成本几乎为零。
- 大小严格限制为1024个元素(256位深 x 32字节宽)。这是为了防止无限递归调用等攻击,也是许多"栈溢出"漏洞的根源。
- 只能通过
PUSH、POP、DUP、SWAP等特定指令访问,无法随机访问。
- 类比:CPU的L1缓存。容量最小,但速度最快。
- calldata(调用数据区)
- 只读区域,存储本次交易或调用的输入数据(例如函数参数)。
- 在外部调用时传入,比如用户调用合约函数时附带的 data 字段。
- 功能 :一个只读 的字节区域,存储触发本次合约执行的原始输入数据 。例如,当你调用一个函数
transfer(address to, uint amount)时,函数选择器和参数就编码在这里。 - 特点 :
- 完全只读,无法修改。
- Gas成本模型特殊:在普通调用中,读取calldata不消耗Gas;但在合约创建(
CREATE)时,calldata是合约的初始化代码,其字节数会影响部署成本。 - 对于
external函数的数组和结构体参数,显式声明为calldata类型可以节省大量Gas,因为它避免了将数据复制到内存的开销。
- 类比:只读的消息报文或网络请求体。
- memory(内存)
- 临时、可读写、线性的字节数组,以 32 字节或 1 字节为单位存取。
- 用于存储函数内的复杂数据结构(如数组、字符串),生命周期仅限于本次合约执行。
- 功能 :一个可动态增长的字节数组 ,用于存储函数内的复杂临时数据,如数组、字符串、结构体,或作为调用外部合约前的数据准备区。
- 特点 :易失性 ,交易结束即释放。可读可写 ,支持以字节或字(32字节)为单位进行精细访问。Gas成本适中。首次扩展(扩容)时需一次性支付Gas,后续访问成本很低。每次调用都从零开始,是线性的、连续的空间。
- 类比:电脑的RAM。程序运行时的主工作内存。
二、不易变数据区
这部分数据会持久化保存到区块链状态中,在交易之间保持存在。
- code(代码区)
- 只读区域,存储合约的字节码,部署后不可更改(除非使用 delegatecall 调用其他合约代码)。
- 通过
EXTCODECOPY等指令可读取其他合约的代码。 - 功能 :存储合约的运行时字节码。这是合约部署后,其代码的只读副本。
- 特点 :
- 完全只读,不可变 (通过
CREATE2和自毁可以变相实现代码更新,但原地址代码不变)。 - 可以通过
EXTCODECOPY等指令读取其他合约的代码,用于实现代理模式或合约验证。 - 部署时,代码大小直接影响一次性部署成本。
- 完全只读,不可变 (通过
- 类比:存储在ROM中的可执行程序文件。
- 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影响分析
- 一次典型的函数调用 :
- 用户交易携带
calldata进入EVM。 - EVM从
code区加载合约字节码并开始执行。 - 执行指令在
stack上进行算术和逻辑运算。 - 如果需要复杂临时数据(如解析calldata中的数组),则在
memory中分配空间进行操作。 - 如果函数需要更新合约状态(如更新余额),则执行昂贵的
storage写入操作。
- 用户交易携带
- Gas优化的关键启发 :
- 最小化Storage操作 :这是节省Gas的黄金法则。使用事件(
event)记录而非存储,使用内存变量进行计算,最后再一次性写入Storage。 - 善用Calldata和Memory :对于外部函数,大数组应使用
calldata传入,避免复制到memory。 - 警惕栈深度:复杂的函数调用链和递归可能导致栈溢出(超过1024深度),使交易失败。
- 代码大小:庞大的合约代码会增加部署成本和调用成本(因为需要加载更多代码)。
- 最小化Storage操作 :这是节省Gas的黄金法则。使用事件(
安全视角
- 栈溢出:恶意递归调用可能耗光栈空间。
- Storage冲突:在代理合约或可升级合约中,Storage布局的不兼容会导致严重漏洞。
- 内存扩展攻击:攻击者可能诱导合约操作一个巨大的内存数组,导致Gas消耗激增而拒绝服务(DoS)。
总结
你提供的这张图精炼地概括了EVM的存储层次结构:
- 栈和内存 是 "工作车间" ,快但临时。
- Calldata 是 "输入原料" ,只读。
- Storage 是 "成品仓库" ,改造成本高。
- Code 是 "生产线图纸" ,一旦部署,不易更改。