译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]
图片来源: Mech Mind[4] on Unsplash[5]
这是深入 Solidity 数据存储位置[6]系列的另一篇。在今天的文章中,我们将学习 EVM 内存的布局,它的保留空间,空闲内存指针,如何使用memory
引用来读写内存,以及使用内存时的常规最佳做法。
我们将使用Ethereum Name Service(ENS)[7]中的合约代码片段,用有意义的例子支持这篇文章。这将帮助我们更好地理解这个流行项目背后的智能合约是如何在底层工作的。
目录
- 简介
- EVM 内存 - 概述
- 内存的布局
- 内存的基础知识
- 从内存中读取("MLOAD")。
- 写入内存(
MSTORE
MSTORE8
)。 - 了解内存大小(
MSIZE
)。 - 空闲内存指针
- 作为函数参数的
memory
引用 - 在函数内部"内存"(memory) 引用
- 扩展内存成本
- 合约调用之间的内存
- 总结
介绍
在介绍性文章深入 Solidity 数据存储位置[8]中,我把 EVM 描述为一个工业工厂。在工厂的某些地方,你会发现由操作员控制的机器和机器人。
这些机器将无法加工的大块钢铁/铝材分解成小块。
我们可以用同样的例子来说明以太坊。EVM 作为一个堆栈机器,它在 32 字节的字上运行。当 EVM 遇到大于 32 字节的数据(复杂的类型,如string
,bytes
,struct
或数组),它不能在堆栈中处理它们,因为这些项目太大。
因此,EVM 需要把这些数据带到其他地方去处理。它有一个专门的地方:内存(memory)。通过将这些变量放在内存中,EVM 就可以将它们以较小的块状形式,一个接一个地送到堆栈中。
EVM 内存也被用于内置 Solidity 的复杂操作,如 abi-encoding,abi-decoding 或通过 keccak256 的哈希函数。对于这些特定的情况,想象一下,内存作为 EVM 的一个刮板或白板。
老师或科学家可能会使用白板在上面写东西来解决问题 m 这同样适用于 EVM。EVM 使用内存作为白板来执行这些操作或计算,并返回最终值。
图片来源:https://giphy.com/explore/physics-lecture
对于abi.decode(...)
或keccak256
,内存是输入的来源。对于abi.encode(...)
来说,内存是输出的储存地。
EVM 内存 - 概述
EVM 内存有 4 个主要特点:
- 廉价 = 在 Gas 方面。
- 可变 = 可以被覆盖和改变。
- 相对于交易 = 来自于函数调用,或构造函数 (=合约创建)
- 短期的 = 不持久的和在外部函数调用之间被删除。
EVM 内存是一个字节寻址的空间。中的所有字节最初都是空的(定义为零)。它是个可变的数据区,意味着你可以从它那里读取和写入。像 calldata 一样,内存是通过字节索引来寻址的,但是我们将在 "与内存交互 " 一节中看到,在内存中一次只能读 32 字节的字。
备注:计算机中,通常把单位处理的数据大小称为一个字长,简称字
EVM 的内存也是易失的。存储在内存中的值在外部调用之间不会持续存在。
当一个合约调用另一个合约时,会获得一个新的内存实例。
内存并没有被擦除和清空。EVM 内存的每个新实例都是特定于一个执行环境,即当前的合约执行。
因此,你应该记住,EVM 内存是特定于 1)消息调用和 2)被调用合约的执行环境的。我们将在后面的单独章节中更详细地解释这个概念。
内存的布局
内存是线性的,可以在字节级进行寻址。
把内存想象成一个非常大的(甚至是巨大的!)字节数组,比如byte[]
。
当你与 EVM 内存交互时,你从(我称之为) "内存块 " 读取或写入,这些内存块有 32 字节长。
保留空间
内存中的前 4 个 32 字节的字是保留空间,用于不同的用途。
- 前 2 个字(偏移量位置
0x00
和0x20
):用于哈希函数的临时空间 - 偏移量位置
0x40
和0x50
,第 3 个字,空闲内存指针 - 偏移量位置
0x60
:零位插槽(永久为零),用作空动态内存数组的初始值。
空闲内存指针(偏移量位置 0x40)是EVM 内存中最关键的部分。必须小心处理,特别是在汇编/Yul 中。我们将在一个单独的章节中介绍它。
更多信息请参见 Solidity 文档中的内存布局[9]。
最大的内存限制
EVM 内存是一个线性数组,可以通过字节索引(称为偏移量 offset)来寻址。它最多可以包含多少个字节呢?
这个数组有多大?EVM 的内存有多大?
这个问题的答案就在 geth 的源代码中(下面的截图)。看一下所使用的转换类型。
来源:instructions.go (geth client source code)[10]
我们可以从 geth 客户端的这个截图中看到,mStart.Uint64()
将内存偏移量转换成uint64
值。意味着你能放在内存中的最大数据量是一个uint64
数字的最大值。
如果指定的偏移量超过了这个值,它就会被回退。
内存的基本原理
只能在函数内部指定memory
,而不能在合约层面的函数外部指定。
以下数据和值默认总是在内存中。
- 复杂类型的函数参数。
- 复杂类型的局部变量(在函数体内部)。
- 从函数返回的值,无论其类型如何(都是通过return操作码完成的)。
- 任何由函数返回的复合值类型必须指定关键字
memory
。
通过复杂类型的变量/值,指的是诸如结构体
、数组、bytes
和strings
等变量。
一旦函数调用结束,这些用关键字memory
定义的变量将消失。这就是我们之前所说的 不持久化
的意思。
原因是,memory
告诉 Solidity 在运行时为该变量创建一块空间,保证其大小和结构,以便在函数执行过程中将来用于该函数。
与内存交互 - 概述
Solidity 文档指出,在 EVM 内存中。
...读被限制在 256 位的宽度,而写可以是 8 位或 256 位的宽度。
如果我们看一下黄皮书,我们可以看到一个操作码被定义为从内存读取(MLOAD
),两个操作码被定义为写入内存:MSTORE
和MSTORE8
。
来源: Ethreum Yellow Paper, page 34[11]
从内存中读取
你可以使用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 个字节。
来源:https://twitter.com/721Orbit/status/1511961696692322305
这些操作码可以在 Solidity 内联汇编或独立的 Yul 代码中使用。
示例:ENS 合约的 SHA1 库
让我们看一下 ENS 合约中的一个例子:SHA1.sol[12]
在下面的代码片段中,mload
操作码被使用了两次。
- 首先检索空闲内存指针 scratch 变量, 它被用作内存中的指针,数据的 sha1 哈希值将被计算和写入。
- 第二,获取数据变量的长度(=字节数)。
来源:Github 上的 ENS 源代码:SHA1.sol[13]
写入内存
你可以使用以下两个操作码中的一个向内存写入:
MSTORE
→ 在内存中写一个字(=32 字节);MSTORE8
→ 在内存中写一个单字节;
这条推文[14]显示了 geth 客户端的 EVM 实例如何从堆栈中取出参数及作为MSTORE
的输入。
在 Solidity 中
在 Solidity 中,每当你用memory
关键字实例化一个变量并赋值(bytes/字符串,或者函数的返回值),底层的 EVM 就会执行mstore
指令。
下面是 ENS 的DNSRegistar.sol
合约中的一个例子:
来源:Github 上的 ENS 源代码 DNSRegistar.sol[15]
在汇编中
mstore
操作码可以在内联汇编中使用。它接受两个参数:
- 要写入内存的偏移量。
- 要写入内存中的数据。
请看mstore
是如何在同一个 ENS 合约SHA1.sol.
中的汇编中使用的:
来源:Github 上的 ENS 源代码,SHA1.sol[16]
了解内存大小
关于
MSIZE
操作码的更多细节,见evm.codes[17]上的操作码解释。
初步猜测,EVM 操作码MSIZE
从它的名字上看,似乎它将返回存储在内存中的数据多少。或者换句话说,当前有多少字节写在内存中。
MSIZE
操作码其实挺复杂。Solidity 编译器的 C 源代码提供了更多信息来理解它:
来源: SemanticInformation.cpp[18]
MSIZE
操作码返回在当前执行环境中访问内存的最高字节偏移。这个大小总是字的倍数(32 字节)。
但是在 Solidity 中,"在内存中存储了多少字节 "和 "在内存中访问的最大索引/偏移量 " 之间有什么区别?
我们将用 Solidity 本身的一个实际例子来说明! 请看下面的代码片断:
代码语言:javascript复制pragma solidity ^0.8.0;
contract TestingMsize {
function test()
public
pure
returns (
uint256 freeMemBefore,
uint256 freeMemAfter,
uint256 memorySize
)
{
// before allocating new memory
assembly {
freeMemBefore := mload(0x40)
}
bytes memory data = hex"cafecafecafecafecafecafecafecafecafecafecafecafecafecafecafecafe";
// after allocating new memory
assembly {
// freeMemAfter = freeMemBefore 32 bytes for length of data data value (32 bytes long)
// = 128 (0x80) 32 (0x20) 32 (0x20) = 0xc0
freeMemAfter := mload(0x40)
// now we try to access something further in memory than the new free memory pointer :)
let whatIsInThere := mload(freeMemAfter)
// now msize will return 224.
memorySize := msize()
}
}
}
这里正在发生什么?
第 1 步: freeMemBefore
首先返回空闲内存指针: 0x80 (= 128)
第 2 步:我们然后在内存中写入data
(64 字节)。空闲内存指针被更新: (freeMemAfter
)成为0xc0 (= 192)
。
在上面的例子中,空闲内存指针被自动更新,是因为我们在汇编块之外。如果你在汇编中通过
mstore
或通过类似的操作码写到内存,如calldatacopy
,空闲内存指针不会被自动更新。你有责任自己手动去做。 记住 Solidity 文档中提到的规则。"内联汇编有些像高级语言,但它是极其底层"。
在这一点上,技术上有 192 字节分配在内存中。
代码语言:javascript复制 32 bytes
x 4 (the first 4 reserved spaces in memory)
---------------------
= 128
64 bytes (the variable `data`)
---------------------
= 192 (total)
现在请注意第 28 行。我们试图读取内存中的偏移量0x0c (192)
。
第三步:当我们使用msize
(第 31 行)时,我们得到的数字是224(=0xe0)
。刚才发生了什么?在内存中总共只有 192 字节的存储/被分配。这 224 是从哪里来的?
224 = 192 32
. 所以msize
返回的值是存储在内存中的总字节数(192
) 32。我们刚刚触发并见证了一次内存扩展。内存每次总是扩展 32 字节。
没有比evm.codes[19]对msize
操作码更好的解释了,可以总结一下:
msize 跟踪当前执行中曾经访问过的最高偏移量。第一次写或读到更大的偏移量将触发内存扩展[20]
空闲内存指针
在 OpenZeppelin 系列文章 "解构智能合约 "中, 揭示了每个智能合约的前 5 个字节背后的操作代码的含义。
代码语言:javascript复制0x6080604052...
来源:OpenZeppelin,解构智能合约(第一部分)[21]
简而言之,这一连串的操作码将数字0x80
(十进制 128)存储到内存的0x40
(十进制 64)位置。为了什么?
正如上一节内存布局
所解释的,内存中的前 4 个字被保留用于特定用途。第 3 个字--位于内存中的0x40
位置 - -被称为空闲内存指针。
Open Zeppelin 将空闲内存指针描述为*"对内存中第一个未使用字的引用 "*。它能够知道在内存中的哪个位置(哪个偏移量)有空闲的空间可以写入数据。这是为了避免覆盖已经存在于内存中的数据。
空闲内存指针是 EVM 最重要和最关键的东西之一,需要了解。
Solidity 中的空闲内存指针
在 Solidity 中,当进行bytes memory myVariable
这样的代码片段时,空闲内存指针被自动获取 更新。
让我们看一个例子。对于 Solidity 的代码:
这些是由 Solidity 编译器生成的操作码。我们感兴趣的是,从指令056
到指令065
,空闲内存指针是如何被获取和更新的:
一个基本的操作码序列,用于写入一个字符串内存。
当一个字符串或一些数据在 Solidity 中被写入内存时,EVM 总是执行以下最初的两个步骤。
步骤 1:获取空闲内存指针
EVM 首先从内存位置0x40
加载空闲内存指针。由mload
返回的值是0x80
。空闲内存指针告诉我们,在内存中第一个有空闲空间可以写入的地方是偏移量0x80
。这就是我们最后栈顶部的内容:
第 2 步:分配内存 用新的空闲内存指针更新。
EVM 现在将在内存中为 "string test"保留这个位置。它把释放内存指针返回的值保留在堆栈中。
但是 Solidity 编译器很聪明,很安全在分配和写入内存的任何值之前,它总是更新空闲内存指针。这是为了指向内存中的下一个空闲空间。
根据 ABI 规范,一个 "string"由两部分组成:长度 字符串本身。那么下一步就是更新空闲内存指针。EVM 在这里说的是"我将在内存中写入 2 x 32 字节的字。所以新的空闲内存指针将比现在的指针多出 64 字节"。
下面的操作码的作用很简单:
- 复制空闲内存指针的当前值 =
0x80
。 - 给它加上
0x40
(=64 的小数,为 64 字节) - 将
0x40
(=空闲内存指针的位置)推到堆栈上 - 通过
MSTORE
用新的值更新空闲内存指针
在汇编中的内存指针
在内联汇编中,必须小心处理空闲内存指针!
它不仅要被手动获取,而且还要被手动更新!
因此,在汇编中处理内存时,你必须小心。你必须确保在汇编中总是先获取空闲内存,然后写入空闲内存指针指向的内存位置,如果你不想最终覆盖内存中已经有一些内容的话。
一旦在内存中写入,你必须确保用新的自由内存偏移量来更新空闲内存指针。
总之,当涉及到空闲内存指针时,一定要记住 OpenZeppelin 的建议。
在汇编级操作内存时,你必须非常小心。否则,你可能会覆盖一个保留的空间。
在检查空闲内存指针所指向的内存位置上实际存储的内容之前,向空闲内存指针写入可能不是一个好的做法。
示例
来自Gonçalo Sá[22] 的 solidity-byte-utils 库,让我们来看看这个流行的 Solidity 库,用来操作bytes
。如果你仔细观察每个函数的初始汇编代码,你会发现加载空闲内存指针是第一件事。
在函数的最后,tempBytes
被返回。在低层上,这可以翻译为 :返回tempBytes
所指向的内存偏移处的内存中存在的东西。
来源:GBSPS/solidity-bytes-utils on Github, BytesLib.sol[23]
内存引用作为函数参数
在 Solidity 中,当我们必须将一个动态或复杂类型的参数传递给一个函数时,我们每次都会使用memory
引用。
例如在 ENS 合约中,DNSRegistar.sol
的claim(...)
函数需要两个参数:一个name
和proof
,都是memory
引用。
但是对于 EVM 来说,作为一个函数参数的memory
引用是什么含义呢?让我们用一个基本的 Solidity 例子。
function test(string memory input) public {
// ...
}
当一个memory
引用作为参数被传递给一个函数时,该函数的 EVM 字节码依次执行 4 个主要步骤:
- 从的
calldata
中加载字符串偏移到堆栈:用于字符串在calldata
中的起始位置。 - 将字符串的长度加载到堆栈中:将用于知道从
calldata
中复制多少数据。 - 分配一些内存空间,将字符串从
calldata
中移到memory
中:这与空闲内存指针
中描述的相同。 - 使用操作码
calldatacopy
将字符串从的calldata
转移到的memory
。
我已经把详细的操作代码放在下面。你也可以在我的 Github 代码库中了解更多细节:
来源:All About Solidity-Memory(Github repository)[24]
函数体内的内存引用
让我们来看看下面这个简单的例子:
代码语言:javascript复制function test() public { uint256[] memory data;}
要问的问题是变量 data
包含什么?
可能会有人回答“一个空的uint256
数字的数组 ”。但是不要被语法所迷惑或误导。这是 Solidity,不是 Javascript 或 Typescript!
在 Typescript 中,声明一个uint256[]
类型的变量而不对其进行初始化,将导致该变量首先容纳一个空数组。
然而,关键字memory
在这里改变了这一切!
让我们回顾一下,在介绍文章 "关于数据位置"中,我们描述了带有关键字 "storage"、"memory"或 "calldata"的变量被称为引用型变量。
因此,当你在 Solidity 函数中看到一个带有关键字memory
的变量时,你所处理的是对内存中某个位置的引用。
因此,上面的变量data
并不持有一个数组,而是持有内存中一个位置的指针。 Solidity 文档对此有很好的描述:
指向内存的局部变量,表示的是内存中变量的地址而不是值本身。
而 Solidity 的解释,更具体:
这样的变量也可以被赋值,但是注意赋值只会改变指针而不是数据。
让我们看看另一个例子来更好地理解:
代码语言:javascript复制function test() public pure returns (bytes memory) {
bytes memory data;
bytes memory greetings = hex"cafecafe";
data = greetings;
data[0] = 0x00;
data[1] = 0x00;
return greetings;
}
人们可能认为变量greetings
在这里是安全的,而且这个函数将返回0xcafecafe
。但这里的假设是错误的,如果你运行这个函数,它将返回以下结果:
内存引用所带来的惊喜和错误假设。
实际上,在底层发生的事情是,我们创建了两个指向内存的指针,由变量data
和greetings
命名。
当我们做data = greetings
时,我们认为我们是把cafecafe
这个值赋值给了变量data
。但是我们在这里根本没有分配任何东西! 我们向 EVM 发出以下指令:
变量
data
,我命令你指向内存中变量greetings
所指向的同一位置!
分配内存中的新元素
我们在上一节中看到,可以在内存中为变量分配一些空间,并通过给变量赋值直接写入内存中。
我们也可以在内存中分配一些空间,但不立即写入内存,同样使用new
关键字。
这主要是在函数内实例化复杂类型如数组时。
当用new
关键字创建数组时,必须在括号中指定数组的长度。在函数体内部的内存中只允许固定大小的数组。
uint[] memory data = new uint[](3 "] memory data = new uint[");
对于结构体,new 关键字是不需要的。
从一个存储参考变量中复制
让我们继续看下面这个 Solidity 例子。
代码语言:javascript复制// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract Playground {
bytes storageData = hex"C0C0A0C0DE";
function test() public {
bytes memory data = storageData;
}
}
在此案例中,我们正在复制一个storage
引用(即=
符号的右边),到一个内存
引用(即=
符号的左边)。这里发生了两件事:
- 新的内存被分配,变量
data
将指向内存中的一个新位置。 - 十六进制数值
0xC0C0A0C0DE
被从内存中加载,并复制到data
所指向的内存位置。
内存扩展成本
关于内存扩展成本的更多细节,请阅读 evm.codes[25]
Solidity 文档陈述如下:
当访问(无论是读还是写)一个先前未触及的内存字时,内存被扩展了一个字(256 位) 在扩展的时候,必须支付 Gas 的成本。内存越大,成本就越高(以二次方增长)。
事实上,每当我们在内存中写下一个新的字时,内存就会被说成是 扩展
,这个字以前没有被使用过(里面有一些数据)或被访问过(通过mload
)。
为什么内存扩展很重要?因为内存增长得越大,每次你与它互动时消耗的 Gas 就越多。
当你通过mstore
(或mstore8
)向内存写入时,这两个操作码会消耗一些 Gas。但是写到内存的 Gas 成本不仅取决于你写到内存的数据量。它还取决于实际的内存大小,在 EVM shadow 开发者社区中被称为内存扩展成本
。
除了写入内存的成本外,还有一个额外的成本与内存的扩展程度有关。
内存扩展成本以下列方式增加:
- 前 724 个字节是线性的。
- 此后呈二次方增加 (解释一下 "二次方 " 的含义)。
当通过mload
操作码访问内存中更高的偏移量时,内存扩展成本也会随着简单的内存读取操作而增加。
合约调用之间的内存
关于 EVM 内存和智能合约,有一个重要的概念需要注意。Solidity 文档很好地说明了这一点:
......合约在每次消息调用时都会获得一个新的清空的实例(内存)。
这有助于我们理解 EVM 内存的一个主要特征。在外部调用之间,获得一个清晰的内存实例。
事实上,EVM 内存的一个实例对于每个合约和当前的执行环境都是特定的。这意味着,在每一个新的合约交互中,都会获得一个新的清空的内存。
让我们在实践中检验一下,在每个新的外部调用中是如何获得一个清空的内存实例的。我们将使用这两个合约作为例子。
代码语言:javascript复制// SPDX-License-Identifier: MIT
pragma solidity ^ 0.8 .0;
contract Source {
Target target;
constructor(Target _target) {
target = _target;
}
function callTarget() public {
target.doSomething();
}
}
contract Target {
function doSomething() public {
// do whatever
}
}
使用这两个基本合约,我们可以使用Source
合约与Target
合约进行交互。让我们在 Remix 中部署和调试它们。
- 打开Remix IDE[26],创建一个新文件,复制上面的 Solidity 代码。
- 在不启用优化器及 runs 的情况下编译该文件,
- 先部署 "Target "合约。
- 其次部署 "Source"合约,将之前部署的 "Target"合约的地址作为构造函数参数。
- 在 "源 "合约上,运行函数 "callTarget()"。
- 在控制台,点击 "Debug" 来调试交易的每个操作码。
当你调试并通过每个操作码时,你应该看到 EVM 内存在不同的偏移量上充满了数据。特别是其中的一个偏移量 0x80
显示的数值 0x82692679000000000000...
。这是目标合约上的函数doSomething()
的函数选择器。
我们在这里可以看到,在外部合约调用之前,内存中已经充满了数据
我们可以在上面的截图中看到执行环境。调试器强调了代码第 12 行,即外部调用target.doSomething()
。
现在请注意下一个步骤! 如果你点击蓝色的箭头按钮,跳到下一个要调试的操作码,就像变魔术一样,内存被清空,变成了空的!
看一下内存切换,说明 "无数据可用"
正如你从上面的截图中所看到的,左侧边栏的 "Memory"字段现在显示 "无数据可用"。刚刚发生了什么?
CALL
操作码使 EVM 改变了执行环境。我们现在在一个新的执行环境中运行 EVM:目标
合约的环境。正如你在上面看到的,函数doSomething()
现在被高亮显示,也为这个新的执行环境切换提供了一个额外的线索。
下面是 Solidity 中这个外部调用的操作码的摘要。为了简洁起见,我省略了一些操作代码,并在注释中解释了发生的情况。
代码语言:javascript复制; ...
057 SLOAD ; load the value for `target` state variable from storage
; ...
; more stack manipulation
; ...
; ...
; ...
; ...
; ...
; ...
; ...
109 PUSH4 82692679 ; 1. load the function selector of doSomething()
114 PUSH1 40
116 MLOAD ; 2. load the free memory pointer
117 DUP2
118 PUSH4 ffffffff
123 AND
124 PUSH1 e0 ; 3.1 push 224 (0x0e) on the stack
126 SHL ; 3.2 shift the functin selector of doSomething() left by 224 bits, so to prepare the calldata to be sent to the Target contract
127 DUP2
128 MSTORE ; 4. store the calldata to be sent to the Target contract in memory, at memory location pointed to by the free memory pointer
; ...
; ...
; ...
; ...
; ...
; ...
; ...
; ...
; ...
; ...
; ...
; ...
145 EXTCODESIZE ; get the size of the code of the Target address, to ensure it is a contract
146 ISZERO ; if the codesize at Target address is zero, then the address is not a contract, so we will stop execution later
; ...
; ...
; ...
; ...
; ...
157 POP
158 GAS
159 CALL ; 5. make the external call to the Target contract, with the calldata to be sent to it (`doSomething()`)
作为一个简单的解释,EVM 将生成 calldata 字节,将doSomething()
的函数选择器(即0x82692679
)推到堆栈中,并向左移动以准备 calldata,所以在 calldata 中有这四个字节作为函数选择器。
然后,要发送的 calldata 有效载荷被存储在内存中,即位于由空闲内存指针检索到的位置。
最后,CALL
操作码将调用外部合约地址,最初从合约存储中获取(指令号为057
),并通过从内存中获取 calldata(之前被写入的地方)来发送。
你可以在All About Solidity 代码库[27]中查看这个外部调用的 EVM 操作码的完整片段。
结论
EVM 中的内存是一个需要学习的重要领域。它使 EVM 能够执行消息调用,如标准的call
,staticcall
和delegatecall
。从内存中存储和检索与消息调用一起发送的 calldata 和有效载荷。
因此,EVM 内存允许更好的可组合性,能够在智能合约中创建灵活的内部函数和子程序。此外,定义为 "memory"的参数使合约能够接收来自不同来源的调用和参数,包括来自 EOA 和外部合约调用(将有效载荷从 "calldata "加载到 "内存"),但也能够直接从内部函数中组合输入。
最后,在低级别的汇编中使用时,应该小心处理内存。这是为了确保你不会覆盖一些已经包含一些数据的保留内存空间。因此,尊照Solidity 内存管理[28]是你的责任。
Solidity 语言也提供关键字 memory-safe[29] 来更安全地使用内联汇编,并尊照 Solidity 内存模型。
请参阅 Solidity 文档中的 Conventions[30] 部分以了解更多细节。
本翻译由 Duet Protocol[31] 赞助支持。
原文链接: https://betterprogramming.pub/solidity-tutorial-all-about-memory-1e1696d71ee4
参考资料
[1]
登链翻译计划: https://github.com/lbc-team/Pioneer
[2]
翻译小组: https://learnblockchain.cn/people/412
[3]
Tiny 熊: https://learnblockchain.cn/people/15
[4]
Mech Mind: https://unsplash.com/@mechmind
[5]
Unsplash: https://unsplash.com/
[6]
深入Solidity数据存储位置: https://learnblockchain.cn/article/4864
[7]
Ethereum Name Service(ENS): https://docs.ens.domains/
[8]
深入Solidity数据存储位置: https://learnblockchain.cn/article/4864
[9]
内存布局: https://learnblockchain.cn/docs/solidity/internals/layout_in_memory.html
[10]
来源:instructions.go (geth client source code): https://github.com/ethereum/go-ethereum/blob/master/core/vm/instructions.go#L506
[11]
Ethreum Yellow Paper, page 34: https://ethereum.github.io/yellowpaper/paper.pdf
[12]
SHA1.sol: https://github.com/ensdomains/ens-contracts/blob/8a2423829a28852297ee208357d148987e8dce0f/contracts/dnssec-oracle/SHA1.sol
[13]
来源:Github上的ENS源代码:SHA1.sol: https://github.com/ensdomains/ens-contracts/blob/8a2423829a28852297ee208357d148987e8dce0f/contracts/dnssec-oracle/SHA1.sol
[14]
这条推文: https://twitter.com/721Orbit/status/1511961696692322305
[15]
来源:Github上的ENS源代码DNSRegistar.sol: https://github.com/ensdomains/ens-contracts/blob/3445b94a187cac1016ec6e3fb69b885227565d8e/contracts/dnsregistrar/DNSRegistrar.sol#L175-L178
[16]
来源:Github上的ENS源代码,SHA1.sol: https://github.com/ensdomains/ens-contracts/blob/8a2423829a28852297ee208357d148987e8dce0f/contracts/dnssec-oracle/SHA1.sol#L41-L56
[17]
evm.codes: https://www.evm.codes/
[18]
SemanticInformation.cpp: https://github.com/ethereum/solidity/blob/develop/libevmasm/SemanticInformation.cpp#L193
[19]
evm.codes: https://www.evm.codes/
[20]
内存扩展: https://www.evm.codes/about
[21]
来源:OpenZeppelin,解构智能合约(第一部分): https://blog.openzeppelin.com/deconstructing-a-solidity-contract-part-ii-creation-vs-runtime-6b9d60ecb44c/
[22]
Gonçalo Sá: https://medium.com/u/3e5dfef854b6?source=post_page-----1e1696d71ee4--------------------------------
[23]
来源:GBSPS/solidity-bytes-utils on Github, BytesLib.sol: https://github.com/GNSPS/solidity-bytes-utils/blob/6458fb2780a3092bc756e737f246be1de6d3d362/contracts/BytesLib.sol#L245-L247
[24]
来源:All About Solidity-Memory(Github repository): https://github.com/CJ42/All-About-Solidity/blob/master/articles/data-locations/Memory.md#a-string-passed-as-a-function-argument
[25]
evm.codes: https://www.evm.codes/about#memoryexpansion
[26]
Remix IDE: https://remix-project.org/
[27]
All About Solidity 代码库: https://github.com/CJ42/All-About-Solidity/blob/master/articles/data-locations/Memory.md#memory-between-function-calls
[28]
Solidity 内存管理: https://docs.soliditylang.org/en/v0.8.16/assembly.html#memory-management
[29]
memory-safe: https://docs.soliditylang.org/en/v0.8.16/assembly.html#memory-safety
[30]
Conventions: https://learnblockchain.cn/docs/solidity/assembly.html#conventions-in-solidity
[31]
Duet Protocol: https://duet.finance/?utm_souce=learnblockchain