发现的过程十分曲折,在这里不做赘述。为了说明问题直接上代码。
#include <iostream>
int main() {
int off = 3;
int64_t a = (1 << off);
off = 35;
int64_t b = (1 << off);
if (a == b) {
std::cout << "There is a bug.\n";
}
return 0;
}
这段代码乍看上去是不会打印if判断中的字符串的,但是实验后就会发现,“There is a bug”的字样清晰的打印了出来。这是为什么呢?
为了找到原因,将这段代码的汇编拿出来看下。
movl $3, -20(%rbp)
movl -20(%rbp), %ecx
movl $1, %eax
sall %cl, %eax
cltq
movq %rax, -16(%rbp)
movl $35, -20(%rbp)
movl -20(%rbp), %ecx
movl $1, %eax
sall %cl, %eax
cltq
movq %rax, -8(%rbp)
movq -16(%rbp), %rax
cmpq -8(%rbp), %rax
jne .L11
movl $.LC0, %esi
movl $_ZSt4cout, %edi
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
我们看到,汇编在进行左移操作时,是将移动的位数存放在CL寄存器中,然后使用sall左移指令。这里特别说明下寄存器的名称与实际寄存期间的对应关系:RAX是一个64bit寄存器,EAX表示RAX的低32bit,是一个32bit寄存器,AX表示EAX的低16bit,是一个16bit寄存器,AH、AL分别表示AX的高8bit与低8bit,是两个8bit寄存器。RBX、RCX等也是同样的道理。
可是,为什么好好的数字35,放到了寄存器里面就变成和数字3一样了呢?
在网上查阅了很多资料,最终结合Intel的时令集手册和实验发现问题原来出在指令sall上面。代码中由于1没有指明类型,编译器默认当作32bit处理,对32bit的数进行左移,会使用sall(对于小于32bit的数也是使用sall,但对于64bit数字会使用salq,后面说明),sall指令有一个不为人知的小秘密,就是他会将存放在CL寄存器中的8bit数据进行一个低5bit的Mask。也就是说虽然存进去的是35,但是sall只看低5bit,35&0x1F=3。问题就出在这里。
那如果是64bit的数字呢?实验证明,对64bit的数据进行左移,编译器会使用salq指令,这个指令虽然强,但是Intel也只是把mask的位数由5bit调整为了6bit。如果位移的位数超过了64,一样会出现bug。
这告诉我们,使用位移操作的时候,移动的位数一定不要超过该数字类型的bit位的数量,否则小bug会带来巨大的损失,最关键是还很难定位。