C按位计算,并给出与-O0和-O2不同的结果

C按位计算,并给出与-O0和-O2不同的结果,c,gcc,C,Gcc,我正在使用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) { u

我正在使用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;
}
#包括
#包括
#包括
#包括
int main(int argc,常量字符**argv)
{
uint32_t value=strtol(argv[1],NULL,16);
uint32_t否定=-(int32_t)(值);
布尔符号=否定&0x8000000;
printf(“值=%X,否定=%X,符号=%X\n”,值,否定,符号);
返回0;
}
-(int32_t)(值)零件取自Bochs的
NEG_EdM()
函数;对于等效操作,DOSBox不强制转换为带符号的int

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

值=80000000,否定=80000000,符号=0

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

值=80000000,否定=80000000,符号=1

这是未定义的行为吗


据我所知,向有符号整数和无符号整数转换/从有符号整数和无符号整数转换的定义很好,按位&对无符号值转换也是如此。

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

  • strtol(“0x8000000”,NULL,16)
    返回的值取决于类型
    long
    的范围:如果类型
    long
    有32位,则返回值应为
    long\u MAX
    ,即
    2147483647
    ,而如果
    long
    更大,则返回
    2147483648
    。将这些值转换为
    uint32\u t
    不会更改值,因为两者都在
    uint32\u t
    范围内。在系统上键入
    long
    似乎有64位。您可以使用
    strtoul()
    而不是
    strtol()
    来避免这种实现定义的行为

  • 无需将中介强制转换为
    (int32\u t)
    :对无符号值求反定义良好,并且
    -0x8000000
    具有类型
    uint32\u t
    的值
    0x8000000

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

  • 使用type
    bool
    时不包括
    :程序不应编译。这是复制/粘贴错误,还是编译为C++?C99
    \u Bool
    语义在initialization语句中添加了隐式测试,但最好将其显式化并编写:

    bool sign = (negation & 0x80000000) != 0;
    
  • 最后,将
    uint32\u t
    值传递给
    %X
    转换说明符的
    printf
    。如果类型
    int
    在您的平台上少于32位,则这是不正确的。使用
    中的宏

请尝试此修改版本:

#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;
}
#包括
#包括
#包括
#包括
#包括
int main(int argc,常量字符**argv)
{
uint32_t value=strtoul(argv[1],NULL,16);
uint32_t否定=-值;
布尔符号=(否定&0x8000000)!=0;
printf(“值=%“PRIX32”,否定=%“PRIX32”,符号=%d\n”,值,否定,符号);
返回0;
}
您的不幸经历源于有符号算术溢出的未定义行为。编译器可以利用未定义的行为来实现高级优化,例如在(int i=0;i>0;i++)
中删除
for(int i=0;i>0;i++)中的结束测试,以及转换
void f(int i)等更明显但非琐碎的优化{int j=i*2/2;..
int j=i;
对于超过
0x3fffff
的值,可能表现出不同的行为

其他语言(例如:java)尝试删除未定义的行为,并完全指定两种语言的补充实现和行为,因此不会执行这些优化

标准C语言委员会似乎倾向于进行更多的优化,但在边境案件中可能会出现一些意外情况,这可能很难发现和解决。您的示例完美地说明了这个问题。

未定义行为的来源 问题的关键部分在于对
-(int32_t)值的否定

此时,
为80000016(231)。由于该值在
int32_t
中不可表示,因此转换受C 2018 6.3.1.3 3的控制,表示行为由实现定义。将80000016包装到
int32_t
模232产生−80000016

然后应用否定运算符
-
−80000016当然是80000016,但这在
int32\t
中不可表示。2该行为受C 2018 6.5的约束:

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

因此,求反具有未定义的行为。当使用
-O0
时,编译器生成简单的直接代码。这将进行包装,为输入位80000016(表示−80000016作为有符号32位整数)。使用
-O2
时,编译器将对
bool sign = - (uint32_t) x & 0x80000000u;
bool sign = - (uint32_t) x >> 31;