深入理解EVM系统(3)

  • XPTY
  • 更新于 2022-03-31 16:13
  • 阅读 3815

本文是《深入理解EVM系统》系列的第三部分,将建立在深入理解EVM系统(1)深入理解EVM系统(2)之上。在这一部分中,我们将深入探讨合约存储的工作原理,提供一些心智模型来帮助你理解以及深入探索存储槽打包 。

作者:noxx

译者:Kurt Pan

本文是《深入理解EVM系统》系列的第三部分,将建立在深入理解EVM系统(1)深入理解EVM系统(2)之上。

在这一部分中,我们将深入探讨合约存储 的工作原理,提供一些心智模型来帮助你理解以及深入探索存储槽打包

如果“槽打包”这个术语对你来说很陌生,也请不要担心。槽打包的知识对于黑客来说是至关重要的,在文章的最后你将会对它有一个深刻的理解。如果你曾经尝试过Ethernaut Solidity Wargame 系列或其他 Solidity “夺旗”类型的游戏,你就会知道槽打包的知识通常是破解挑战谜题/成功hack的关键。

1存储基础

下面链接的帖子对存储的基础知识进行了高层次的概述。我将复习本文所需的关键点,但我还是强烈建议你去阅读一下全文。

https://programtheblockchain.com/posts/2018/03/09/understanding-ethereum-smart-contract-storage/

数据结构

我们将从合约存储的数据结构开始,这为其余知识奠定了坚实的基础。

合约存储 只是一个键值映射。它将一个 32 字节的键映射到一个 32 字节的值。因为键长为 32 字节,我们最多可以有 (2^256)-1 个键。(32 字节等于 256 位,这为我们提供了 (2^256)-1 个二进制数可供选择作为键。)

所有值都初始化为 0,并且0不被显式存储。这是有道理的,因为 2^256 大约是已知可观测宇宙中的原子数。没有一台计算机可以保存这么多数据。这也是将存储值设置为0会给你返还一些gas的原因,因为该键-值对不再需要由网络节点存储。

从概念上讲,存储可以被视为一个天文数字的大数组。我们的第一个二进制值为 0 的键表示数组中的第 0 项,二进制值为 1 的键表示数组中的第 1 项,依此类推。

1.png

定长变量

声明为存储变量的合约变量可以分为定长和变长两大阵营。我们将专注于定长变量,以及 EVM 如何将多个变量打包到一个 32 字节的存储槽中。

要了解有关变长变量的更多信息,请参阅下面链接。

既然我们知道存储是一个键值映射,那么下一个问题是键是如何分配给变量的。假设我们有以下solidity代码。

contract StorageTest {
 uint256 valuel;
 uint256[2] value2;
 uint256 value3;
}

因为所有这些变量都是定长的,EVM 可以从使用保留存储位置(键)槽 0(二进制值 0 的键)开始并线性向前移动到槽 1、2 等。将根据在合约中声明变量的顺序来执行此操作。第一个声明的存储变量将存储在槽 0 中。

本例中,槽 0 将保存变量value1,变量value2是一个固定大小的 2 数组,因此将占用槽 1 和 2,最后,槽 3 将保存变量value3。如下图所示。

2.png

现在让我们看一下一个类似的合约,并看看变量在这种情况下是如何存储的。

contract StorageTest {
 uint32 valuel;
 uint32 value2;
 uint64 value3;
 uint128 value4;
}

注意变量类型不是 uint256。

你可能认为这会和上例一样占用槽 0 到 3。在前面的示例中有 4 个值要存储(考虑到大小为 2 的数组),在这个示例中我们也有 4 个值要存储。

你会惊讶地发现在此例中只使用了存储槽 0。

主要区别在于用于变量的uint类型。上例中,所有变量都是 uint256 类型,代表 32 个字节的数据。这里我们使用 uint32uint64uint128 分别代表 4、8 和 16 字节的数据。

槽打包

