深入Solidity数据存储位置 – 内存

深入了解EVM的内存

  • 原文链接: https://betterprogramming.pub/solidity-tutorial-all-about-memory-1e1696d71ee4
  • 译文出自:登链翻译计划
  • 译者:翻译小组 校对:Tiny 熊
  • 本文永久链接:learnblockchain.cn/article…

图片来源: Mech Mind on Unsplash

这是深入Solidity数据存储位置系列的另一篇。在今天的文章中,我们将学习EVM内存的布局,它的保留空间,空闲内存指针,如何使用memory引用来读写内存,以及使用内存时的常规最佳做法。

我们将使用 Ethereum Name Service (ENS)中的合约代码片段,用有意义的例子支持这篇文章。这将帮助我们更好地理解这个流行项目背后的智能合约是如何在底层工作的。

目录

  • 简介
  • EVM内存 - 概述
  • 内存的布局
  • 内存的基础知识
  • 从内存中读取("MLOAD")。
  • 写入内存(MSTORE+MSTORE8)。
  • 了解内存大小(MSIZE)。
  • 空闲内存指针
  • 作为函数参数的memory引用
  • 在函数内部"内存"(memory) 引用
  • 扩展内存成本
  • 合约调用之间的内存
  • 总结

介绍

在介绍性文章深入Solidity数据存储位置中,我把EVM描述为一个工业工厂。在工厂的某些地方,你会发现由操作员控制的机器和机器人。

这些机器将无法加工的大块钢铁/铝材分解成小块。

我们可以用同样的例子来说明以太坊。EVM作为一个堆栈机器,它在32字节的字上运行。当EVM遇到大于32字节的数据(复杂的类型,如stringbytesstruct或数组),它不能在堆栈中处理它们,因为这些项目太大。

因此,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个字(偏移量位置 0x000x20):用于哈希函数的临时空间

  • 偏移量位置 0x400x50,第3个字,空闲内存指针

  • 偏移量位置 0x60:零位插槽(永久为零),用作空动态内存数组的初始值。

空闲内存指针(偏移量位置0x40)是EVM内存中最关键的部分。必须小心处理,特别是在汇编/Yul中。我们将在一个单独的章节中介绍它。

更多信息请参见Solidity文档中的内存布局。

最大的内存限制

EVM内存是一个线性数组,可以通过字节索引(称为偏移量offset)来寻址。它最多可以包含多少个字节呢?

这个数组有多大?EVM的内存有多大?

这个问题的答案就在geth的源代码中(下面的截图)。看一下所使用的转换类型。

来源: instructions.go (geth client source code).

我们可以从geth客户端的这个截图中看到,mStart.Uint64()将内存偏移量转换成uint64值。意味着你能放在内存中的最大数据量是一个uint64数字的最大值。

如果指定的偏移量超过了这个值,它就会被回退。

内存的基本原理

只能在函数内部指定memory,而不能在合约层面的函数外部指定。

以下数据和值默认总是在内存中。

  • 复杂类型的函数参数。
  • 复杂类型的局部变量(在函数体内部)。
  • 从函数返回的值,无论其类型如何(都是通过return操作码完成的)。
  • 任何由函数返回的复合值类型必须指定关键字memory

通过复杂类型的变量/值,指的是诸如结构体、数组、bytesstrings等变量。

一旦函数调用结束,这些用关键字memory定义的变量将消失。这就是我们之前所说的 不持久化的意思

原因是,memory告诉Solidity在运行时为该变量创建一块空间,保证其大小和结构,以便在函数执行过程中将来用于该函数。

与内存交互 - 概述

Solidity文档指出,在EVM内存中。

...读被限制在256位的宽度,而写可以是8位或256位的宽度。

如果我们看一下黄皮书,我们可以看到一个操作码被定义为从内存读取(MLOAD),两个操作码被定义为写入内存:MSTOREMSTORE8

来源: Ethreum Yellow Paper, page 34

从内存中读取

你可以使用MLOAD操作码从内存中读取。

黄皮书公式

下面是黄皮书中关于MLOAD操作码规范的内容。

让我们来揭开这个非常正式的公式的神秘面纱!

黄皮书中的公式可以解释为如下:

  • Us[0]= 栈顶元素
  • Us'[0] = 被放在栈顶的结果项。
  • Um=内存中从特定偏移开始的内容。

