如何防止gcc优化器产生错误的位操作?

时间:2018-02-13 07:50:15

标签: c gcc compiler-optimization

考虑以下计划。

#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下生成此行为?

4 个答案:

答案 0 :(得分:84)

  1. -2147483648没有按照您的想法行事。 C没有负常数。包括limits.h并使用INT_MIN代替(几乎每个INT_MIN定义在两台补充机器上将其定义为(-INT_MAX - 1),这是有充分理由的。)

  2. A = ~A + 1;调用未定义的行为,因为~A + 1导致整数溢出。

  3. 它不是编译器,而是你的代码。

答案 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 intsee this的解释)。

    将您通过的intunsigned int进行比较,&#34;通常的算术转换&#34; (see this)强制隐式转换为intunsigned 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