正在 x86 计算机上移动超过 32 位的uint64_t整数未定义的行为

Is Shifting more than 32 bits of a uint64_t integer on an x86 machine Undefined Behavior?

本文关键字:整数 未定义 uint64 位的 计算机 x86 移动 正在      更新时间:2023-10-16

艰难的学习方式,我尝试左移一个long long,并在x86机器上uint64_t到超过32位,结果0。我依稀记得在 32 位机器班次之外的地方读过,操作员只在前 32 位上工作,但无法回忆起来源。我想知道在 x32 机器上移动超过 86 位的 uint64_t 整数是否是一种未定义的行为?

标准说(n1570 中的 6.5.7

(:

3 整数提升对每个操作数执行。结果的类型为 提升的左操作数。如果右操作数的值为负数或大于或等于提升的左操作数的宽度,则行为未定义。

4 E1 <<E2 的结果是 E1 左移 E2 位位置;空出的位填充 零。如果 E1 具有无符号类型,则结果的值为 E1 × 2E2,约模 比结果类型中可表示的最大值多一个。如果 E1 具有已签名的 类型和非负值,并且 E1 × 2E2 在结果类型中表示,即 结果值;否则,行为是未定义的。

5 E1>> E2 的结果是 E1 右移 E2 位位置。如果 E1 具有无符号类型 或者,如果 E1 具有有符号类型和非负值,则结果的值为 E1/2E2 的商的一部分。如果 E1 具有有符号类型和负值,则 结果值由实现定义。

uint64_t移位的距离小于 64 位完全由标准定义。

由于long long必须至少为 64 位,因此如果结果没有溢出,则小于 64 位的long long值由非负值的标准定义。

但是请注意,如果您编写适合 32 位的文字,例如 uint64_t s = 1 << 32正如@drhirsch所推测的那样,您实际上不会移动 64 位值,而是移动 32 位值。这是不确定的行为。

最常见的结果是偏移 shift_distance % 32 或 0,具体取决于硬件的作用(并假设编译器的编译时评估模拟硬件语义,而不是鼻魔(。

使用1ULL < 63使移位操作数在移位之前unsigned long long

C 标准要求移位才能正常工作。 特定的错误编译器可能存在您描述的缺陷,但这是错误行为。

这是一个测试程序:

#include <stdio.h>
#include <inttypes.h>
int main(void)
{
    uint64_t x = 1;
    for (int i = 0; i < 64; i++)
        printf("%2d: 0x%.16" PRIX64 "n", i, (x << i));
    return 0;
}
这是运行带有GCC 4.1.2的RHEL 5的i686机器,以及x86/64

机器(也运行RHEL 5和GCC 4.1.2(和x86/64 Mac(运行带有GCC 4.7.0的Mac OS X 10.7.3(上的输出。 由于这是预期的结果,我得出结论,在 32 位机器上没有必要的问题,并且 GCC 至少自 GCC 4.1.2 以来没有表现出任何此类错误(并且可能从未表现出这样的错误(。

 0: 0x0000000000000001
 1: 0x0000000000000002
 2: 0x0000000000000004
 3: 0x0000000000000008
 4: 0x0000000000000010
 5: 0x0000000000000020
 6: 0x0000000000000040
 7: 0x0000000000000080
 8: 0x0000000000000100
 9: 0x0000000000000200
10: 0x0000000000000400
11: 0x0000000000000800
12: 0x0000000000001000
13: 0x0000000000002000
14: 0x0000000000004000
15: 0x0000000000008000
16: 0x0000000000010000
17: 0x0000000000020000
18: 0x0000000000040000
19: 0x0000000000080000
20: 0x0000000000100000
21: 0x0000000000200000
22: 0x0000000000400000
23: 0x0000000000800000
24: 0x0000000001000000
25: 0x0000000002000000
26: 0x0000000004000000
27: 0x0000000008000000
28: 0x0000000010000000
29: 0x0000000020000000
30: 0x0000000040000000
31: 0x0000000080000000
32: 0x0000000100000000
33: 0x0000000200000000
34: 0x0000000400000000
35: 0x0000000800000000
36: 0x0000001000000000
37: 0x0000002000000000
38: 0x0000004000000000
39: 0x0000008000000000
40: 0x0000010000000000
41: 0x0000020000000000
42: 0x0000040000000000
43: 0x0000080000000000
44: 0x0000100000000000
45: 0x0000200000000000
46: 0x0000400000000000
47: 0x0000800000000000
48: 0x0001000000000000
49: 0x0002000000000000
50: 0x0004000000000000
51: 0x0008000000000000
52: 0x0010000000000000
53: 0x0020000000000000
54: 0x0040000000000000
55: 0x0080000000000000
56: 0x0100000000000000
57: 0x0200000000000000
58: 0x0400000000000000
59: 0x0800000000000000
60: 0x1000000000000000
61: 0x2000000000000000
62: 0x4000000000000000
63: 0x8000000000000000

Daniel Fischer的回答回答了关于C语言规范的问题。 至于当您按可变数量发布班次时,x86 机器上实际发生的情况,请参阅英特尔软件开发人员手册第 2B 卷,第 4-506 页:

计数被屏蔽为 5 位(或 6 位 如果在 64 位模式和 REX 中。使用 W(。计数范围限制为 0 到 31(如果为 63,则为 64 位模式和 REX。使用 W(。

因此,如果您尝试移位量大于

31 或 63(分别为 32 位和 64 位值(,硬件将仅使用移位量的底部 5 位或 6 位。 所以这段代码:

uint32_t RightShift(uint32_t value, uint32_t count)
{
    return value >> count;
}
将导致在 x86

和 x86-64 上出现RightShift(2, 33) == 1。 根据 C 标准,它仍然是未定义的行为,但在 x86 上,如果编译器将其编译为sar指令,它将在该体系结构上定义行为。 但是,您仍然应该避免编写这种依赖于特定于体系结构的怪癖的代码。

按介于

0 和类型宽度的前置数字之间的数字移动不会导致未定义的行为,但左移负数会导致未定义的行为。你会这样做吗?

另一方面,右移

负数是实现定义的,大多数编译器在右移有符号类型时会传播符号位。

不,没关系。

ISO 9899:2011 6.5.7 按位移位运算符

如果右操作数的值为负数或大于或等于提升的左操作数的宽度,则行为未定义

这里的情况并非如此,所以一切都很好,定义明确。