公式Um[Us[0]...Us[0]+31]]可以用普通英语翻译如下:

  1. 取堆栈中最后一个顶层项目 Us[0]
  2. 用这个值作为读取内存的起始指针 Um (=偏移量)
  3. 从这个内存指针 Us[0]读出后面的31个字节(Us[0]+31)。

从内存中读出的数据一次只能读32个字节。这意味着你每次只能用mload操作码从内存中读取32个字节。

来源:https://twitter.com/721Orbit/status/1511961696692322305

这些操作码可以在Solidity内联汇编或独立的Yul代码中使用。

示例:ENS合约的SHA1库

让我们看一下ENS合约中的一个例子:SHA1.sol

在下面的代码片段中,mload操作码被使用了两次。

  • 首先检索空闲内存指针 scratch变量, 它被用作内存中的指针,数据的sha1哈希值将被计算和写入。
  • 第二,获取数据变量的长度(=字节数)。

来源:Github上的ENS源代码:SHA1.sol

写入内存

你可以使用以下两个操作码中的一个向内存写入:

  • MSTORE → 在内存中写一个字(=32字节);
  • MSTORE8 → 在内存中写一个单字节;

这条推文显示了geth客户端的EVM实例如何从堆栈中取出参数及作为MSTORE的输入。

在Solidity中

在Solidity中,每当你用memory关键字实例化一个变量并赋值(bytes/字符串,或者函数的返回值),底层的EVM就会执行mstore指令。

下面是ENS的DNSRegistar.sol合约中的一个例子:

来源:Github上的ENS源代码DNSRegistar.sol

在汇编中

mstore操作码可以在内联汇编中使用。它接受两个参数:

  • 要写入内存的偏移量。
  • 要写入内存中的数据。

请看mstore是如何在同一个ENS合约SHA1.sol.中的汇编中使用的:

来源:Github上的ENS源代码,SHA1.sol

了解内存大小

关于MSIZE操作码的更多细节,见evm.codes上的操作码解释。

初步猜测,EVM操作码MSIZE从它的名字上看,似乎它将返回存储在内存中的数据多少。或者换句话说,当前有多少字节写在内存中。

MSIZE操作码其实挺复杂。Solidity编译器的C++源代码提供了更多信息来理解它:

来源: SemanticInformation.cpp

MSIZE操作码返回在当前执行环境中访问内存的最高字节偏移。这个大小总是字的倍数(32字节)。

但是在Solidity中,"在内存中存储了多少字节 ""在内存中访问的最大索引/偏移量 "之间有什么区别?

我们将用Solidity本身的一个实际例子来说明! 请看下面的代码片断:

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字节分配在内存中。

  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对msize操作码更好的解释了,可以总结一下:

msize 跟踪当前执行中曾经访问过的最高偏移量。第一次写或读到更大的偏移量将触发内存扩展

空闲内存指针

在OpenZeppelin系列文章 "解构智能合约 "中, 揭示了每个智能合约的前5个字节背后的操作代码的含义。

0x6080604052...

来源:OpenZeppelin,解构智能合约(第一部分)

简而言之,这一连串的操作码将数字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字节 "

下面的操作码的作用很简单:

  1. 复制空闲内存指针的当前值 = 0x80
  2. 给它加上0x40(=64的小数,为64字节)
  3. 0x40(=空闲内存指针的位置)推到堆栈上
  4. 通过MSTORE用新的值更新空闲内存指针

在汇编中的内存指针

在内联汇编中,必须小心处理空闲内存指针!

它不仅要被手动获取,而且还要被手动更新!

因此,在汇编中处理内存时,你必须小心。你必须确保在汇编中总是先获取空闲内存,然后写入空闲内存指针指向的内存位置,如果你不想最终覆盖内存中已经有一些内容的话。

一旦在内存中写入,你必须确保用新的自由内存偏移量来更新空闲内存指针。

总之,当涉及到空闲内存指针时,一定要记住OpenZeppelin的建议。

在汇编级操作内存时,你必须非常小心。否则,你可能会覆盖一个保留的空间。

在检查空闲内存指针所指向的内存位置上实际存储的内容之前,向空闲内存指针写入可能不是一个好的做法。

示例

来自Gonçalo Sá 的solidity-byte-utils库,让我们来看看这个流行的Solidity库,用来操作bytes。如果你仔细观察每个函数的初始汇编代码,你会发现加载空闲内存指针是第一件事。