这就是术语槽打包 出现的地方。solidity 编译器知道它可以在一个存储槽中存储 32 个字节的数据。这样一来,当uint32 value1只占用4个字节存储在槽0时,编译器读取下一个变量时会看是否可以打包到当前存储槽中。

槽0有 32个字节的空间,而 value1 只占用了其中的 4 个,只要下一个变量的大小小于 28 个字节,它也会被打包到槽0中。

对于上面的例子,我们从槽0的 32 字节开始;

  • value1 存储在槽 0 中,占用 4 个字节
  • 槽 0 剩余 28 个字节
  • value2 是 4 个字节 <= 28 因此它可以存储在槽 0
  • 槽 0 剩余 24 个字节
  • value3 是 8 个字节, <= 24 因此它可以存储在槽 0
  • 槽 0 剩余 16 个字节
  • value4 是 16 个字节,即 <= 16 因此它可以存储在槽 0
  • 槽 0 剩余 0 个字节

注意 uint8 是最小的solidity类型,因此打包不能小于1字节(8位)

下图显示了槽 0 中的 32 字节数据如何保存所有 4 个变量。

3.png

EVM 存储操作码

现在我们了解了存储的数据结构和槽打包的概念,来快速看一下2个存储操作码SSTORESLOAD

SSTORE

我们从 SSTORE 开始,它从调用栈中获取一个 32 字节的键和一个 32 字节的值,并将该 32 字节的值存储在该 32 字节的键的位置。查看如下EVM 游乐场,了解它是如何工作的。

SLOAD

SLOAD,从调用栈中获取一个 32 字节的键,并将存储在该 32 字节键位置的 32 字节值压入调用栈。查看如下EVM 游乐场,了解它是如何工作的。

在这里你应该问自己的问题是,如果 SSTORE&lt;span> &lt;/span>SLOAD 只会处理 32 字节的值,你要如何提取出已打包到一个 32 字节槽中的一个变量。

以上面的示例为例,当我们在槽 0 上运行 SLOAD 时,我们将获得存储在该位置的完整 32 字节值。该值将包括 value1value2value3value4 的数据。EVM 要如何提取 32 字节槽中的特定字节以返回我们需要的值呢?

当我们运行 SSTORE 时也是同样,如果我们每次都存储 32 个字节,那么EVM 如何确保当我们存储 value2 时它不会覆盖 value1?当我们存储 value3 时,它不会覆盖 value2 ?等等。

这些就是我们接下来要回答的问题。

2存储和检索打包变量

下面是一个简单的合约,模仿了我们上面看到的例子。唯一的补充是一个store函数,它设置变量值并且必须读取一个变量来执行一些算术运算。

pragma solidity >=0.7 .0&lt;0.9.0;
/**
* @title Storage
* @dev Store & retrieve value in a variable
*/
contract StorageTwo {
 uint32 value1;
 uint32 value2;
 uint64 value3;
 uint128 value4;

 function store() public {
  value1 = 1 ;
  value2 = 22;
  value3 = 333;
  value4 = 4444;
  uint96 value5 = value3 + uint32(666);
 }
}

上述 Solidity 中的 store() 函数将执行我们对之有疑问的操作:将多个变量存储在单个槽中而不覆盖现有数据并从 32 字节槽中检索变量的那些特定字节。

让我们从查看槽 0 的结束状态开始,然后从那里向前反推。下面是槽 0 的二进制和十六进制表示。

谨记机器最终会将十六进制数视为二进制数,这很重要,因为在槽打包中使用了许多位运算。

十六进制:

0000000000000000000000000000115c000000000000014d0000001600000001

二进制:

000000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000010001010111 000000000000000000000000000000000000000000000000000000000101001 1010000000000000000000000000001011000000000000000000000000000000001

注意你可以看到的十六进制值,0x115c 等于十进制的 4444,0x14d = 333, 0x16 = 22 & 0x01 = 1。这些对应于我们在 Solidity 代码中看到的内容。一个槽可容纳 32 个字节的数据,即 64 个十六进制字符或 256 位。

