繁体   English   中英

C 按位与 -O0 和 -O2 给出不同的结果

[英]C bitwise AND gives different result with -O0 and -O2

我正在使用 Bochs 和 DOSBox 作为参考来开发 PC 模拟器。

在模拟“NEG Ed”指令(双字的二进制补码否定)时,如果我使用-O0而不是-O2编译,我会得到不同的结果。

这是一个只有相关位的测试程序:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>

int main(int argc, const char **argv)
{
    uint32_t value = strtol(argv[1], NULL, 16);
    uint32_t negation = -(int32_t)(value);
    bool sign = negation & 0x80000000;

    printf("value=%X, negation=%X, sign=%X\n", value, negation, sign);
    
    return 0;
}

-(int32_t)(value); 部分取自 Bochs 的NEG_EdM() function; 对于等效操作,DOSBox 不会强制转换为带符号的 int。

如果您使用-O2选项使用 GCC 10 编译此程序并使用十六进制值0x80000000作为输入,您将得到错误的sign结果:

value=80000000, negation=80000000, sign=0

使用-O0编译时,结果是正确的:

value=80000000, negation=80000000, sign=1

这是未定义的行为吗?

据我所知,有符号和无符号整数的转换是明确定义的,无符号值的按位 & 也是如此。

未定义行为的来源

问题的关键部分在于否定-(int32_t)value 1

此时, value 80000000 16 (2 31 )。 由于这在int32_t中无法表示,因此转换由 C 2018 6.3.1.3 3 管理,这表示行为是实现定义的。 GCC 10.2 将其定义为模 2 N包装,其中目标宽度为N位。 将 80000000 16包装到int32_t模 2 32产生 −80000000 16

然后应用否定运算符- -80000000 16的数学否定当然是 80000000 16 ,但这在int32_t中无法表示。 2行为受 C 2018 6.5 5 约束:

如果在计算表达式期间出现异常情况(即,如果结果未在数学上定义或不在其类型的可表示值范围内),则行为未定义。

因此,否定具有未定义的行为。 当使用-O0时,编译器生成简单的直接代码。 Godbolt 显示它会生成一个否定指令,该指令会生成 output 80000000 16用于输入位 80000000 16 (将 -80000000 16表示为带符号的 32 位整数)。 当使用-O2时,编译器会对程序进行复杂的分析和转换,缺乏定义的行为使编译器可以自由地产生任何结果。 事实上, Godbolt 表明否定指令不存在 实际上,编译器“知道”取反int32_t值永远不会产生在具有定义行为的程序中设置 2 31位的结果。

优化讨论

考虑int32_t中可表示的值的范围是 -2 31到 2 31 -1。 这些的数学否定是 -(2 31 -1) 到 2 31 但是,2 31溢出,导致异常情况。 不溢出的结果范围是 -(2 31 -1) 到 2 31 -1。 因此,在具有已定义行为的程序中,只会出现这些结果,因此编译器可能会像只出现这些结果一样进行优化。 在这些结果中没有一个是 2 31位集。 因此,在具有定义行为的程序中, negation & 0x80000000始终为零,编译器可能会基于此生成代码。

使固定

看来您想要测试符号位是否会设置在使用二进制补码取反的int32_t中,即包装结果模 2 32 为此,可以使用无符号算术。 如果x是一个int32_t值或一个uint32_t ,其中包含表示此类值的位,则可以通过以下任一方式获得取反值的符号位:

bool sign = - (uint32_t) x & 0x80000000u;
bool sign = - (uint32_t) x >> 31;

脚注

1我们推断long比 32 位更宽。 Were it not, strtol("0x80000000", NULL, 16) would return LONG_MAX , per C 2018 7.22.1.4 8. That would be representable in uint32_t and int32_t , so value would be initialized to LONG_MAX , converting to int32_t would keep that value , negation将是 - LONG_MAX ,并且在程序的优化和未优化版本中, sign都将为零。

2如果int32_tint窄,则操作数将在取反之前提升为int ,并且数学结果将是可表示的。 您使用的 GCC 版本和选项并非如此,我们可以从观察结果中推断出。

您的代码中存在一些问题:

  • strtol("0x80000000", NULL, 16)返回的值取决于long类型的范围:如果long类型为 32 位,则返回值应为LONG_MAX ,即2147483647 ,而如果long更大,则返回2147483648 . 将这些值转换为uint32_t不会在uint32_t的范围内更改值。 您的系统上的long类型似乎有 64 位。 您可以使用strtoul()而不是strtol()来避免这种实现定义的行为。

  • 不需要中间转换为(int32_t) :否定无符号值是明确定义的,并且-0x80000000对于uint32_t类型的值为0x80000000

  • 此外,这种转换会适得其反,并且观察到的行为的可能原因是否定值INT32_MIN由于有符号算术溢出而具有未定义的行为。 启用优化后,编译器确定您正在提取符号,就好像通过bool sign = -(int32_t)value < 0并将此表达式简化为bool sign = (int32_t)value > 0 ,这对于除INT32_MIN之外的所有值都是正确的编译器认为任何行为都可以,因为无论如何该行为都是未定义的。 您可以在Godbolt 的 Compiler Explorer上查看代码。

  • 你使用bool类型而不包括<stdbool.h> :程序不应该编译。 这是复制/粘贴错误还是您编译为 c++? C99 _Bool语义在初始化语句中添加了一个隐式测试,但最好让它显式并编写:

     bool sign = (negation & 0x80000000);= 0;
  • 最后,将uint32_t值传递给printf以获取%X转换说明符。 如果平台上的int类型少于 32 位,则这是不正确的。 使用<inttypes.h>中的宏。

试试这个修改后的版本:

#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <inttypes.h>

int main(int argc, const char **argv)
{
    uint32_t value = strtoul(argv[1], NULL, 16);
    uint32_t negation = -value;
    bool sign = (negation & 0x80000000) != 0;

    printf("value=%"PRIX32", negation=%"PRIX32", sign=%d\n", value, negation, sign);
    
    return 0;
}

您不幸的经历源于有符号算术溢出的未定义行为。 编译器可以利用未定义的行为来实现高级优化,例如删除for (int i = 0; i > 0; i++)中的最终测试以及更明显但非平凡的优化,例如转换void f(int i) { int j = i * 2 / 2; ... void f(int i) { int j = i * 2 / 2; ...int j = i; 对于超过0x3fffffff的值,这可能会表现出不同的行为。

其他语言(即:java)尝试删除未定义的行为并完全指定二进制补码实现和行为,因此不会执行这些优化。

标准 C 语言委员会似乎支持更多的优化,但代价是边境案件中的一些意外情况,这可能很难发现和解决。 你的例子是这个问题的完美例证。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM