Solidity与EVM:汇编(YUL)中的位移与掩码操作

本文介绍了Solidity中的位移操作及其应用,包括数据类型的转换、特定位的提取和设置。同时,文章还深入探讨了在YUL汇编中进行存储读取和写入时如何应用位移和掩码技术。

注意:如果你已经是一个经验丰富的开发者/研究者,请直接跳到文章第2点!

照片由 Markus SpiskeUnsplash 提供

1: 位移介绍

位移是 Solidity 中一个重要的概念,它允许有效地操作二进制数据。在这篇博客文章中,我们将探讨 Solidity 中的位移及其应用。它可以用于在数据类型之间转换,提取特定的位,或设置特定的位。理解位移对于编写高效和优化的代码至关重要。

什么是位移?

位移是一种操作,它涉及将一个二进制数的位向左或向右移动。这个操作对于操作二进制数据是有用的,比如在不同的数据类型之间转换、提取特定的位或设置特定的位。

在 Solidity 中,我们可以使用 <<>> 操作符进行位移。<< 操作符将一个数字的位向左移动,而 >> 操作符将位向右移动。

位移示例

以下是 Solidity 中进行位移的一些示例:

向左位移

uint256 a = 2;      // 0b10
uint256 b = a << 1; // 0b100

在这个例子中,我们将 a 的位向左移动了一位,结果是二进制数 0b100,它等价于十进制数 4

向右位移

uint256 a = 8;      // 0b1000
uint256 b = a >> 1; // 0b100

在这个例子中,我们将 a 的位向右移动了一位,结果是二进制数 0b100,它等价于十进制数 4

位移的应用

位移是一个强大的工具,可以用于 Solidity 中的许多目的。以下是一些示例:

在数据类型之间转换

位移可以用于在不同的数据类型之间转换,例如将 uint256 转换为 uint8。例如:

uint256 a = 256;      // 0b100000000
uint8 b = uint8(a);   // 0b00000001

在这个例子中,我们使用 uint8 构造函数将 auint256 值转换为 uint8。由于 uint8 只能保存高达 255 的值,所以最重要的位在转换时会被截断。

提取特定的位

位移可以用于从一个二进制数中提取特定的位。例如:

uint256 a = 0b11001100;
uint256 b = (a >> 4) & 0b00001111; // 0b1100

在这个例子中,我们将 a 的位向右移动四位,结果是二进制数 0b1100,对应十进制数 12。然后我们使用按位与运算符 & 与二进制数 0b00001111 来提取最低的四位。

设置特定的位

位移可以用于在一个二进制数中设置特定的位。例如:

uint8 a = 0b00000100;
uint8 b = a | 0b00001000; // 0b00001100

在这个例子中,我们使用按位或运算符 | 与二进制数 0b00001000 来将 a 的第三个位设置为 1,结果是二进制数 0b00001100,对应十进制数 12

2 位移和掩码在汇编(Yul)中的应用。

从存储读取

有时在使用汇编时,你需要访问或写入变量。

有一个特定的情况是多个变量被打包在一起。

由于 EVM 有 256 位(32 字节)槽,你无法直接访问打包的变量,而是要访问存储槽并找到该变量的偏移量。

之后,你需要右移位,直到到达存储在槽内变量的偏移量。

如果需要,你将需要掩盖一些位,以便它返回预期的值。

让我们进入代码,因为在简单的文本中很难消化:

我们将从使用汇编读取存储变量开始。

以下函数加载你作为参数传递的存储槽内的 bytes32 内容。

function readBySlot(uint256 slot) external view returns (bytes32 value) {
        assembly {
            value := sload(slot)
        }
    }

示例1:

uint256 public weiss = 789;

返回:0x0000000000000000000000000000000000000000000000000000000000000315

注意,315 是 789 的十六进制表示

示例2:如果在同一个槽中有多个变量呢?

uint128 public C = 4;
    uint104 public D = 6;
    uint16 public E = 8;
    uint8 public F = 1;

返回:0x0100080000000000000000000000000600000000000000000000000000000004

你可以看到所有变量都打包在同一个 32 字节槽中。

!!!!!!!!!

注意:变量总是从下到上打包。所以最后一个,在这种情况下,F,始终是槽中的第一个,如你所见 0x01。

!!!!!!

你看到了问题吗?我们如何获取例如变量 E?

要实现这一点,我们需要知道变量的偏移量,也就是说,变量在槽中的准确字节位置,从右到左。

让我们编码:

