深入了解EVM的内存
这是深入Solidity数据存储位置系列的另一篇。在今天的文章中,我们将学习EVM内存的布局,它的保留空间,空闲内存指针,如何使用memory
引用来读写内存,以及使用内存时的常规最佳做法。
我们将使用 Ethereum Name Service (ENS)中的合约代码片段,用有意义的例子支持这篇文章。这将帮助我们更好地理解这个流行项目背后的智能合约是如何在底层工作的。
MSTORE
+MSTORE8
)。MSIZE
)。memory
引用在介绍性文章深入Solidity数据存储位置中,我把EVM描述为一个工业工厂。在工厂的某些地方,你会发现由操作员控制的机器和机器人。
这些机器将无法加工的大块钢铁/铝材分解成小块。
我们可以用同样的例子来说明以太坊。EVM作为一个堆栈机器,它在32字节的字上运行。当EVM遇到大于32字节的数据(复杂的类型,如string
,bytes
,struct
或数组),它不能在堆栈中处理它们,因为这些项目太大。
因此,EVM需要把这些数据带到其他地方去处理。它有一个专门的地方:内存(memory)。通过将这些变量放在内存中,EVM就可以将它们以较小的块状形式,一个接一个地送到堆栈中。
EVM内存也被用于内置Solidity的复杂操作,如abi-encoding,abi-decoding或通过keccak256的哈希函数。对于这些特定的情况,想象一下,内存作为EVM的一个刮板或白板。
老师或科学家可能会使用白板在上面写东西来解决问题m这同样适用于EVM。EVM使用内存作为白板来执行这些操作或计算,并返回最终值。
对于abi.decode(...)
或keccak256
,内存是输入的来源。对于abi.encode(...)
来说,内存是输出的储存地。
EVM内存有4个主要特点:
EVM内存是一个字节寻址的空间。中的所有字节最初都是空的(定义为零)。它是个可变的数据区,意味着你可以从它那里读取和写入。像calldata一样,内存是通过字节索引来寻址的,但是我们将在"与内存交互 "一节中看到,在内存中一次只能读32字节的字。
备注:计算机中,通常把单位处理的数据大小称为一个字长,简称字
EVM的内存也是易失的。存储在内存中的值在外部调用之间不会持续存在。
当一个合约调用另一个合约时,会获得一个新的内存实例。
内存并没有被擦除和清空。EVM内存的每个新实例都是特定于一个执行环境,即当前的合约执行。
因此,你应该记住,EVM内存是特定于1)消息调用和2)被调用合约的执行环境的。我们将在后面的单独章节中更详细地解释这个概念。
内存是线性的,可以在字节级进行寻址。
把内存想象成一个非常大的(甚至是巨大的!)字节数组,比如byte[]
。
当你与EVM内存交互时,你从(我称之为)"内存块 "读取或写入,这些内存块有32字节长。
内存中的前4个32字节的字是保留空间,用于不同的用途。
前2个字(偏移量位置 0x00
和0x20
):用于哈希函数的临时空间
偏移量位置 0x40
和0x50
,第3个字,空闲内存指针
偏移量位置 0x60
:零位插槽(永久为零),用作空动态内存数组的初始值。
空闲内存指针(偏移量位置0x40)是EVM内存中最关键的部分。必须小心处理,特别是在汇编/Yul中。我们将在一个单独的章节中介绍它。
更多信息请参见Solidity文档中的内存布局。
EVM内存是一个线性数组,可以通过字节索引(称为偏移量offset)来寻址。它最多可以包含多少个字节呢?
这个数组有多大?EVM的内存有多大?
这个问题的答案就在geth的源代码中(下面的截图)。看一下所使用的转换类型。
来源: instructions.go (geth client source code).
我们可以从geth客户端的这个截图中看到,mStart.Uint64()
将内存偏移量转换成uint64
值。意味着你能放在内存中的最大数据量是一个uint64
数字的最大值。
如果指定的偏移量超过了这个值,它就会被回退。
只能在函数内部指定memory
,而不能在合约层面的函数外部指定。
以下数据和值默认总是在内存中。
memory
。通过复杂类型的变量/值,指的是诸如结构体
、数组、bytes
和strings
等变量。
一旦函数调用结束,这些用关键字memory
定义的变量将消失。这就是我们之前所说的 不持久化
的意思。
原因是,memory
告诉Solidity在运行时为该变量创建一块空间,保证其大小和结构,以便在函数执行过程中将来用于该函数。
Solidity文档指出,在EVM内存中。
...读被限制在256位的宽度,而写可以是8位或256位的宽度。
如果我们看一下黄皮书,我们可以看到一个操作码被定义为从内存读取(MLOAD
),两个操作码被定义为写入内存:MSTORE
和MSTORE8
。
你可以使用MLOAD
操作码从内存中读取。
下面是黄皮书中关于MLOAD
操作码规范的内容。
让我们来揭开这个非常正式的公式的神秘面纱!
黄皮书中的公式可以解释为如下:
Us[0]
= 栈顶元素Us'[0]
= 被放在栈顶的结果项。Um
=内存中从特定偏移开始的内容。公式Um[Us[0]...Us[0]+31]]
可以用普通英语翻译如下:
Us[0]
。Um
(=偏移量)。Us[0]
读出后面的31个字节(Us[0]+31
)。从内存中读出的数据一次只能读32个字节。这意味着你每次只能用mload
操作码从内存中读取32个字节。
这些操作码可以在Solidity内联汇编或独立的Yul代码中使用。
让我们看一下ENS合约中的一个例子:SHA1.sol
在下面的代码片段中,mload
操作码被使用了两次。
你可以使用以下两个操作码中的一个向内存写入:
MSTORE
→ 在内存中写一个字(=32字节);MSTORE8
→ 在内存中写一个单字节;这条推文显示了geth客户端的EVM实例如何从堆栈中取出参数及作为MSTORE
的输入。
在Solidity中
在Solidity中,每当你用memory
关键字实例化一个变量并赋值(bytes/字符串,或者函数的返回值),底层的EVM就会执行mstore
指令。
下面是ENS的DNSRegistar.sol
合约中的一个例子:
在汇编中
mstore
操作码可以在内联汇编中使用。它接受两个参数:
请看mstore
是如何在同一个ENS合约SHA1.sol.
中的汇编中使用的:
关于
MSIZE
操作码的更多细节,见evm.codes上的操作码解释。
初步猜测,EVM操作码MSIZE
从它的名字上看,似乎它将返回存储在内存中的数据多少。或者换句话说,当前有多少字节写在内存中。
MSIZE
操作码其实挺复杂。Solidity编译器的C++源代码提供了更多信息来理解它:
MSIZE
操作码返回在当前执行环境中访问内存的最高字节偏移。这个大小总是字的倍数(32字节)。
但是在Solidity中,"在内存中存储了多少字节 "和"在内存中访问的最大索引/偏移量 "之间有什么区别?
我们将用Solidity本身的一个实际例子来说明! 请看下面的代...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!