位运算

槽打包使用 3 种按位运算,ANDOR & NOT。这些对应于 3 个具有相同命名的 EVM 操作码。让我们快速浏览一下每一个。

AND

下面我们有两个 8 位二进制数。在AND 操作中,第一个数字中的第一个位与第二个数字中的第一个位进行比较。

如果两个值都是 1,则 AND 语句返回 true,结果的第一位将等于 1,否则语句返回 false,该位等于 0。

继续进行,第一个数字的第 2 位与我们的第二个数字的第 2 位等进行比较。

4.png

OR

在 OR 运算中,只有一个值需要为 1 才能使语句返回&lt;span> &lt;/span>true。输入和上面相同,但得到了完全不同的输出。

5.png

NOT

NOT 略有不同,它只接受一个值,而不是对两个值进行比较。NOT 对每一位执行逻辑非。为 0 的位变为 1,为 1 的位变为 0。

6.png

现在让我们看看在上面的solidity示例中如何使用它们。

槽操作之槽打包 SSTORE

我们将专注于solidity 代码的第18行:

value2 = 22;

在这个阶段,一些数据&lt;span> &lt;/span>value1 已经存储在槽0 中,我们现在需要将一些额外的数据打包到同一个槽中。

我们在此例中看到的所有逻辑都会与存储value3value4 时相同。我们将看看这是如何在概念上完成的,并将提供一个 EVM 游乐场供你进一步探索。

我们从以下值开始。

0x00000016 = 22 (value2)
0x00 = 0 (槽0)
0x04 = 4 (4 字节输入, value2的开始位置)
0x0100 = 256 (256位在1字节中)
0xffffffff = 4294967295 (二进制的1, 4字节大小)

注意0xffffffff等于二进制的1111111111111111111111111111111

EVM 做的第一件事是使用 EXP 操作码,输入一个基整数和一个指数并返回值。

这里我们使用 0x0100 作为代表 1 个字节的基整数,其指数是 0x04,即value2的起始位置。

EXP操作码产生了值:0x0000000000000000000000000000000000000000000000000000000100000000

如果将这个值乘以value2的值,将在32字节槽中正确的位置得到我们想要的值0x016 :0x0000000000000000000000000000000000000000000000000000001600000000

我们可以看到 EXP 函数的结果使我们能够在正确的位置插入数据。

但是我们不能使用它,因为它会覆盖已经存储的 value1。这就是要使用位掩码 的地方了。

EXP操作码产生了值:0x0000000000000000000000000000000000000000000000000000000100000000

如果用0xffffffffff乘以这个值就得到了一个在value2的4个字节的位置的一个位掩码:

0x000000000000000000000000000000000000000000000000fffffffff00000000

在这个值上进行按位NOT,得到除了value2的4个字节的位置之外所有位置的位掩码:

0xffffffffffffffffffffffffffffffffffffffffffffffff00000000ffffffff

在槽0使用SSLOAD,将返回:(注意value1在返回值中出现)

0x0000000000000000000000000000000000000000000000000000000000000001

对槽0中的数据和位掩码使用AND,将返回在赋值给value2的4个字节之外的数据,在value2的4个字节位置的数据返回0:0x0000000000000000000000000000000000000000000000000000000000000001

上面显示了如何使用位掩码从槽中获取所有数据,但要覆盖的字节除外。在本例中,value2&lt;span> &lt;/span>的字节已经设置为 0,但是如果没有设置,我们会看到这些数据被擦除。

下面是另一个具体说明正在发生的事情的例子。这是相同的过程,但这里所有 4 个值都已被存储了,我们希望将 value2 从 22 更新为 99 ,我们想看看会发生什么。注意现有的 0x016 值被清零。

位掩码:0xffffffffffffffffffffffffffffffffffffffffffffffff00000000ffffffff

