C中位操作的奇怪行为

时间:2013-07-20 04:07:01

标签: c bit

我正在学习C编程语言及其位运算符。 我编写了如下代码,我期望代码的结果是相同的。 但事实并非如此。

#include <stdio.h>
#define N 0

int main() {
    int n = 0;
    printf("%d\n", ~0x00 + (0x01 << (0x20 + (~n + 1))));
    printf("%d\n", ~0x00 + (0x01 << (0x20 + (~N + 1))));
    return 0;
}

我假设机器在32位上将数字表示为2的补码。 它们都必须是-1,即所有位都是1,但第一个是0,第二个是-1。 我认为除了使用变量或常量之外,两者都是完全相同的代码。

我在i5 CPU的Mac上使用gcc和选项-m32。

它出了什么问题?

感谢。

2 个答案:

答案 0 :(得分:5)

简短回答

您正在以两种不同的方式评估相同的表达式 - 一次在运行时在x86上,一次在编译时。 (我假设您在编译时已禁用优化,请参阅下文。)

答案很长

查看反汇编的可执行文件,我注意到以下内容:第一个printf()的参数是在运行时计算的:

movl   $0x0,-0x10(%ebp)
mov    -0x10(%ebp),%ecx  ; ecx = 0 (int n)
mov    $0x20,%edx        ; edx = 32
sub    %ecx,%edx         ; edx = 32-0 = 32
mov    %edx,%ecx         ; ecx = 32
mov    $0x1,%edx         ; edx = 1
shl    %cl,%edx          ; edx = 1 << (32 & 31) = 1 << 0 = 1
add    $0xffffffff,%edx  ; edx = -1 + 1 = 0

转换由x86 SHL指令执行,%cl作为运算符。根据英特尔手册:“目标操作数可以是寄存器或存储单元。计数操作数可以是立即值或寄存器CL。计数被屏蔽为5位,这将计数范围限制为0到31 。计数为1的特殊操作码编码。“

对于上面的代码,这意味着您正在转移0,因此在转移指令后保留1

相反,第二个printf()的参数本质上是由编译器计算的常量表达式,编译器不会屏蔽移位量。因此,它执行32b值的“正确”移位:1<<32 = 0然后将-1添加到该值 - 您会看到0+(-1) = -1作为结果。

这也解释了为什么你只看到一个warning: left shift count >= width of type而不是两个,因为警告源于编译器评估32位值偏移32位。编译器没有发出任何关于运行时转换的警告。

减少测试用例

以下是将您的示例简化为基本要素:

  #define N 0
  int n = 0;

  printf("%d %d\n", 1<<(32-N) /* compiler */, 1<<(32-n) /* runtime */);

打印0 1,展示转变的不同结果。

提醒

请注意,上面的示例仅适用于-O0编译代码,在编译时您没有编译器优化(计算和折叠)常量表达式。如果您使用简化的测试用例并使用-O3进行编译,那么您将从此优化代码中获得相同且正确的结果0 0

movl   $0x0,0x8(%esp)
movl   $0x0,0x4(%esp)

我认为如果更改测试的编译器选项,您将看到相同的更改行为。

注意在gcc-4.2.1(以及其他?)中似乎存在代码生成错误,由于优化中断,运行时结果刚刚关闭0 8027。< / p>

答案 1 :(得分:5)

简化示例

unsigned n32 = 32;
printf("%d\n", (int) sizeof(int));  // 4
printf("%d\n",  (0x01 << n32));     // 1
printf("%d\n",  (0x01 << 32));      // 0

你在(0x01 << n32)获得UB作为shift&gt; = int的宽度。 (看起来只有5个nb的n32参与了班次。因此转移了0。)

你在(0x01 << 32)得到一个UB,因为shift&gt; = int的宽度。 (看起来编译器用更多的位来执行数学运算。)这个UB可能与上面相同。