在做CSAPP的datalab发现的一个问题,代码如下

int isTmax(int x)
{
int y = x + 1;
int z = y + y;
// printf("y = %d
", y);
// printf("z = %d
", z);
return (!z) (!!y);
}

int main()
{
printf("isTmax = %d
", isTmax(2147483647));
return 0;
}

当我注释掉printf时,返回值是0,当我放开注释后,

y = -2147483648

z = 0

isTmax = 1

这就是我预想的情况,应该返回1的,不知道为什么会出现这种情况。

使用的编译选项是gcc -O -Wall -m32 -lm -o,编译环境是虚拟机的debian 10 64位。

我又把它放到VS2019运行,即使没有注释也是返回的1和我预想的一样。


有符号数溢出是UB,所以我们直接通过编译器生成的汇编来看看不同编译器的处理逻辑吧。

TL;DR:

gcc 10.1 -O2 -m32,注释掉 printf:

输出:

isTmax = 0

可以看到这个函数直接被优化成了 return 0,因为在编译器在假定有符号数不会溢出的情况下,认为「一个非0有符号数数的两倍是0」这种情况是不会发生的,所以返回了 0。

gcc 10.1 -O2 -m32,不注释掉 printf:

输出:

y = -2147483648
z = 0
isTmax = 1

可以发现因为 printf 需要输出所以求值是必要的,然后 gcc 选择了老老实实用前面求出的结果算一遍,所以结果是 1。

(实际上和-m32/-m64没有关系,64位的编译结果是一样的)


再来看 clang。注释掉 printf 的结果是一样的(输出 0),这里就不再复述了。

clang 10.0.0 -O2 -m32,不注释掉 printf:

输出:

y = -2147483648
z = 0
isTmax = 0

可以发现 clang 这里与 gcc 的选择有所不同,在完成 printf 所需的计算之后依然是直接返回了 0。


最后是 msvc。

msvc 19.24 /O2:

输出:

isTmax = 1

可以发现即使注释了 printf,msvc 也选择老老实实算了一遍,结果也是预料之中的 1。不注释 printf 的结果也是显而易见的。

总之在发生UB的情况下,不同编译器产生的结果有所不同是很正常的,不要太在意啦。


到godbolt上测试了一下,gcc8.1开始,-O就能复现错误,而在此之前,-O2才能复现错误。

https://godbolt.org/z/4MLJE-?

godbolt.org

既然是优化的问题……那就比较容易解释了:

有符号整数溢出的结果是undefiend behavior,虽然平时我们都认为有符号整数是补码 ,但标准并没有规定(C++20开始限定是补码)。

基础类型 - cppreference.com?

zh.cppreference.com

所以在人看来,2147483647(0x7fffffff)+1=-2147483648(0x80000000)。

但在开了优化的编译器看来,有符号整型溢出是undefined,2147483647(0x7fffffff)+1=0。

不过有意思的是,clang和gcc就算加了-std=c++2a也是一样优化到0。不是说从gcc9和clang9开始就支持P1236R1了嘛?


不知道,不过我猜,就算加了m32,可能优化部分算常数折叠没有严格根编译结果吻合。


有符号数运算溢出是未定义行为吧?

那编译器输出什么甚至编译出的程序是格式化硬碟都是符合标准的(


有点意思,试了一下是 -O 的锅:

可以看到整个 isTmax 被优化了。使用 -O0 的话结果是 1:

最早出现这个现象的是 gcc 8,7.5 的输出还是 1:

C++ 的整数溢出是 UB,所以在函数里计算时需要使用更多位的整数存储中间结果,或者换一门对溢出行为有明确定义的语言。


被常数优化了:

108 0000000000400522 &:
109 400522: b8 00 00 00 00 mov $0x0,%eax
110 400527: c3 retq


安卓也是基于 linux 系统,感兴趣的可以下个 Termux ,记得哪个 lab 会有报错,神奇,可移植性有点高


gcc 加参数-m 32 会让你的程序用32位进行编译,参数溢出了,具体参数是多少你可以把y打出来看看


推荐阅读:
相关文章