32位和64位无符号数相减的结果是什么?

12

情况:
我有一段代码,当编译为32位时可以正常工作,但在使用gcc 4.6编译为64位后失败了。经过确定问题并阅读标准后,我仍然不太明白为什么它能在32位下工作。希望有人能解释一下发生了什么。

代码(被简化并缩减到有趣的部分):

// tbl: unsigned short *, can be indexed with positive and negative values
// v: unsigned int
// p: unsigned char *
tmp = tbl[(v >> 8) - p[0]]; // Gives segfault when not compiled with -m32

使用-m32编译代码可以正常运行,没有使用-m32编译代码会导致段错误。导致段错误的原因是当编译成64位时,(v >> 8) - p[0]被解释为一个无符号整数,对于"负"结果将会偏差很大。

根据这个问题,C99标准规定如下:
6.2.5c9: 包含无符号操作数的计算永远不会溢出,因为不能由生成的无符号整数类型表示的结果将被模除比生成类型大一的数字。

从这里看来,unsigned 减去 unsigned 将始终产生一个 unsigned 输出,这与64位情况的结果一致。但在32位的情况下,似乎并非如此,这让我感到非常奇怪。

有人能解释一下32位情况发生了什么吗?


2
需要一个ISO委员会才能称之为“永不溢出”。 - Hans Passant
2
@HansPassant:无论是ISO委员会还是其他人,在编写标准文件时都应该精确使用技术术语。溢出的技术定义是计算结果的大小超过了所选类型的表示范围。在C中,整数溢出是未定义行为,但无符号整数不会溢出——这是一个非常重要的澄清,否则有些人可能会认为无符号算术可以引发信号等。在某些平台(MIPS)上,有符号溢出会引发信号(崩溃)。 - Dietrich Epp
1个回答

14

两种情况下都会得到一个非常大的数字,因为 unsigned int 会绕过并重新开始计数,但在32位情况下,指针算术 也会 绕过并重新开始计数,因此它们相互抵消了。

为了进行指针算术,编译器将数组索引提升为与指针相同的宽度。 因此,对于带有32位指针的 unsigned,您将获得与带有32位指针的 int 相同的结果。

例如:

char *p = (char *) 0x1000;

// always points to 0x0c00
// in 32-bit, the index is 0xfffffc00
// in 64-bit, the index is 0xfffffffffffffc00
int r = p[(int) -0x400]; 

// depends on architecture
// in 32-bit, the index is 0xfffffc00 (same as int)
// in 64-bit, the index is 0x00000000fffffc00 (different from int)
int r = p[(unsigned) -0x400];

实际上,数组索引被(有效地)提升为 ptrdiff_t,它通常与数据指针具有相同的宽度。请参见6.5.6:8、:9。 - ecatmur
1
@ecatmur:我已经阅读了n1256的那一部分,没有地方提到ptrdiff_t。额外挑战:你能否命名一个32位或64位架构,在该架构中sizeof(ptrdiff_t) != sizeof(void *) - Dietrich Epp
好的观点;我假设指针减法的结果类型(6.5.6:9)已经包含了这一点,但实际上并没有。额外挑战-无。 - ecatmur
很棒的答案!绝对没有考虑指针算术的环绕。 - Leo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接