本文介绍了Solidity中的位移操作及其应用,包括数据类型的转换、特定位的提取和设置。同时,文章还深入探讨了在YUL汇编中进行存储读取和写入时如何应用位移和掩码技术。
注意:如果你已经是一个经验丰富的开发者/研究者,请直接跳到文章第2点!
照片由 Markus Spiske 在 Unsplash 提供
位移是 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
构造函数将 a
的 uint256
值转换为 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
。
有时在使用汇编时,你需要访问或写入变量。
有一个特定的情况是多个变量被打包在一起。
由于 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)
}
}
返回:
你可以看到,它返回了 E 的值 8
恭喜你,现在你知道如何从存储中读取,但我们仍然需要发现如何写入和存储变量。
写入可能会很困难,因为 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!