Skip to main content

2. Memory

在EVM中,Memory可以被认为是一个可拓展的通过字节寻址的一维数组,开始时是空的,读取写入和扩展都需要花费Gas。同时内存的成本是和使用的成比例上升的,所以即使memory理论上有$2^256$ 个elements但是一般也用不到那么多。同时还有Gas限制,所以更用不到了。

Calldata类似,但是Calldata无法扩展或者覆盖,并且充当合约调用的输入

Memory和Calldata不是持久的,在交易结束后就会被丢弃,同时几乎所有的从内存中的读取操作都是以32字节为单位的。

Gas Cost

在智能合约执行期间,可以使用操作码访问内存。当首次访问偏移量(读取或写入)时,内存可能会触发扩展,这会消耗gas。

当访问的字节偏移量(模 32)大于之前的偏移量时,可能会触发内存扩展。如果发生内存扩展的较大偏移量触发,则会计算访问较高偏移量的成本,并将其从当前调用上下文中可用的总 Gas 中删除。

一般总成本的计算方式如下:

memory_size_word = (memory_byte_size + 31) / 32
memory_cost = (memory_size_word ** 2) / 512 + (3 * memory_size_word)

当触发内存扩展时,只需支付额外的内存字节费用。因此,特定操作码的内存扩展成本为:

memory_expansion_cost = new_memory_cost - last_memory_cost

memory_byte_size 可以通过操作码 MSIZE 获得。 MSIZE 触发的内存扩展成本呈二次方增长,通过使更高的偏移量成本更高来抑制内存的过度使用。任何访问内存的操作码都可能触发扩展(例如 MLOAD、RETURN 或 CALLDATACOPY)。

Memort Data Structure

合约内存是一个简单的字节数组,其中数据可以用32字节(256位)或者1字节(8位)进行存储,但是读取是32字节的读取。

关于存储的操作主要有三个Opcode:

  • MSTORE(x,y) - 从内存位置X存储32字节的Y
  • MLOAD(x) - 将内存位置x开始的32字节加载到stack上
  • MSTORE8(x,y) - 将1字节的y存储到位置x上

Memory是一个数组,也就是我们能从任何位置读取并且返回32字节的数据,Memory是线性的,可以在字节级别进行寻址。

Free Memory Pointer

Memory中的布局是这样的:

Free Memory Pointer只是执行空闲内存开始位置的指针,能够确保智能合约跟踪那些内存已经被写入,哪些没有被写入。

Free Memory Pointer可以防止合约覆盖已分配给另外一个变量的内存

当一个变量写入内存的时候,合约将首先引用Free memory pointer来确定数据应该存储在哪里。

然后,它通过记录要写入新位置的数据量来更新空闲内存指针。这两个值的简单相加将产生新的可用内存的开始位置。

freeMemoryPointer + dataSizeBytes = newFreeMemoryPointer 

一开始空闲指针的定义是:

60 80                       =   PUSH1 0x80
60 40 = PUSH1 0x40
52 = MSTORE

这些有效地表明空闲内存指针位于内存中的字节 0x40(十进制 64),其值为 0x80(十进制 128)。

那为什么说是从0x80开始空闲呢?这是因为Solidity 的内存布局保留了 4 个 ``32` 字节槽:

  • 0x00 - 0x3f (64 bytes): scratch space 暂存空间
    • 暂存空间可用于语句之间,即内联汇编内和散列方法。
  • 0x40 - 0x5f (32 bytes): free memory pointer 空闲内存指针
    • 空闲内存指针,当前分配的内存大小,空闲内存的起始位置,初始为0x80
  • 0x60 - 0x7f (32 bytes): zero slot 零槽
    • 零槽用作动态内存阵列的初始值,并且永远不应该被写入。

接下来让我们来看看EVM是如何操作Memory的:

Operate Memory

Struct

虽然到这里我们还没有提到Storage中的slot,但是先记住,在Memory中Data不是和Storage一样Packed,哪怕没有32字节大小,比如可能是一个uint32或者address,他依然会占据一个32字节的槽。E.g:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract MemBasic {
struct Point {
uint256 x;
uint32 y;
uint32 z;
}

function read() public pure returns (uint256 x,uint256 y,uint256 z) {
Point memory p = Point(1,2,3);

}
}

我们首先定义了一个struct,然后在memory中初始化了他,现在我想要使用mload来读取memory中的x,y,z我们该怎么做呢:

assembly {
x := mload(0x80) // 空闲指针初始化的位置
y := mload(add(0x80,0x20)) // 往后加32字节
z := mload(add(0xa0,0x20)) // 再加32字节
}

这是读取,下面我们再来看看如何写入struct数据:

Point memory p;
assembly {
mstore(0x80,1)
mstore(add(0x80,0x20),2)
mstore(add(0xa0,0x20),3)
}

这时候如果想看一下Free memory pointer指向哪里我们可以使用:

Free_memory_pointer := mload(0x40) //因为这是存储指针的位置

Fixed size Array

和Struct一样,只需要记住在Memory中不管大小有多大,都是32字节的,不会打包

contract MemFixedArray {
function test_read()
public
pure
returns (uint256 a0, uint256 a1, uint256 a2)
{
// arr is loaded to memory starting at 0x80
// Each array element is stored as 32 bytes
uint32[3] memory arr = [uint32(1), uint32(2), uint32(3)];

assembly {
a0 := mload(0x80)
a1 := mload(0xa0)
a2 := mload(0xc0)
}
}

function test_write()
public
pure
returns (uint256 a0, uint256 a1, uint256 a2)
{
uint32[3] memory arr;

assembly {
// 0x80
mstore(arr, 11)
// 0xa0
mstore(add(arr, 0x20), 22)
// 0xc0
mstore(add(arr, 0x40), 33)
}

a0 = arr[0];
a1 = arr[1];
a2 = arr[2];
}
}

DynamicArray

一样,直接贴代码:

contract MemDynamicArray {
function test_read()
public
pure
returns (bytes32 p, uint256 len, uint256 a0, uint256 a1, uint256 a2)
{
uint256[] memory arr = new uint256[](5);
arr[0] = uint256(11);
arr[1] = uint256(22);
arr[2] = uint256(33);
arr[3] = uint256(44);
arr[4] = uint256(55);

assembly {
p := arr
// 0x80
len := mload(arr)
// 0xa0
a0 := mload(add(arr, 0x20))
// 0xc0
a1 := mload(add(arr, 0x40))
// 0xe0
a2 := mload(add(arr, 0x60))
}
}

function test_write() public pure returns (bytes32 p, uint256[] memory) {
uint256[] memory arr = new uint256[](0);

assembly {
p := arr
// Store length of arr
mstore(arr, 3)
// Store 1, 2, 3
mstore(add(arr, 0x20), 11)
mstore(add(arr, 0x40), 22)
mstore(add(arr, 0x60), 33)
// Update free memory pointer
mstore(0x40, add(arr, 0x80))
}

// Data will be ABI encoded when arr is returned to caller
return (p, arr);
}
}