C中位操作的奇怪行为

lby*_*san 1 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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

它出什么问题了?

谢谢.

Jen*_*ens 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
Run Code Online (Sandbox Code Playgroud)

该移位由作为其运算符的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 */);
Run Code Online (Sandbox Code Playgroud)

这些印刷品0 1展示了不同的转变结果.

提醒一句

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

movl   $0x0,0x8(%esp)
movl   $0x0,0x4(%esp)
Run Code Online (Sandbox Code Playgroud)

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

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


chu*_*ica 5

一个简化的例子

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

你得到UB in (0x01 << n32)shift> = int的宽度.(看起来只有5升的n32参与了班次.因此转移了0.)

你得到一个UB in (0x01 << 32)shift> = int的宽度.(看起来编译器用更多位执行数学运算.)这个UB可能与上面相同.

  • 现代处理器仅使用少数LSBits来确定移位指令的移位计数.例如奔腾"计数被掩盖为五位".BITD 8088(一个8位机器)使用所有8位进行移位,因此可能会将寄存器移位255次并占用255个时钟周期.这造成了理论上错误的中断响应时间,因为在中断被确认之前可能需要_long_时间才能完成一次移位.以下处理器80186仅使用低位.这是用于区分这两个非常相似的处理器的hack. (2认同)