1

我正在学习 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 上使用了带有选项 -m32 的 gcc。

它出什么问题了?

谢谢。

4

2 回答 2

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

移位由 x86SHL指令执行,%cl作为其运算符。根据英特尔手册:“目标操作数可以是寄存器或内存位置。计数操作数可以是立即数或寄存器 CL。计数被屏蔽为 5 位,这将计数范围限制为 0 到 31。一个特殊的为计数 1 提供操作码编码。”

对于上面的代码,这意味着您正在移动0,因此1在 shift 指令之后保留原位。

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

这也解释了为什么您只看到一个warning: left shift count >= width of type而不是两个,因为警告源于编译器评估 32b 值移位 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由于优化中断而刚刚关闭。

于 2013-07-20T04:29:26.187 回答
5

一个简化的例子

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

你得到 UB(0x01 << n32)作为 int 的 shift >= width。(看起来只有 5 个 lsbits 的 n32 参与了移位。因此移位为 0。)

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

于 2013-07-20T04:31:55.603 回答