在函数的最后,tempBytes被返回。在低层上,这可以翻译为 :返回tempBytes所指向的内存偏移处的内存中存在的东西。

来源:GBSPS/solidity-bytes-utils on Github, BytesLib.sol

内存引用作为函数参数

在Solidity中,当我们必须将一个动态或复杂类型的参数传递给一个函数时,我们每次都会使用memory引用。

例如在ENS合约中,DNSRegistar.solclaim(...)函数需要两个参数:一个nameproof,都是memory引用。

但是对于EVM来说,作为一个函数参数的memory引用是什么含义呢?让我们用一个基本的Solidity例子。

function test(string memory input) public {
    // ...
}

当一个memory引用作为参数被传递给一个函数时,该函数的EVM字节码依次执行4个主要步骤:

  1. 从的calldata中加载字符串偏移到堆栈:用于字符串在calldata中的起始位置。
  2. 将字符串的长度加载到堆栈中:将用于知道从calldata中复制多少数据。
  3. 分配一些内存空间,将字符串从calldata中移到memory中:这与 空闲内存指针中描述的相同。
  4. 使用操作码 calldatacopy将字符串从calldata转移到的memory

我已经把详细的操作代码放在下面。你也可以在我的Github代码库中了解更多细节:

来源:All About Solidity - Memory (Github repository)

函数体内的内存引用

让我们来看看下面这个简单的例子:

function test() public {    uint256[] memory data;}

要问的问题是变量 data包含什么?

可能会有人回答“一个空的uint256数字的数组 ”。但是不要被语法所迷惑或误导。这是Solidity,不是Javascript或Typescript!

在Typescript中,声明一个uint256[]类型的变量而不对其进行初始化,将导致该变量首先容纳一个空数组。

然而,关键字memory在这里改变了这一切!

让我们回顾一下,在介绍文章 "关于数据位置"中,我们描述了带有关键字 "storage"、"memory"或 "calldata"的变量被称为引用型变量

因此,当你在Solidity函数中看到一个带有关键字memory的变量时,你所处理的是对内存中某个位置的引用。

因此,上面的变量data并不持有一个数组,而是持有内存中一个位置的指针。 Solidity文档对此有很好的描述:

指向内存的局部变量,表示的是内存中变量的地址而不是值本身。

而Solidity的解释,更具体:

这样的变量也可以被赋值,但是注意赋值只会改变指针而不是数据。

让我们看看另一个例子来更好地理解:

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。但这里的假设是错误的,如果你运行这个函数,它将返回以下结果:

内存引用所带来的惊喜和错误假设。

实际上,在底层发生的事情是,我们创建了两个指向内存的指针,由变量datagreetings命名。

当我们做data = greetings时,我们认为我们是把cafecafe这个值赋值给了变量data。但是我们在这里根本没有分配任何东西! 我们向EVM发出以下指令:

变量data,我命令你指向内存中变量greetings所指向的同一位置!

分配内存中的新元素

我们在上一节中看到,可以在内存中为变量分配一些空间,并通过给变量赋值直接写入内存中。

我们也可以在内存中分配一些空间,但不立即写入内存,同样使用new关键字。

这主要是在函数内实例化复杂类型如数组时。

当用new关键字创建数组时,必须在括号中指定数组的长度。在函数体内部的内存中只允许固定大小的数组。

uint[] memory data = new uint[](3);

对于结构体,new关键字是不需要的。

从一个存储参考变量中复制

让我们继续看下面这个Solidity例子。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract Playground {
    bytes storageData = hex"C0C0A0C0DE";

    function test() public {
        bytes memory data = storageData;
    }
}

在此案例中,我们正在复制一个storage引用(即=符号的右边),到一个内存引用(即=符号的左边)。这里发生了两件事:

  1. 新的内存被分配,变量data将指向内存中的一个新位置。
  2. 十六进制数值0xC0C0A0C0DE被从内存中加载,并复制到data所指向的内存位置。

内存扩展成本

关于内存扩展成本的更多细节,请阅读 evm.codes

Solidity 文档陈述如下:

当访问(无论是读还是写)一个先前未触及的内存字时,内存被扩展了一个字(256位)

在扩展的时候,必须支付Gas的成本。内存越大,成本就越高(以二次方增长)。