function getoffset()external pure returns(uint32 offset){
    assembly{
        offset := E.offset
    }

返回:29

这意味着该变量向左移动 29 字节

现在我们得到了所需的内容,我们将开始位移

我们必须将 E 的值右移 29 字节,以便返回它。为此,我们需要使用 shr,它获取右移的位数。由于我们有字节,我们必须将该数字乘以 8:

function shiftE()external view returns(bytes32 e){
assembly{
    let slot := sload(E.slot)
    e := shr(mul(E.offset, 8), slot)
}
}

注意 shr 有 2 个参数,右移的位数和我们正在位移的槽内的值。

返回:0x0000000000000000000000000000000000000000000000000000000000010008

伙计们,我们快要得到了,现在我们在最后的字节中得到了 E,但有些奇怪。我们仍然有变量 F 1,向左移动 2 字节,我们必须去掉它。

现在我们将开始掩码

一些关于掩码的规则:

// 掩码可以是硬编码的,因为变量的存储槽和偏移量是固定的
   // V 代表值
   // V 和 00 = 00
   // V 和 FF = V
   // V 或 00 = V

让我们对掩码做个简要介绍:

你会看到很多 0xffffffffffffffffffffffffffffffffffff

实际上,这些 ffffff 是 二进制中的 1

// 规则:
//
// f = 1111
// ff = 11111111

所以,可以看到,一个 f 对应于 4 个 1。

现在让我们掩码 F 变量,以仅返回 E

function shiftE()external view returns(bytes32 e, uint256 masked){
assembly{
    let slot := sload(E.slot)
    e := shr(mul(E.offset, 8), slot)
    masked := and(0xffff,e)
}
}

返回:

  • 0:bytes32: e 0x0000000000000000000000000000000000000000000000000000000000010008
  • 1:uint256: masked 8

你可以看到,它返回了 E 的值 8

恭喜你,现在你知道如何从存储中读取,但我们仍然需要发现如何写入和存储变量。

3 位移和掩码在汇编(Yul)中的应用。

在不破坏代码的情况下写入存储

写入可能会很困难,因为 EVM 只能以 32 字节的增量写入。

我们要写入一个被打包的变量,比如说,E,再次是 uint16(2 字节)。

我们必须再次记住,v 和 00 是 00,v 或 00 是 v,和 v 和 ff 是 v。

让我们开始写入存储!让我们将 E 的值改为 10

步骤:

1 加载我们想要写入的槽的值。

function writeE(uint16 newE)external  returns(bytes32 value,bytes32 clearedE, bytes32 newV, bytes32 shifted){
assembly{
    //newE = 0x0000000000..0000000a = 10
     value:= sload(E.slot)

2 通过位掩码删除 E 值。我们知道 v 和 00 是 00,因此我们将 0000 传递到 E 存储的 2 字节中,因此我们将其重置为 0。

为了保持其他变量的当前状态,我们使用 ffffff,因为 v 和 ff 是 v。

//我们想删除 E
      clearedE := and(0xff0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, value)

3 为 E 添加新值,在我们的例子中是 10,或者在十六进制中是 a,我们在函数中作为参数传递。

现在我们必须将值向左移动,移动到槽中之前的存储偏移量。如前所述,shl 接收的是位而不是字节,因此我们乘以 8。

shifted := shl(mul(E.offset,8),newE)
//0x00000a0000000000000000000000000000000000000000000000000000000000

4 最后,将两个 32 字节变量 clearedE 和 new shiftedE 结合在一起,使用 or。你可以查看官方 Solidity 文档,以更好地理解使用的操作码。

newV := or(shifted,clearedE)
//newV 0x01000a0000000000000000000000000600000000000000000000000000000004
sstore(E.slot,newV)

5 如我们所知,使用 or 时,v 或 00 是 v。因此,我们能够在同一个偏移量中插入新变量 E。

完整函数:

function writeE(uint16 newE)external  returns(bytes32 value,bytes32 clearedE, bytes32 newV, bytes32 shifted){
assembly{
    //newE = 0x0000000000..0000000a = 10
     value:= sload(E.slot)
     //value: value 0x0100010000000000000000000000000600000000000000000000000000000004
      //我们想删除 E
      clearedE := and(0xff0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, value)
      //删除的 E: 0x0100000000000000000000000000000600000000000000000000000000000004
      shifted := shl(mul(E.offset,8),newE)
      //0x00000a0000000000000000000000000000000000000000000000000000000000
      newV := or(shifted,clearedE)
      //newV 0x01000a0000000000000000000000000600000000000000000000000000000004
      sstore(E.slot,newV)
}

希望你喜欢这篇文章,如果你觉得这篇文章对你有帮助,我将非常感激任何形式的支持。

Twitter: https://twitter.com/0xWeisss

  • 原文链接: medium.com/@mweiss.eth/s...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

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