[英]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_t
比int
窄,则操作数将在取反之前提升为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.