我正在学习未定义的行为,并在没有任何明确解释的情况下偶然发现了这段代码:
#include <stdio.h>
#include <limits.h>
int foo ( int x) {
printf ("% d\n" , x ); //2147483647
printf ("% d\n" , x+1 ); //-2147483648 overflow
return ( x+1 ) > x ; // 1 but How????
}
int main ( void ) {
printf ("% d\n" , INT_MAX ); //2147483647
printf ("% d\n" , INT_MAX+1 ); //-2147483648 overflow
printf ("% d\n" , ( INT_MAX+1 ) > INT_MAX ); //0 makes sense, since -ve < +ve
printf ("% d\n" , foo(INT_MAX) ); //1
return 0;
}
在 gcc 上编译时,编译器发出警告:
<块引用>警告:“int”类型的表达式中的整数溢出导致“-2147483648”
所以,INT_MAX+1
的值显然是负数,这就解释了为什么 (INT_MAX+1) > INT_MAX
的计算结果为 0。
但是,为什么(或如何)对 (x+1) > x
中的 x = INT_MAX
求值 foo(...)
为 1?
答案 0 :(得分:13)
当程序出现 undefined behavior 时,C 标准不会预测程序将做什么。程序可能会崩溃,可能会输出奇怪的结果,或者可能看起来工作正常。
事实上,编译器通常会在假设程序不包含未定义行为的情况下工作。
在这个表达式的情况下:
( x+1 ) > x
鉴于 x
的类型为 int
,编译器知道有符号溢出是 UB 并且在它不会发生的假设下工作。考虑到这一点,x
在此表达式可能为假的情况下没有值,因此编译器可以优化该表达式并将其替换为值 1。
当我在 gcc 4.8.5 下运行这个程序时,我用 -O0
和 -O1
得到以下结果:
2147483647
-2147483648
0
2147483647
-2147483648
0
以下带有 -O2
和 -O3
:
2147483647
-2147483648
0
2147483647
-2147483648
1
然后在后一种情况下查看 foo
的程序集:
foo:
.LFB11:
.file 1 "x1.c"
.loc 1 4 0
.cfi_startproc
.LVL0:
pushq %rbx // first call to printf
.cfi_def_cfa_offset 16
.cfi_offset 3, -16
.loc 1 5 0
movl %edi, %esi
.loc 1 4 0
movl %edi, %ebx
.loc 1 5 0
xorl %eax, %eax
movl $.LC0, %edi
.LVL1:
call printf
.LVL2:
.loc 1 6 0 // second call to printf
leal 1(%rbx), %esi
movl $.LC0, %edi
xorl %eax, %eax
call printf
.LVL3:
.loc 1 8 0 // return value
movl $1, %eax
popq %rbx
.cfi_def_cfa_offset 8
.LVL4:
ret
.cfi_endproc
我们可以看到这正是编译器所做的:它优化了比较并始终返回 1。
这说明了编译器如何利用未定义的行为来应用各种优化。
答案 1 :(得分:0)
在编写标准时,传统架构的编译器通常会以环绕二进制补码的方式执行整数运算,但有时做其他事情可能更有用。举几个例子:
如果已知程序不会故意导致整数溢出,那么在溢出时设置实现陷阱会比让它输出表面上有效但错误的输出更糟糕。
即使在普通平台上,有时也可以像使用比指定类型更宽的类型来执行算术。例如,在 8086 上,乘法指令将采用两个 16 位操作数并产生一个 32 位结果,因此在执行像 int32a=int16a*int16b+int32b;
这样的计算时,保持乘法的 32 位结果将比使用符号扩展指令将结果的低 16 位提升为 32 位。此外,该抽象模型将允许简化多种表达式,例如将 (x*30/15)
替换为 (x*2)
,或者(如示例中所示)将 x+y > x
替换为 y > 0
.
与其试图猜测实现处理整数溢出可能有用的所有方式,或者冒着阻止实现以客户认为最有用的任何方式处理整数溢出的风险,标准允许实现选择他们认为的任何方法觉得最有用。 gcc 的作者认为,处理整数溢出最有用的方法是使用它来产生不受正常因果关系约束的扩展推理。
考虑,例如:
unsigned arr[32771];
unsigned mul_mod_32768(unsigned short x, unsigned short y)
{
/* Note that the authors of the Standard specified that the multiply
here happens as signed, because--according to the Rationale--they
expected that commonplace implementations would process signed and
unsigned math identically in cases like this! */
return (x * y) & 0x7FFFu;
}
void test(unsigned short n)
{
unsigned total=0;
unsigned short s2=65535;
for (unsigned short i=32768; i < n; i++)
{
total += mul_mod_32768(i, 65535);
}
if (n < 32770)
arr[n] = total;
}
在优化级别 2 或 3 时,gcc 将为 test()
生成完全等效于:
void test(unsigned short n)
{
arr[n] = 0;
}
如果 n 为 32768 或更少,则循环根本不会运行,total 将为零,并且 total 将存储到 arr[n] 中。如果 n 为 32769,则循环将运行一次,将总数加 0,然后将其存储到 arr[n] 中。如果 n 为 32770 或更大,则标准不会强加任何要求,因此 gcc 会像处理其他情况一样处理这些情况,盲目地将零存储到 arr[n] 中。
该标准故意没有试图禁止专门用于特定狭隘目的的实现,其行为方式会使它们不适用于许多其他目的。此处 gcc 的行为可能适用于专门处理来自可信赖来源的数据的程序,但这并不意味着它应该被视为适用于其他任何事物。不幸的是,clang 和 gcc 寻求处理的语言与 C 标准委员会被特许描述的语言大不相同。