本文详细介绍了 Solidity 中的有符号整数及其在 EVM 层面的使用方式,重点解释了两补码(Two’s Complement)表示法及其在加减乘除等算术运算中的应用。
Solidity 签名整数允许在智能合约中使用负数。本文档记录了它们在 EVM 级别上的使用。假定读者对 EVM 和二进制数有基本的了解。
像所有数据类型一样,Solidity 仍然使用32字节的字来表示签名整数。在 EVM 中,没有任何语义指示该类型,就像没有任何指示说明一个32字节的插槽实际上是一个布尔值、一个地址或一个160位数一样。该值在编译时被“视为”负一。
因为你可以使用“type(int256).max”获得一个整数的最大值,或者使用.min字段获取最小值。判断一个数字是正是负需要额外的一位,因此它只能存储比无符号版本少一位的数字。
一的补码意味着一个 uint256 变成一个 uint255,最左边的一位指示它是正数还是负数。如果 EVM 使用一的补码。这会意味着 type(int256).max == absoluteValue(type(int256.min))
,但情况并非如此。二的补码负数的最大幅度比正数的最大幅度大一。例如,int8 的最大正数是 127,但 int8 的最大负数是 -128。
与其进行一堆数学证明,不如使用一些真实的例子(这并不是为了证明二的补码算术,仅供感兴趣的读者参考文献)。
让我们使用 int8 来使示例更具可读性。以下是二进制而非十六进制。
int8(0) == 0000 0000
type(int8).max == 0111 1111
type(int8).min == 1000 0000
看到 +1 和 -1 的表示是说明性的:
int8(1) == 0000 0001
int8(-1) == 1111 1111
让我们向下计数,以便一个模式变得明显:
int8(-2) == 1111 1110
int8(-3) == 1111 1101
int8(-4) == 1111 1100
int8(-5) == 1111 1011
你可以大致认为二的补码负数是“向下计数”。
这是二的补码的一个有趣特性。-2 + -2 应该等于 -4,而在二的补码中允许溢出的加法使得这一点得以实现。以下是在 Python 中使用二的补码表示将 -2 相加的示例
>>> (int(b'11111110', 2) + int(b'11111110', 2)) % 256
252
>>> bin(252)
'0b11111100'
这与上面预期的模式相匹配。
如果我们将 +4 加到 -2 上呢?我们应该得到 +2。让我们看看实际情况
>>> # -2 + 4
>>> (int(b'11111110', 2) + int(b'00000100', 2)) % 256
>>> 2
只有在两个数字都是二的补码表示时,这才有效。Solidity 不允许将无符号和签名整数相加,因为这种情况下意图是不明确的。
二的补码还可以处理乘法。-2 和 -2 的预期结果是 +4,读者可参考之前的代码来验证这一点。
二的补码不需要对加法、减法、乘法,甚至是左移操作(<<)进行更改。这些对应于 EVM 操作码 ADD、SUB、MUL 和 SHL。我们将在本教程的后面部分讨论左移为何仍在二的补码中有效。
然而,乘法、取模、右移和转换为更大的签名整数无法使用签名方法完成,并且需要自己的操作码。类似地,传统比较运算符将无法工作,因为负数“看起来”比正数更大。
SDIV,或签名除法,用于除法操作签名数字。这个操作码在如下代码中被用作为后台。
function divide(int256 a, int256 b) public pure returns (int256 quotient)
{
quotient = a / b;
}
由于二的补码算术为除法需要自己的操作码,因此对取模(余数)同样适用。
function divide(int256 a, int256 b) public pure returns (int256 remainder)
{
remainder = a % b;
}
为了比较签名数字的幅度,我们首先需要确定它是正数还是负数,然后比较幅度。这些操作码一步到位地完成了这个操作。
与无符号对应物一样,避免使用 >= 和 <= 会更节省Gas成本,尽量使用严格不等式运算符。
SAR 是一个非常少用的操作码,但它将出现在编译此 Solidity 代码的结果中。请注意 x 是整数,y 是无符号整数。
contract SarExample {
function main(int256 x, uint256 y) public pure returns (int256 res) {
res = x >> y;
}
}
我们应该如何理解这一点?在普通的无符号数字中,右移一位的位效果相当于除以二,右移两位的位效果相当于除以四,等等。
uint256 x = 8 >> 2; // x = 2
uint256 y = 4 >> 1; // y = 2
如果你对签名整数执行此操作,这种现象则得以保留。
int256 x = -8 >> 2; // x = -2
int256 y = -4 >> 1; // y = -2
为什么没有 SAL(签名算术左移)操作码?在以下示例中会发生什么?
int256 x = -8 << 2; // x = -32
int256 y = -4 << 1; // y = -8
我们分别乘以 4 和 2。在二的补码中,左移仍然按预期保留数字。
在内部,使用了常规的 SHL(左移)操作码。对于算术左移,不需要特殊情况。这看起来可能有些直观,因为右侧位变为零时数字会增大。但请记住,在二的补码中,最大负值是当最左边的一位为 1,所有其他位为零时。
比 256 位小的签名整数将有前导零。然而,二的补码负数总是以最左边的一位为 1 开头。因此,如果一个二的补码整数被提升为更大的类型,值将从负变为正,因为最左边的位将为零。Signextend 无缝地处理此过渡。
你不能在 Solidity 中直接使用 signextend,但它在将较小的整数转换为较大的整数时用于后台。以下代码在其编译字节码中包含 signextend 操作码,将 int8 转换为 int256。
contract SignExtendExample {
function main(int8 x) public pure returns (int256 res) {
res = x;
}
}
此时应该显而易见,较大的整数无法转换为较小的整数。
在我们的专家Solidity 培训课程中学习更多高级主题。
最初发布于 2023 年 4 月 11 日
- 原文链接: rareskills.io/post/signe...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!