跳到主要内容

EVM 内存布局

· 阅读需 5 分钟

合约内存是一个简单的字节数组,其中数据存储可以使用 32 字节(256 位)或 1 字节(8 位)的数据块存储数据,但是读取时每次只能读取固定大小的 32 字节(256 位)的数据块。

对内存操作时的操作码有:

  • MSTORE(offset, value):从内存位置 offset 开始存储 32 字节的 value
  • MSTORE8(offset, value):从内存位置 offset 开始存储 1 字节的 value
  • MLOAD(offset):从内存位置 offset 开始将 32 字节入栈

对于下列操作码

PUSH32 0x1111111111111111111111111111111111111111111111111111111111111111 // 将 32 字节数据入栈
PUSH1 0 // 将 0 入栈
MSTORE // 从内存 0 开始,写入 32 字节数据

PUSH1 0x22 // 将 1 字节数据 0x22 入栈
PUSH1 0x20 // 将 1 字节数据 0x20 入栈 (0x20 是 10 进制 32)
MSTORE8 // 从内存 0x20 开始,写入 1 字节数据 0x22

PUSH1 0x20 // 将 1 字节数据 0x20 入栈
MLOAD // 从内存的 0x20 开始,加载32字节数据入栈

前三行代码执行 MSTORE 后,内存数据变为

1111111111111111111111111111111111111111111111111111111111111111

5 - 7 行代码执行 MSTORE8 后,内存数据变为

1111111111111111111111111111111111111111111111111111111111111111
2200000000000000000000000000000000000000000000000000000000000000

疑惑的是 MSTORE8 是向内存写入 1 字节数据,为什么会出现 32 字节的数据被写入内存。原因是写入数据在内存中的位置是之前未开辟的内存空间时,内存以 32 字节(256 位)为增量扩展。

第 10 行代码 MLOAD, 会将栈顶元素出栈,其值作为内存开始位置, 并往后读取内存 32 字节的数据入栈, 此时内存数据不变,栈变为

2200000000000000000000000000000000000000000000000000000000000000

合约中的内存

将智能合约编译成字节码时都是以 0x6080604052 开头, 60 是操作码 PUSH1, 52 是操作码 MSTORE。翻译成操作码就是

PUSH1 0x80 // 将数据 0x80(10 进制 128) 入栈
PUSH1 0x40 // 将数据 0x40(10 进制 64) 入栈
MSTORE // 从内存 64字节位置开始, 写入数据 0x80

执行完成后内存变为

0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000080

其实在内存中保留了 4 个 32 字节的插槽

  • 0x00 - 0x3f (64 字节): 暂存空间,可用于语句之间,即内联汇编和哈希散列方法
  • 0x40 - 0x5f (32 字节): 空闲内存指针,当前分配的内存大小,空闲内存的起始位置,初始化为 0x80
  • 0x60 - 0x7f (32 字节): 零槽,用作动态内存数组的初始值,永远不能写入值

空闲内存指针是一个指向空闲内存开始位置的指针。将数据写入内存时, 首先引用空闲内存指针来确定数据的存储位置,写入数据完成后,更新内存指针为

新空闲内存指针 = 旧空闲内存指针 + 数据的字节大小

回到之前提到的 0x60806040, 其实是在设置空闲指针

在定义变量时,需要先加载空间指针

PUSH1 0x40 // 0x40(64)入栈
MLOAD // 从内存的64字节处加载32字节的数据入栈(加载空闲指针)

加载完成后, 存入变量值到内存后,更新空闲指针的值。