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字节的YMLOAD(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);
}
}