在所用值被存储后在槽0上使用SSLOAD0x0000000000000000000000000000115c000000000000014d0000001600000001

对槽0中的数据和位掩码使用AND:0x0000000000000000000000000000115c000000000000014d0000000000000001

你可能已经在想按位OR可以如何帮助我们组合我们有的值。如下展示了下面的步骤:

Value2:0x00000016

4 字节 0xffffffff ,二进制全1串:0xffffffff

value20xffffffff进行按位AND操作确保了如果提供的值大于4字节将会被截断:0x00000016

EXP操作码生成值:0x0000000000000000000000000000000000000000000000000000000100000000

将此值和value2相乘,将在32字节槽中的正确位置上得到想要的值0x016

0x0000000000000000000000000000000000000000000000000000001600000000

使用之前位掩码一节的结果:

0x0000000000000000000000000000000000000000000000000000000000000001

在之前的两个值上使用按位OR ,将在特定位置存储value2,与此同时维持槽0中已经存入的值:0x0000000000000000000000000000000000000000000000000000001600000001

我们现在知道如何可以在槽 0 的这个 32 字节值上使用 SSTORE了,其中包含处于正确字节位置的 value1value2 的数据。

槽操作之检索打包变量 SLOAD

对于检索,我们将关注solidity的第22行:

uint96 value5 = value3 + uint32(666)

我们对算术本身不感兴趣,对如何检索出value3 以执行计算感兴趣。

我们有一组略有不同的起始值。

0x00 = 0(槽0)

0x08 = 8 (8 字节输入, value3的起始位置)

0x0100 = 256 (256位在1字节中)

0xffffffffffffffff = 18446744073709551615 (二进制的1, 8字节大小)

0x0000000000000000000000000000115c000000000000014d0000001600000001 = 槽0的值

我们已经看到的大部分内容,将重新用于检索,只有一些修改。

0x01000x08&lt;span> &lt;/span>上使用EXP 操作码将生成:0x0000000000000000000000000000000000000000000000010000000000000000

在存储槽0SSLOAD :0x0000000000000000000000000000115c000000000000014d0000000000000001

DIV操作码用EXP值除以槽0的值,这在效果上截断了value1value2 的低8字节:0x00000000000000000000000000000000000000000000115c000000000000014d

对前8字节的位掩码:0x000000000000000000000000000000000000000000000000ffffffffffffffff

对此位掩码和从DIV有效截断清零前16字节的返回值进行 AND操作,返回变量value3的8字节:0x000000000000000000000000000000000000000000000000000000000000014d

我们从打包的槽 0 中检索到了 value3。十六进制 0x14d 等于 333,这正是我们在上面的 solidity 代码中设置的。

位掩码和按位运算再一次被用于帮助从 32 字节槽中提取特定字节。这个值现在在栈上,EVM 可以使用它来计算value3 + uint32(666)

3EVM 游乐场

我把我们刚刚探索的store() 函数中执行的所有操作码放入了EVM 游乐场。在这里,你将能够以交互方式使用所使用的操作码,并查看调用栈和合约存储在你跳转时如何变化。

https://tinyurl.com/ms49stff

我已经在我们探索的 2 个部分(solidity 第 18 和 22行)的操作码旁边留下了注释。强烈建议检查一下并单步运行一下操作码,这将大大增强你的理解。

7.png

你现在应该深入了解了存储槽打包的工作原理以及 EVM 如何能够在 32 字节槽的特定位置检索和存储字节。尽管 EVM 操作码SLOAD&lt;span> &lt;/span>SSTORE 仅处理 32 字节块,但我们可以使用按位操作和位掩码来存储和加载我们想要的数据。

希望你喜欢这篇文章。

本文首发于:https://mp.weixin.qq.com/s/OuxdJLADjmX36OrbW7xS3A

点赞 4
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
XPTY
XPTY
江湖只有他的大名,没有他的介绍。