考虑以下计划。
#include <stdio.h>
int negative(int A) {
return (A & 0x80000000) != 0;
}
int divide(int A, int B) {
printf("A = %d\n", A);
printf("negative(A) = %d\n", negative(A));
if (negative(A)) {
A = ~A + 1;
printf("A = %d\n", A);
printf("negative(A) = %d\n", negative(A));
}
if (A < B) return 0;
return 1;
}
int main(){
divide(-2147483648, -1);
}
如果在没有编译器优化的情况下编译它,它会产生预期的结果。
gcc -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 1
使用编译器优化进行编译时,会产生以下错误输出。
gcc -O3 -Wall -Werror -g -o TestNegative TestNegative.c
./TestNegative
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 0
我正在运行gcc version 5.4.0
。
我是否可以在源代码中进行更改,以防止编译器在-O3
下生成此行为?
答案 0 :(得分:84)
-2147483648
没有按照您的想法行事。 C没有负常数。包括limits.h
并使用INT_MIN
代替(几乎每个INT_MIN
定义在两台补充机器上将其定义为(-INT_MAX - 1)
,这是有充分理由的。)
A = ~A + 1;
调用未定义的行为,因为~A + 1
导致整数溢出。
它不是编译器,而是你的代码。
答案 1 :(得分:44)
编译器将A = ~A + 1;
语句替换为单个neg
指令,即此代码:
int just_negate(int A) {
A = ~A + 1;
return A;
}
将编译为:
just_negate(int):
mov eax, edi
neg eax // just negate the input parameter
ret
但是编译器也很聪明地意识到,如果A & 0x80000000
在否定之前是非零,那么必须在否定之后为零,除非你依赖于undefined行为即可。
这意味着第二个printf("negative(A) = %d\n", negative(A));
可以“安全”优化为:
mov edi, OFFSET FLAT:.LC0 // .string "negative(A) = %d\n"
xor eax, eax // just set eax to zero
call printf
我使用在线godbolt compiler explorer检查程序集以进行各种编译器优化。
答案 2 :(得分:17)
详细解释这里发生的事情:
在这个答案中我假设long
是32位而long long
是64位。这是最常见的情况,但不能保证。
C没有带符号的整数内容。 -2147483648
实际上是long long
类型,您可以在其上应用一元减号运算符。
在检查2147483648
是否适合后,编译器会选择整数常量的类型:
int
内?不,它不能。 long
内?不,它不能。long long
内?是的,它可以。因此整数常量的类型将为long long
。然后在long long
上使用一元减号。long long
显示给期望int
的函数。一个好的编译器可能在这里警告。您强制将隐式转换为较小的类型(&#34;左值转换&#34;)
但是,假设2的补码,值-2147483648
可以放在int
内,因此转换不需要实现定义的行为,否则就是这种情况。 下一个棘手的部分是使用negative
的函数0x80000000
。这不是int
,也不是long long
,而是unsigned int
(see this的解释)。
将您通过的int
与unsigned int
进行比较,&#34;通常的算术转换&#34; (see this)强制隐式转换为int
到unsigned int
。在这种特定情况下,它不会影响结果,但这就是为什么gcc -Wconversion
用户在这里得到一个很好的警告。
(提示:已启用-Wconversion
!这有助于捕捉微妙的错误,但不是-Wall
或-Wextra
的一部分。)
接下来,您执行~A
,这是值的二进制表示的按位反转,最后是值0x7FFFFFFF
。事实证明,这与32位或64位系统上的INT_MAX
值相同。因此0x7FFFFFFF + 1
给出一个有符号整数溢出,导致未定义的行为。这就是程序行为不端的原因。
Cheekily,我们可以将代码更改为A = ~A + 1u;
,突然一切都按预期工作,再次因为隐式整数提升。
经验教训:
在C中,整数常量以及隐式整数提升非常危险且不直观。他们可以巧妙地巧妙地改变程序的含义并引入错误。在C中的每个操作中,您需要考虑所涉及的操作数的实际类型。
使用C11 _Generic
可能是查看实际类型的好方法。例如:
#define TYPE_SAFE(val, type) _Generic((val), type: val)
...
(void) TYPE_SAFE(-2147483648, int); // won't compile, type is long or long long
(void) TYPE_SAFE(0x80000000, int); // won't compile, type is unsigned int
保护自己免受这类错误的良好安全措施是始终使用stdint.h并使用MISRA-C。
答案 3 :(得分:13)
您依赖于未定义的行为。对于32位有符号整数,0x7fffffff + 1
导致有符号整数溢出,这是根据标准的未定义行为,所以任何事情都会发生。
在gcc中,您可以通过传递-fwrapv
强制环绕行为;仍然,如果你无法控制标志 - 更一般地说,如果你想要一个更便携的程序 - 你应该在unsigned
整数上做所有这些技巧,这是标准要求的整数(并且有用于按位运算的定义良好的语义,与有符号整数不同。)
首先将int
转换为unsigned
(根据标准定义,产生预期结果),做你的东西,转换回int
- 实现定义(≠undefined) )对于大于int
范围的值,但实际上由每个编译器工作在2的补码中来定义&#34;正确的事情&#34;。
int divide(int A, int B) {
printf("A = %d\n", A);
printf("negative(A) = %d\n", negative(A));
if (negative(A)) {
A = ~((unsigned)A) + 1;
printf("A = %d\n", A);
printf("negative(A) = %d\n", negative(A));
}
if (A < B) return 0;
return 1;
}
您的版本(在-O3):
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 0
我的版本(在-O3):
A = -2147483648
negative(A) = 1
A = -2147483648
negative(A) = 1