我正在学习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。
它出了什么问题?
感谢。
答案 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可能与上面相同。