深入solidity内部 -以太坊EVN插槽存储关系

Posted Zeke Luo

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入solidity内部 -以太坊EVN插槽存储关系相关的知识,希望对你有一定的参考价值。

以太坊虚拟机Ethereum Virtual Machine(EVM) 拥有三种存储区域。

  • 存储storage ( 贮存了合约声明中所有的变量)

贮存了合约声明中所有的变量。 虚拟机会为每份合约分别划出一片独立的 存储storage 区域,并在函数相互调用时持久存在,所以其使用开销非常大。

每个账户有一块持久化内存区称为 存储 。 存储是将256位字映射到256位字的键值存储区。 在合约中枚举存储是不可能的,且读存储的相对开销很高,修改存储的开销甚至更高。合约只能读写存储区内属于自己的部分。

  • 内存memory ( 用于暂存数据)

用于暂存数据。其中存储的内容会在函数被调用(包括外部函数)时擦除,所以其使用开销相对较小。

合约会试图为每一次消息调用获取一块被重新擦拭干净的内存实例。 内存是线性的,可按字节级寻址,但读的长度被限制为256位,而写的长度可以是8位或256位。当访问(无论是读还是写)之前从未访问过的内存字(word)时(无论是偏移到该字内的任何位置),内存将按字进行扩展(每个字是256位)。扩容也将消耗一定的gas。 随着内存使用量的增长,其费用也会增高(以平方级别)

  • (用于存放小型的局部变量)

用于存放小型的局部变量。使用几乎是免费的,但容量有限。

对绝大部分数据类型来说,由于每次被使用时都会被复制,所以你无法指定将其存储在哪里。

在数据类型中,对所谓存储地点比较重视的是结构和数组。 如果你在函数调用中传递了这类变量,假设它们的数据可以被贮存在 存储storage 或 内存memory 中,那么它们将不会被复制。也就是说,当你在被调用函数中修改了它们的内容,这些修改对调用者也是可见的

EVM 不是基于寄存器的,而是基于栈的,因此所有的计算都在一个被称为 栈(stack) 的区域执行。 栈最大有1024个元素,每个元素长度是一个字(256位)。对栈的访问只限于其顶端,限制方式为:允许拷贝最顶端的16个元素中的一个到栈顶,或者是交换栈顶元素和下面16个元素中的一个。所有其他操作都只能取最顶的两个(或一个,或更多,取决于具体的操作)元素,运算后,把结果压入栈顶。当然可以把栈上的元素放到存储或内存中。但是无法只访问栈上指定深度的那个元素,除非先从栈顶移除其他元素