事实上,每当我们在内存中写下一个新的字时,内存就会被说成是 扩展,这个字以前没有被使用过(里面有一些数据)或被访问过(通过mload)。

为什么内存扩展很重要?因为内存增长得越大,每次你与它互动时消耗的Gas就越多。

当你通过mstore(或mstore8)向内存写入时,这两个操作码会消耗一些Gas。但是写到内存的Gas成本不仅取决于你写到内存的数据量。它还取决于实际的内存大小,在EVM shadow开发者社区中被称为内存扩展成本

除了写入内存的成本外,还有一个额外的成本与内存的扩展程度有关。

内存扩展成本以下列方式增加:

  • 前724个字节是线性的。
  • 此后呈二次方增加(解释一下 "二次方 "的含义)。

当通过mload操作码访问内存中更高的偏移量时,内存扩展成本也会随着简单的内存读取操作而增加。

合约调用之间的内存

关于EVM内存和智能合约,有一个重要的概念需要注意。Solidity 文档很好地说明了这一点:

......合约在每次消息调用时都会获得一个新的清空的实例(内存)。

这有助于我们理解EVM内存的一个主要特征。在外部调用之间,获得一个清晰的内存实例。

事实上,EVM内存的一个实例对于每个合约和当前的执行环境都是特定的。这意味着,在每一个新的合约交互中,都会获得一个新的清空的内存。

让我们在实践中检验一下,在每个新的外部调用中是如何获得一个清空的内存实例的。我们将使用这两个合约作为例子。

// 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中部署和调试它们。

  1. 打开Remix IDE,创建一个新文件,复制上面的Solidity代码。
  2. 在不启用优化器及runs 的情况下编译该文件,
  3. 先部署 "Target "合约。
  4. 其次部署 "Source"合约,将之前部署的 "Target"合约的地址作为构造函数参数。
  5. 在 "源 "合约上,运行函数 "callTarget()"。
  6. 在控制台,点击"Debug"来调试交易的每个操作码。

当你调试并通过每个操作码时,你应该看到EVM内存在不同的偏移量上充满了数据。特别是其中的一个偏移量 0x80显示的数值 0x82692679000000000000...。这是目标合约上的函数doSomething()的函数选择器。

我们在这里可以看到,在外部合约调用之前,内存中已经充满了数据

我们可以在上面的截图中看到执行环境。调试器强调了代码第12行,即外部调用target.doSomething()

现在请注意下一个步骤! 如果你点击蓝色的箭头按钮,跳到下一个要调试的操作码,就像变魔术一样,内存被清空,变成了空的!

看一下内存切换,说明 "无数据可用"

正如你从上面的截图中所看到的,左侧边栏的 "Memory"字段现在显示 "无数据可用"。刚刚发生了什么?

CALL操作码使EVM改变了执行环境。我们现在在一个新的执行环境中运行EVM:目标合约的环境。正如你在上面看到的,函数doSomething()现在被高亮显示,也为这个新的执行环境切换提供了一个额外的线索。

下面是Solidity中这个外部调用的操作码的摘要。为了简洁起见,我省略了一些操作代码,并在注释中解释了发生的情况。

; ...
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" 代码库中查看这个外部调用的EVM操作码的完整片段。

结论

EVM中的内存是一个需要学习的重要领域。它使EVM能够执行消息调用,如标准的callstaticcalldelegatecall。从内存中存储和检索与消息调用一起发送的calldata和有效载荷。

因此,EVM内存允许更好的可组合性,能够在智能合约中创建灵活的内部函数和子程序。此外,定义为 "memory"的参数使合约能够接收来自不同来源的调用和参数,包括来自EOA和外部合约调用(将有效载荷从 "calldata "加载到 "内存"),但也能够直接从内部函数中组合输入。

最后,在低级别的汇编中使用时,应该小心处理内存。这是为了确保你不会覆盖一些已经包含一些数据的保留内存空间。因此,尊照Solidity内存管理是你的责任。

Solidity 语言也提供关键字 "memory-safe" 来更安全地使用内联汇编,并尊照 Solidity 内存模型。

请参阅 Solidity 文档中的 Conventions 部分以了解更多细节。


本翻译由 Duet Protocol 赞助支持。

本文参与区块链技术网 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2022-10-17 18:23
  • 阅读 ( 573 )
  • 学分 ( 57 )
  • 分类:Solidity

评论