前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过调试理解EVM #3 :存储布局如何工作?

通过调试理解EVM #3 :存储布局如何工作?

作者头像
Tiny熊
发布2023-01-09 17:18:36
4550
发布2023-01-09 17:18:36
举报

译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]

本文是关于通过调试理解 EVM 第 3 篇,本系列包含 7 篇文章:

  • 第 1 篇:理解汇编[4]
  • 第 2 篇:部署智能合约[5]
  • 第 3 篇:存储布局是如何工作的?[6]
  • 第 4 篇:结束/中止执行的 5 个指令[7]
  • 第 5 篇:执行流 if/else/for/函数[8]
  • 第 6 篇:完整的智能合约布局[9]
  • 第 7 篇:外部调用和合约部署[10]

笨笨我们将看看不同类型的变量是如何在 EVM 内存和存储中存储和处理的。

每次,当我们在分析一段代码时,我建议你同时用remix来调试它。你会对正在发生的事情有一个更好的理解。如果你不知道怎么做,请查看本系列的第 1 篇:理解汇编[11]

1. 简单的例子

我们将首先使用一个非常简单的例子。

不要忘记编译下面的合约,我们的设置是:solidity 0.8.7版本编译器、启用优化器,run 为 200 。

部署它可以并调用函数 "modify()":

代码语言:javascript
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Test {
    uint balance;
    uint balance2;
    uint balance3;

    function modify() external {
        balance = 1;
        balance2 = 2;
        balance3 = 3;
    }
}

当我们用 remix 调试 "modify" 函数时,会被 remix 直接 "路由"到函数modify() ,因此在modify() 之前执行的代码(如函数选择器或 payable 验证)已经完成,对我们的分析没有帮助:

代码语言:javascript
复制
045 JUMPDEST |0x64cf33b8| (this is the function signature, we will discard it)
046 PUSH1 42 |0x42|
048 PUSH1 01 |0x01|0x42|
050 PUSH1 00 |0x00|0x01|0x42|
052 DUP2     |0x01|0x00|0x01|0x42|
053 SWAP1    |0x00|0x01|0x01|0x42|
054 SSTORE   |0x01|0x42|
055 PUSH1 02 |0x02|0x01|0x42|
057 SWAP1    |0x01|0x02|0x42|
058 DUP2     |0x02|0x01|0x02|0x42|
059 SWAP1    |0x01|0x02|0x02|0x42|
060 SSTORE   |0x02|0x42|
061 PUSH1 03 |0x03|0x02|0x42|
063 SWAP1    |0x02|0x03|0x42|
064 SSTORE   |0x42|
065 JUMP     ||
066 JUMPDEST ||
067 STOP     ||

在我们调用函数 modify 后,结果是相当明显的。

在第 48 指令,EVM 在堆栈中PUSH 42(十进制的66),这就是合约末尾代码中的 "位置"。(066 jumpdest 067 stop)当 modify()的执行将结束时,EVM 将 JUMP 到这个字节。

在指令 48 和 54 之间,EVM 在存储槽 0保存 1 在指令 55 和 60 之间,EVM 在存储槽 1 保存2。在指令 61 和 64 之间,EVM 在存储槽 2 保存3。

在第 65 个指令,函数JUMP到 66(0x42),在函数 modify()的开头保存的字节,并通过使用STOP指令结束智能合约的执行。

你可以通过运行调试器和检查堆栈中的汇编来验证。这段代码相当于:

代码语言:javascript
复制
sstore(0x0,0x1)
sstore(0x1,0x2)
sstore(0x2,0x3)

这里,即使我们的值比 32 字节少很多,它们也被存储在单独的槽里,这可能会花费一些 Gas。(如果以前的值是 0,则每个槽要花费 20000 个 Gas)

> 修改函数的 Gas 成本

但是,如果你第二次调用该函数,由于存储中的值是非零的,Gas 成本会便宜很多。(每 SSTORE 使用 2200gas)

提示:每条在 EVM 上指令都要花费 Gas,一个交易的 Gas 成本是所有指令的 Gas 总和(+21000Gas 的基本成本),你可以在调试器标签中的 "步骤详情(step details)"部分看到 Gas 的使用。