不同数据类型的变量会有各自默认的存储地点:

  • 状态变量总是会存在

    存储storage

  • 函数参数默认存放在

    内存memory

  • 结构、数组或映射类型的局部变量,默认会放在

    存储storage

  • 除结构、数组及映射类型之外的局部变量,会储存在栈中

    因为结构和数组属于引用类型,  映射mapping和动态数组不可预知大小,不能在状态变量之间存储他们

    引用类型

    引用类型可以通过多个不同的名称修改它的值,而值类型的变量,每次都有独立的副本。因此,必须比值类型更谨慎地处理引用类型。 目前,引用类型包括结构,数组和映射,如果使用引用类型,则必须明确指明数据存储哪种类型的位置(空间)里:

    • 内存memory 即数据在内存中,因此数据仅在其生命周期内(函数调用期间)有效。不能用于外部调用。
    • 存储storage 状态变量保存的位置,只要合约存在就一直存储.
    • 调用数据calldata 用来保存函数参数的特殊数据位置,是一个只读位置。

    状态变量在储存(storage)中的布局

    合约的状态变量以一种紧凑方式存储到区块存储中, 除了动态大小的数组和 映射mapping (见下文),数据的存储方式是从位置 0 开始连续放置在 存储storage 中。 对于每个变量,根据其类型确定字节大小。

    存储大小少于 32 字节的多个变量会被打包到一个 存储插槽storage slot 中,规则如下:

    • 存储插槽storage slot 的第一项会以低位对齐的方式储存。
    • 值类型仅使用存储它们所需的字节。
    • 如果  中的剩余空间不足以储存一个值类型,那么它会被存入下一个存储插槽storage slot
    • 结构体(struct)和数组数据总是会开启一个新插槽(但结构体或数组中的各元素,则按规则紧密打包)。
    • 结构体和数组之后的数据也或开启一个新插槽。

    对于使用继承的合约,状态变量的排序由C3线性化合约顺序( 顺序从最基类合约开始)确定。如果上述规则成立,那么来自不同的合约的状态变量会共享一个 存储插槽storage slot 。

    结构体和数组中的成员变量会存储在一起,就像它们单独声明时一样

     在使用小于 32 字节的元素(变量)时,合约的 gas 使用量可能会高于使用 32 字节的元素。这是因为 以太坊虚拟机Ethereum Virtual Machine(EVM) 每次操作 32 个字节, 所以如果元素比 32 字节小,以太坊虚拟机Ethereum Virtual Machine(EVM) 必须执行额外的操作以便将其大小缩减到到所需的大小。

    当我们在处理状态变量时,利用编译器会将多个元素缩减的存储大小打包到一个 存储插槽storage slot 中,也许是有益,因为可以合并多次读写为单个操作。

    如果你不是在同一时间读或写一个槽中的所有值,这可能会适得其反。 当一个值被写入一个多值存储槽时,必须先读取该存储槽,然后将其与新值合并,避免破坏同一槽中的其他数据,再写入。

    当处理函数参数或 内存memory 中的值时,因为编译器不会打包这些值,所以没有什么额外的益处。

    最后,为了允许 以太坊虚拟机Ethereum Virtual Machine(EVM) 对此进行优化,请确保 存储storage 中的变量和 struct 成员的书写顺序允许它们被紧密地打包。

    例如,应该按照 uint128,uint128,uint256 的顺序来声明状态变量,而不是使用 uint128,uint256,uint128 , 因为前者只占用两个 存储插槽storage slot,而后者将占用三个。

    映射和动态数组

    由于 映射mapping 和动态数组不可预知大小,不能在状态变量之间存储他们。相反,他们自身根据 以上规则 仅占用 32 个字节,然后他们包含的元素的存储的其实位置,则是通过 Keccak-256 哈希计算来确定。

    假设 映射mapping 或动态数组根据上述存储规则最终可确定某个位置 p 。 对于动态数组,此插槽中会存储数组中元素的数量(字节数组和字符串除外,见下文)。 对于 映射mapping ,该插槽未被使用(为空),但它仍是需要的,以确保两个彼此挨着 映射mapping ,他们的内容在不同的位置上。

    数组的元素会从 keccak256(p) 开始; 它的布局方式与静态大小的数组相同。一个元素接着一个元素,如果元素的长度不超过16字节,就有可能共享存储槽。

    动态数组的数组会递归地应用这一规则,例如,如何确定 x[i][j] 元素的位置,其中 x 的类型是 uint24[][],计算方法如下(假设 x`本身存储在槽`p): 槽位于 keccak256(keccak256(p)+i)+floor(j/floor(256/24)) 且可以从槽数据 v``得到元素内容,使用``(v>>((j%floor(256/24))*24))&type(uint24).max.

    映射mapping 中的键 k 所对应的槽会位于 keccak256(h(k). p) ,其中 . 是连接符, h 是一个函数,根据键的类型:

    • 值类型, h 与在内存中存储值的方式相同的方式将值填充为32字节。
    • 对于字符串和字节数组, h(k) 只是未填充的数据。

    如果映射值是一个非值类型,计算槽位置标志着数据的开始位置。例如,如果值是结构类型,你必须添加一个与结构成员相对应的偏移量才能到达该成员。

    例如,考虑下面的合约:

    *// SPDX-License-Identifier: GPL-3.0***pragma solidity** >=**0.4.0** <**0.9.0**;
    
    **contract** **C** 
        struct S  uint16 a; uint16 b; uint256 c; 
        uint x;
        mapping(uint => mapping(uint => S)) data;
    
    

    让我们计算一下 data[4][9].c 的存储位置。映射本身的位置是 1``( 前面有32字节变量 ``x )。 因此 data[4] 存储在 keccak256(uint256(4) . uint256(1))。 data[4] 的类型又是一个映射, data[4][9] 的数据开始于槽位 keccak256(uint256(9). keccak256(uint256(4). uint256(1))

    在结构 S 的成员 c 中的槽位偏移是 1,因为 a 和 b``被装在一个槽位中。 最后 ``data[4][9].c 的插槽位置是 keccak256(uint256(9) . keccak256(uint256(4) . uint256(1)) + 1. 该值的类型是 uint256,所以它使用一个槽

    变量在内存布局

    Solidity保留了四个32字节的插槽,字节范围(包括端点)特定用途如下:

    • 0x00 - 0x3f (64 字节): 用于哈希方法的暂存空间(临时空间)
    • 0x40 - 0x5f (32 字节): 当前分配的内存大小(也作为空闲内存指针)
    • 0x60 - 0x7f (32 字节): 零位插槽

    暂存空间可以在语句之间使用 (例如在内联汇编中)。 零位插槽用作动态内存数组的初始值,并且永远不应写入(空闲内存指针最初指向 0x80).

    Solidity 总是将新对象放在空闲内存指针上,并且内存永远不会被释放(将来可能会改变)。

    Solidity 中的内存数组中的元素始终占据32字节的倍数(对于 bytes1[] 总是这样,但不适用与 bytes 和 string )。

    多维内存数组是指向内存数组的指针,动态数组的长度存储在数组的第一个插槽中,然后是数组元素。

    警告:

    💡 Solidity中有一些需要临时存储区的操作需要大于64个字节, 因此无法放入暂存空间。 它们将被放置在空闲内存指向的位置,但是由于使用寿命短,指针不会更新。 内存可以归零,也可以不归零。 因此,不应指望空闲内存指针指向归零内存区域。

    尽管使用 msize 到达绝对归零的内存区域似乎是一个好主意,但使用此类非临时指针而不更新空闲内存指针可能会产生意外结果

    与存储中布局的不同

    如上所述,在内存中的布局与在 存储中有一些不同。下面是一些例子:

    • 数组的不同

    下面的数组在存储中占用32字节(1个槽),但在内存中占用128字节(4个元素,每个32字节)。

    uint8[4] a;
    
    • 结构体的不同

    下面的结构体在存储中占用 96 (1个槽,每个32字节) ,但在内存中占用 128 个字节(4 个元素每个 32 字节)。

    struct S 
        uint a;
        uint b;
        uint8 c;
        uint8 d;
    
    

    Call Data布局

    假定函数调用的输入数据采用 ABI规范。 其中,ABI规范要求将参数填充为32的倍数 个字节。 内部函数调用使用不同的约定。

    假定函数调用的输入参数数据采用 ABI规范 定义的格式。 其中,ABI规范要求将参数填充为32字节的倍数。 内部函数调用则使用不同的约定。

    合约构造函数的参数直接附加在合约代码的末尾,也采用ABI编码。 构造函数将通过硬编码偏移量而不是通过使用 codesize 操作码来访问它们,因为在将数据追加到代码时,它就会会改变

    数据位置与赋值行为

    数据位置不仅仅表示数据如何保存,它同样影响着赋值行为:

    • 在  和  之间两两赋值(或者从  赋值 ),都会创建一份独立的拷贝。

      存储storage

      内存memory

      调用数据calldata

    • 从  到  的赋值只创建引用, 这意味着更改内存变量,其他引用相同数据的所有其他内存变量的值也会跟着改变。

      内存memory

      内存memory

    • 从  到本地存储变量的赋值也只分配一个引用。

      存储storage

    • 其他的向  的赋值,总是进行拷贝。 这种情况的示例如对状态变量或  的结构体类型的局部变量成员的赋值,即使局部变量本身是一个引用,也会进行一份拷贝

      存储storage

    // SPDX-License-Identifier: GPL-3.0
    pragma solidity >=0.5.0 <0.9.0;
    
    contract Tiny 
        uint[] x; // x 的数据存储位置是 storage, 位置可以忽略
    
        // memoryArray 的数据存储位置是 memory
        function f(uint[] memory memoryArray) public 
            x = memoryArray; // 将整个数组拷贝到 storage 中,可行
            uint[] storage y = x;  // 分配一个指针(其中 y 的数据存储位置是 storage),可行
            y[7]; // 返回第 8 个元素,可行
            y.pop(); // 通过 y 修改 x,可行
            delete x; // 清除数组,同时修改 y,可行
    
            // 下面的就不可行了;需要在 storage 中创建新的未命名的临时数组,
            // 但 storage 是“静态”分配的:
            // y = memoryArray;
            // 下面这一行也不可行,因为这会“重置”指针,
            // 但并没有可以让它指向的合适的存储位置。
            // delete y;
    
            g(x); // 调用 g 函数,同时移交对 x 的引用
            h(x); // 调用 h 函数,同时在 memory 中创建一个独立的临时拷贝
        
    
        function g(uint[] storage ) internal pure 
        function h(uint[] memory) public pure 
    
    

                                                                                                                    


Solidity 中的内部/外部和公共/私人功能有啥区别?

【中文标题】Solidity 中的内部/外部和公共/私人功能有啥区别?【英文标题】:What is the difference between an internal/external and public/private function in solidity?Solidity 中的内部/外部和公共/私人功能有什么区别? 【发布时间】:2018-05-17 06:19:19 【问题描述】:

目前正在阅读 Solidity 文档: https://solidity.readthedocs.io/en/develop/types.html#function-types

默认情况下,函数类型是内部的,所以 internal 关键字可以 被省略。相反,合约函数本身是公开的 默认,仅当用作类型的名称时,默认为 内部。

这对我来说没有意义。一个函数如何同时是内部的和公共的?

我知道internal 表示函数只能在合约内调用,external 可以在合约外调用。所以对我来说,内部是私有的,外部是公开的,但文档听起来像是它可以同时是公开的和内部的?

那么internal/externalpublic/private 在功能方面有什么区别(如果有的话)?

【问题讨论】:

您的问题可能更适合另一个 StackExchange 站点。有关区块链、比特币和其他加密货币的问题,请转至Bitcoin StackExchange。对于以太坊特定的问题,请转至Ethereum StackExchange。 :) 【参考方案1】:

• 外部:这些功能可从其他合约和交易中访问。除非使用 this 关键字,否则它们不能在内部调用。在外部呼叫期间您需要小心,因为它们可以 涉及遇到安全风险、损失gas和抛出错误。

• 公共:默认情况下,函数是公共的。它们可以在内部调用,也可以通过消息调用。

• 内部:可以在智能合约和派生的智能合约中访问。对于状态变量,只有两种可见性类型:publicinternal。默认为internal

• 私有:私有函数仅对声明它们的同一合约可见。它更严格。除非需要,否则请使用最低限度的可见性来保护您的代码免受外部攻击。私有可见性只会阻止其他 从访问或修改驻留在驻留在信息的合同 合同,但它仍然在现场公众中为全世界可见 区块链。

【讨论】:

【参考方案2】:

以下是四个关键字的区别:

private 表示只能从合约内的其他函数调用

internal 类似于私有,但也可以被从当前继承的合约调用

external只能在合约外调用

public 可以在任何地方调用,无论是内部还是外部。

【讨论】:

【参考方案3】:

在 Solidity 的术语中,internal/external 也用作描述“两种函数调用”,而不仅仅是作为访问修饰符。

Take a look at the documentation section about 'Visibility and Getters' inside the contracts.

由于 Solidity 知道两种函数调用(不创建实际 EVM 调用(也称为“消息调用”)的内部函数调用和创建实际 EVM 调用的外部函数调用),因此函数和状态变量有四种类型的可见性。

【讨论】:

以上是关于深入solidity内部 -以太坊EVN插槽存储关系的主要内容,如果未能解决你的问题,请参考以下文章

智能合约从入门到精通:Solidity的特性与内部机制

智能合约从入门到精通:Solidity的特性与内部机制

内部数据库和 Solidity 之间的数据重复

智能合约从入门到精通:Solidity汇编语言

智能合约实战 solidity 语法学习 03 [ 函数修饰符 view public private constant payable ]

Solidity内嵌汇编学习