合约内存是一个简单的字节数组,其中数据存储可以使用 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字节的数据入栈(加载空闲指针)
加载完成后, 存入变量值到内存后,更新空闲指针的值。