这里,SWAP1 指令使用了 3 个 Gas

如果你不理解这第一部分,请随时阅读本系列的第一篇或第二篇文章,在那里更详细地解释汇编代码:https://learnblockchain.cn/article/4913

2. 使用 uint8 而不是 uint256

到现在为止,我们还没学到什么,但是如果我们把uint代替为uint8 呢?有什么区别吗?让我们看看结果吧!

代码语言:javascript
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;  contract Test {
    uint8 balance;
    uint8 balance2;
    uint8 balance3;

    function modify() external {
         balance = 1;
         balance2 = 2;
         balance3 = 3;
    }
    function modify2() external {
         balance2 = 5;
    }
}

你可能已经知道,uint8 只使用1 个字节。所以 3 个 uint8 应该只用3 个字节,这比一个槽要少得多。(32 字节)

因此,这三个变量加起来应该只使用一个槽,对吗?

是的,你是对的!只有一个存储被执行,而且代码要短得多。

代码语言:javascript
复制
045 JUMPDEST     |function signature (discarded)|
046 PUSH1 00     |0x00|
048 DUP1         |0x00|0x00|
049 SLOAD        |0x00|0x00| (the slot 0 in storage contains 0x030201)
050 PUSH3 ffffff |0xffffff|0x00|0x00|
054 NOT          |0xffffff...fffff000000|0x00|0x00| (the NOT inverse all 32 bytes of Stack(0)
055 AND          |0x00|0x00|
056 PUSH3 030201 |0x030201|0x00|0x00|
060 OR           |0x030201|0x00|
061 SWAP1        |0x00|0x030201|
062 SSTORE       ||
063 STOP         ||

让我们看看在这个函数中到底发生了什么。像往常一样,不要忘记在阅读的同时使用调试器,你会对情况有更好的理解。

在第 49 指令,SLOAD在 Stack(0)槽中加载 Storage 值,由于 Stack(0)=0 (备注:存储槽没有写入过,默认都是 0),所以堆栈没有变化。

接下来的 3 个操作(指令 50-55)有点神秘。

EVM 推送 "ffffff" 和 NOT(取反)这个,结果是 0xfffffffffffffffffffffffffffffffffffffffffffffffff000000, NOT 指令反转了**Stack(0)**的所有字节。

在这之后,它与之前的SLOAD 进行 AND 运算(与运算),也就是 0x00。

我们知道,0 AND x = 0(对每一个 x),结果是 0x00,堆栈保持与指令 50 之前一样。

在这 6 条指令之后,没有任何变化,这非常奇怪......我们将在后面几行看到原因。

就在这之后的第 56 字节:0x030201 被推到了堆栈中,这显然是我们的balance=1balance2=2balance3=3的值。

在第 60 指令,因为Stack(1) 是 0,OR(或)操作码在这里没有任何作用,因为 0 OR x = x (对于所有的 x),Stack 保持不变,只有 Stack(1)的 0x00 被删除。

之后,SSTORE用来将030201存储在0槽中。这就是我们所要做的。

你可以注意到,03 02 01 在存储空间和堆栈中都占用了 1 个槽,就像我们所期望的那样。因此,我们可以证明,这 3 个变量占用了相同的存储槽,因此使用的 Gas 更少了

只有 43286 个 Gas 被使用,而之前是 87504。

第二个智能合约函数 modify(),只使用了 43286 个 Gas 而不是 87504 个。如果你是一个智能合约的开发者,你已经证明了,使用更少的变量(当它是可能的)可以节省大量的 Gas......

现在让我们调用函数modify2(在modify() 之后 ),这里是整个函数的反汇编。

提示一下:modify2 只把 balance2(插槽 1)设置为 5。

代码语言:javascript
复制
075 PUSH1 00   |0x00|
077 DUP1       |0x00|0x00|
078 SLOAD      |0x030201|0x00| (Slot 0 = balance which contains 0x030201 as set previously)
079 PUSH2 ff00 |0xff00|0x030201|0x00|
082 NOT        |0xfff...fffff00ff|0x030201|0x00|
083 AND        |0x000...000030001|0x01|0x00|
084 PUSH2 0500 |0x0500|0x000...000030001|0x01|0x00|
087 OR         |0x000...000030501|0x01|0x00|
088 SWAP1      |0x01|0x000...000030501|0x00|
089 SSTORE     |0x00|
090 STOP       ||
  1. 首先(指令 78),EVM 加载存储的槽 0,即0x030201
  2. 其次(指令 79-82),EVM 对 ff00 取反(NOT),在 32 个字节的结果是 0xfffffffffffffffffffffffffffffffffffffffffffffffffff00ff
  3. 在指令 83,2 个结果进行与运算,即0x00000000000000000000000000000000000000000000000000030001(或 0x030001)。这和存储槽 0 是一样的,但是没有 02(合约中的 "balance2"的部分),这是正常的!为什么?这是因为在modify2()中,EVM 修改了balance2。首先它需要擦除之前的结果,而不擦除balancebalance3(因为它们在同一个槽中),所以它通过使用0xfff...ff00ff掩码来 "清洗" 结果。
  4. 之后,0500被推入堆栈(指令 84),最后的 2 个结果进行 OR 操作(指令 85),最后的结果是:0x030501,"OR"的目的是在 03 和 01 的边上加上 05。因此余额 2 被成功地修改,而没有改变余额和余额 3。

这个0xfffffffffffffffffffffffffffffffffffffffffffffffffff00ff被称为 "掩码"。

如果我们想代替balance2修改balance3为 5,我们应该使用掩码0xfffffffffffffffffffffffffffffffffffffffffffffffff00ffff(擦除 balance3 在 0x00 槽中的字节),在第四步,我们应该PUSH 050000.(05 应该在这里,因为这里放置了balance3的存储。)

这就是为什么你在需要的时候应该使用较小的类型:它需要更少的 Gas。

但是,不要滥用较小的类型,因为它增加了 EVM 执行的操作的数量(通过使用带有掩码的操作),所以它使用更多的 Gas。

3. 使用不同类型的数据

让我们看看节省 Gas 的技巧是否只适用于uint类型或其他 solidity 内置类型。

下面是新的智能合约,用相同的设置(编译器 0.8.7 和优化器为 200)编译它:

代码语言:javascript
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Test {
    uint8 balance;
    bytes4 data;
    address addr;
    function modify() external {
        balance = 17;
        data = 0xaaaaaaaa;
        addr = 0x358AA13c52544ECCEF6B0ADD0f801012ADAD5eE3;
    }
}

这个智能合约包含一个uint8,占1 个字节,一个byte4,占4 个字节,一个地址,占20 个字节。我们创建的 modify函数总共 "修改"了25 个字节,这比一个 EVM 插槽(32 个字节)还要少。

理论上,这 3 个变量应该用一个存储槽就可以。

是这样吗?

代码语言:javascript
复制
045 JUMPDEST |function signature discarded|
046 PUSH1 00 |0x00|
048 DUP1     |0x00|0x00|
049 SLOAD    |0x00|0x00| (slot 0 in storage contains 0)
050 PUSH1 01 |0x01|0x00|0x00|
052 PUSH1 01 |0x01|0x01|0x00|0x00|
054 PUSH1 c8 |0xc8|0x01|0x01|0x00|0x00|
056 SHL      |0x00.50zeros.0100...00|0x01|0x00|0x00| move 0x01 to 0xc8 = 200times (50 hex numbers) to the left
057 SUB      |0x00.15zeros0tttffff.49f.fffff|0x00|0x00|
058 NOT      |0xfffffffffffffff00..49zeros..00|0x00|0x00| Our mask is created !
059 AND      |0x00|0x00|
060 PUSH25 358aa13c52544eccef6b0add0f801012adad5ee3aaaaaaaa11
             |0x00...00358aa13...|0x00|0x00|
086 OR       |0x00...00358aa13...|0x00|
087 SWAP1    |0x00|0x00...00358aa13...|
088 SSTORE   ||
089 STOP     ||

你可能猜到了,答案是肯定的!

该函数的结构与上一个例子(使用 uint8)几乎相同。只是在开始创建掩码时有一些不同。

因为有 25 个字节,所以掩码应该是0xfffffffffff00000...49zeros00000在指令 58/59 的AND之前。

之后,在第 60 指令,包含智能合约中存在的值的 25 个字节被推送(addr = 0x358aa13c52544eccef6b0add0f801012adad5ee3data = 0xaaaaaaaabalance = 0x11),并与指令86的掩码进行 "OR"。

最后,它们被 "存储"在 0 槽中。

使用了 43298 个 Gas(只执行了 1 个 SSTORE)。

除此之外,Gas 成本与存储 uint8 的成本差不多低。(43286 vs 43298)

4. 变量的位置是否有关系?

要了解 EVM,最好的方法是通过修改不同的参数,进行尽可能多的测试。这正是我们在这里所做的。

在这个例子中,我们将互换addrdata变量。

它们应该在存储空间中占据相同的位置,但位置应该是颠倒的,对吗?

代码语言:javascript
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Test {
    uint8 balance;
    address addr;
    bytes4 data;
    function modify() external {
        balance = 17;
        data = 0xaaaaaaaa;
        addr = 0x358AA13c52544ECCEF6B0ADD0f801012ADAD5eE3;
   }
}

让我们来编译和反汇编,在指令 60 处,我们有:(当然,代码的其他指令是一样的)

代码语言:javascript
复制
PUSH aaaaaaaa358aa13c52544eccef6b0add0f801012adad5ee311

这与上次的情况:“358aa13c52544eccef6b0add0f801012adad5ee3aaaaaaaa11” 不同。

我们可以看到,addrdata变量被互换了。

我们的假设是对的,两个变量的位置被颠倒了。balance = 17 (十六进制 11)仍然是在第一个位置,因为 EVM 使用的是小端(litter-endian)架构(第一个在最后)。

5. 如何存储结构体?

如果我们用结构体做同样的事情,会发生什么?

代码语言:javascript
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Test {
    struct Values {
        uint8 balance;
        address addr;
        bytes4 data;
    }
    Values value;
    function modify() external {
        value.balance = 17;
        value.addr = 0x358AA13c52544ECCEF6B0ADD0f801012ADAD5eE3;
        value.data = 0xaaaaaaaa;
     }
 }

下面是函数的额完整汇编代码:

代码语言:javascript
复制
045 JUMPDEST
046 PUSH1 00 |0x00|
048 DUP1     |0x00|0x00|
049 SLOAD    |0x00|0x00|
050 PUSH1 01 |0x01|0x00|0x00|
052 PUSH1 01 |0x01|0x01|0x00|0x00|
054 PUSH1 c8 |0xc8|0x01|0x01|0x00|0x00|
056 SHL      |0x00.50zeros.0100...00|0x01|0x00|0x00|
057 SUB      |0x00.15zeros0tttffff.49f.fffff|0x00|0x00|
058 NOT      |0xfffffffffffffff00..49zeros..00|0x00|0x00|
059 AND      |0x00|0x00|
060 PUSH25 aaaaaaaa358aa13c52544eccef6b0add0f801012adad5ee311 |0x00...00358aa13...|0x00|0x00|
086 OR       |0x00...00358aa13...|0x00|
087 SWAP1    |0x00|0x00...00358aa13...|
088 SSTORE   ||
089 STOP     ||

使用结构体的代码是完全一样。

警告:这意味着有时要区分 3 个不同的变量还是一个结构体可能很困难。

Gas 费也和以前一样:

好了,这对我们来说太容易了,直到现在,操作码之间没有什么区别。但我们还没有结束! 我们需要更多的挑战。

6. 那么数组呢?

数组是如何在 EVM 中存储的?像结构体或变量一样?

代码语言:javascript
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Test {
    uint[] values;
    // uint value2 in comment
    function modify() external {
        values.push(7);
        values.push(8);
    }
}

不是的 !

这一次,不幸的是,它更长、更复杂。我们将需要更多关于 EVM 中存储的理论。

"values" 变量是一个动态数组,最多可以存储无限多的值。但是有一个问题,如果我们在数组之后创建一个value2变量(就像注释中的那样),怎么办?它应该被存储在哪个存储槽中?

value2需要在 "value"之后,并且在槽 1 中,但是由于 "value"数组是动态的,可以改变大小,所以很难指定一个插槽。嗯...

在现实中,这仍然是正确的,value2将被存储在槽 1 中(因此接下来的变量将被存储在槽 2,3 中,以此类推...)。

但是槽 0 呢?槽 0 包含什么?

事实上,槽 0 存储的是数组的长度,在此案例中,在智能合约执行函数**modify()**后,它将存储 2,因为有 2 个值被推入数组。

但是,这些值被存储在哪里

由于数组的长度被存储在槽 0 中,这些值应该被存储在其他地方。

它是在槽Keccak256(0)+n中。

第一个值存储在槽 Keccak256(0)+0 中 下一个:Keccak256(0)+1第三个:Keccak256(0)+2第 n 个:Keccak256(0)+(n-1)

由于Keccak256(0) 是一个非常大的数字,EVM 不可能用完插槽。因此,我们解决了这个问题。

为什么在keccak256(0)中使用0?因为声明的数组是在 0 槽里。如果它是:

代码语言:javascript
复制
uint value2 //value 2 is in slot 0
uint[] values; //the array is in slot 1

因为数组在槽 1 中,这个数组中的第 i 个值被存储在Keccak256(1)+(n-1) ,数组的长度被存储在槽 1 中。

现在我们应该看看它在汇编中是如何工作的。首先,这是函数 modify()在汇编中的第一部分:

代码语言:javascript
复制
048 PUSH1 00 |0x00|
050 DUP1     |0x00|0x00|
051 SLOAD    |0x00|0x00|
052 PUSH1 01 |0x01|0x00|0x00|
054 DUP2     |0x00|0x01|0x00|0x00|
055 DUP2     |0x01|0x00|0x01|0x00|0x00|
056 ADD      |0x01|0x01|0x00|0x00|
057 DUP4     |0x00|0x01|0x01|0x00|0x00|
058 SSTORE   |0x01|0x00|0x00|
059 DUP3     |0x00|0x01|0x00|0x00|
060 DUP1     |0x00|0x00|0x01|0x00|0x00|
061 MSTORE   |0x01|0x00|0x00|

在指令51,EVM 从存储槽 0 加载,结果是 0,因为数组的长度是 0。

在第56指令,EVM 在 0 号存储槽加载的值上加 1,并在第 58 指令将其SSTORE到同一存储槽。因此,现在数组的长度是 1。

在第 61指令,EVM 在地址 0 处 MSTORE 0,我们将在后面看到原因。

代码语言:javascript
复制
062 PUSH1 07 |0x07|0x01|0x00|0x00|
064 PUSH32 290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563 |hash|0x07|0x01|0x00|0x00|
097 SWAP3    |0x00|0x07|0x01|hash|0x00|
098 DUP4     |hash|0x00|0x07|0x01|hash|0x00|
099 ADD      |hash|0x07|0x01|hash|0x00|
100 SSTORE   |0x01|hash|0x00|

0x07 被推到堆栈中,0 的哈希值也是如此,它是keccak256(0x00)(它等于 290decd...e563)

由于数组的当前索引是 0,EVM 将 0 添加到哈希值上(在指令 99),即堆栈中的第四个值。(这一点很重要,因为当 EVM 将另一个值添加到数组中时,索引将与 0 不同,因此结果也将不同)。

然后存储(SSTORE) 0x07 到结果槽,(槽 keccak256(0x00)+0 = 7)

这就成功存储了数组的第一个元素。

在第 2 次 SSTORE 之后,重复同样的代码,存储值为 8,但有细微差别:

代码语言:javascript
复制
101 DUP3     |0x00|0x01|hash|0x00|
102 SLOAD    |0x01|0x01|hash|0x00|
103 SWAP1    |0x01|0x01|hash|0x00|
104 DUP2     |0x01|0x01|0x01|hash|0x00|
105 ADD      |0x02|0x01|hash|0x00|
106 SWAP1    |0x01|0x02|hash|0x00|
107 SWAP3    |0x00|0x02|hash|0x01|
108 SSTORE   |hash|0x01|
109 PUSH1 08 |0x08|hash|0x01|
111 SWAP2    |0x01|hash|0x02|
112 ADD      |hash+1|0x02|
113 SSTORE   ||

在第 102 指令,SLOAD在槽 0 处返回 1,这是数组的长度。

在第 105 指令,ADD在结果上加 1,所以 1+1=2。

在第 108 指令,EVM SSTORE结果(是 2)在槽 0,这就是数组的新长度。

在第 112 指令,EVM 将 1 添加到 Hash 中,这是分配给values[1] 的槽。

在第 113 指令,EVM存储 值 8,在keccak256(0)+1 而不是上次的keccak256(0)

如果我们分析一下这种操作的 Gas 成本,我们会发现,这比非数组的情况下要高。

87746 Gas

有 1 个SSTORE来存储一个数组的长度。(+ 20000 gas) 有 2 个SSTORE来存储 7 和 8 的值。(+40000 gas) 有 1 个SSTORE到一个非零值槽,将数组的长度从 1 更新到 2。

我们就快完成了......

7. 如何存储映射?

现在我们来谈谈映射,像数组一样,如何存储映射的所有值并不明显。(ERC20 代币中的变量balances就是一个好例子)

我们知道,映射是一组键值对

对比数组,在映射中存储值的方式非常相似,存储槽等于SHA3(mapping_slot.key)

(其中 . 是连接运算符)在这最后一部分,我们将验证这个公式。

让我们来编译这个最后的合约,编译选项 没有优化(但仍然是 solidity 0.8.7) ,并调用modify() 函数!

代码语言:javascript
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Test {
    mapping(address => uint) balances;
    function modify() external {                  balances[0xbc5D291D2165f130375B94c62211f594dB48fEF2] = 15;     balances[0x9a8af21Ac492D5055eA7e1e49bD91BC9b5549334] = 55;
    }
}

这是函数的第一部分的完整反汇编:

代码语言:javascript
复制
054 PUSH1 0f |0x0f|
056 PUSH1 00 |0x00|0x0f|
058 DUP1     |0x00|0x00|0x0f|
059 PUSH20 bc5d291d2165f130375b94c62211f594db48fef2
             |address1|0x00|0x00|0x0f|
080 PUSH20 ffffffffffffffffffffffffffffffffffffffff
             |0xff..ff|address1|0x00|0x00|0x0f|
101 AND      |address1|0x00|0x00|0x0f|
102 PUSH20 ffffffffffffffffffffffffffffffffffffffff
             |0xff..ff|address1|0x00|0x00|0x0f|
123 AND      |address1|0x00|0x00|0x0f|
124 DUP2     |0x00|address1|0x00|0x00|0x0f|
125 MSTORE   |0x00|0x00|0x0f|
126 PUSH1 20 |0x20|0x00|0x00|0x0f|
128 ADD      |0x20|0x00|0x0f|
129 SWAP1    |0x00|0x20|0x0f|
130 DUP2     |0x20|0x00|0x20|0x0f|
131 MSTORE   |0x20|0x0f|
132 PUSH1 20 |0x20|0x20|0x0f|
134 ADD      |0x40|0x0f|
135 PUSH1 00 |0x00|0x40|0x0f|
137 SHA3     |keccak256(address1)|0x0f|
138 DUP2     |0x0f|keccak256(address1)|0x0f|
139 SWAP1    |keccak256(address1)|0x0f|0x0f|
140 SSTORE   |0x0f|

在第 54-59 指令 0f(十进制的 15),0,0 和 bc5d...f2 被推送到堆栈。

  • 0f 是modify() 函数中第一个地址的余额。
  • bc5d...f2 就像是 balance=15" 的地址。

在第 80 和 102 指令之间,这段代码没有任何作用,它只是通过使用掩码:0x00000000000000000000000ffffffffffffffffffffffffffffffffffffffff 保证堆栈中的地址 0xbc5d...之后只有 0000

例如,地址可能是0xd0000....bc5d...fd,EVM 和 0x000...000ffff的掩码进行与运算,从而去除开头的d

在第 125 指令,它用MSTORE把结果保存到内存(保存在地址为 0x00 的 "清洁 "内存中)。

在第 128 指令,它将 20 加到0x00,并在第 131 指令将 0 存储到内存。(因此 0 被存储在内存的 0x20 处)

在第 137 指令,调用SHA3指令,使用堆栈中的参数:0(offset)和 40(size)。

这条指令返回keccak256(memory[offset:offset+length])。这里 offset = Stack(0) = 0, length = Stack(1) = 40 (因为堆栈里有最后两个值)

这是地址0xbc5d...f2与 0 相连接的SHA3(或KECCAK256)。

之后,通过使用SSTORE,15 被存储在SHA3操作的结果槽中。

如果我们把它与开头的公式进行比较: SHA3(mapping_slot.key)

  • 余额变量的mapping_slot当然等于 0(存储在 0x20 到 0x40 的内存中,而不是在开头位置,因为 EVM 使用小端结构)
  • key等于地址 0xbc5d291d2165f130375b94c62211f594db48fef2(存储在 0x00 到 0x20 的内存中)。

对于那些想知道memory[0x00:0x40]的目的是什么的人来说,的答案是:目标是(仅仅)存储使用keccak256 进行 Hash 的操作数。

由于 EVM 对 0x00 和 0x40 之间的所有东西进行散列,我们的说法是正确的。

下面是这样一个操作的 Gas 成本:

使用 65594 Gas

成本比存储在数组中要低(至少在开始时),因为 mapping_slot(槽 0)不包含任何东西,也没有被修改,所以少了一个SSTORE操作。

8. 结论

如果你读完了这篇文章,恭喜你! 这并不容易,但它是值得的。你现在对 EVM 的了解比 99.9%的开发者还要多。


本翻译由 Duet Protocol[12] 赞助支持。

原文链接:https://trustchain.medium.com/reversing-and-debugging-ethereum-evm-smart-contracts-part-3-ebe032a08f97

参考资料

[1]

登链翻译计划: https://github.com/lbc-team/Pioneer

[2]

翻译小组: https://learnblockchain.cn/people/412

[3]

Tiny 熊: https://learnblockchain.cn/people/15

[4]

第1篇:理解汇编: https://learnblockchain.cn/article/4913

[5]

第2篇:部署智能合约: https://learnblockchain.cn/article/4927

[6]

第3篇:存储布局是如何工作的?: https://learnblockchain.cn/article/4943

[7]

第4篇:结束/中止执行的5个指令: https://medium.com/@TrustChain/reversing-and-debugging-evm-the-end-of-time-part-4-3eafe5b0511a

[8]

第5篇:执行流 if/else/for/函数: https://medium.com/@TrustChain/reversing-and-debugging-evm-the-execution-flow-part-5-2ffc97ef0b77

[9]

第6篇:完整的智能合约布局: https://medium.com/@TrustChain/reversing-and-debugging-part-6-full-smart-contract-layout-f236c3121bd1

[10]

第7篇:外部调用和合约部署: https://medium.com/@TrustChain/reversing-and-debugging-theevm-part-7-2a20a44a555e

[11]

第1篇:理解汇编: https://learnblockchain.cn/article/4913

[12]

Duet Protocol: https://duet.finance/?utm_souce=learnblockchain

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-10-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 深入浅出区块链技术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 简单的例子
  • 2. 使用 uint8 而不是 uint256
  • 3. 使用不同类型的数据
  • 4. 变量的位置是否有关系?
  • 5. 如何存储结构体?
  • 6. 那么数组呢?
  • 7. 如何存储映射?
  • 8. 结论
    • 参考资料
    相关产品与服务
    对象存储
    对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档