我正在学习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选项.
它出什么问题了?
谢谢.
简短的回答
您将以两种不同的方式评估相同的表达式 - 一次在运行时在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由于优化中断,运行时结果刚刚关闭.
一个简化的例子
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可能与上面相同.