作者:gfree.wind@gmail.com博客:linuxfocus.blog.chinaunix.net今天发现了一个Intel逻辑左移指令shl的一个bug。逻辑左移的概念是对给定的目的操作数左移COUNT次,每次移位时最高位移入标志位CF中,最低位补零. 其中OPRD1为目的操作数, 可以是通用寄存器或存储器操作数。首先说明一下我的环境:Intel(R) Pentium(R) 4 CPU,操作系统是Fedora 12,gcc的版本是4.4.2。下面请看测试程序:#include stdio.h>int main(){#define MOVE_CONSTANT_BITS 32    unsigned int move_step=MOVE_CONSTANT_BITS;    unsigned int value1 = 1ul MOVE_CONSTANT_BITS;    printf("value1 is 0x%X\n", value1);    unsigned int value2 = 1ul move_step;    printf("value2 is 0x%X\n", value2);    return 0;}编译:[root@Lnx99 test]#gcc -g test.c -o testtest.c: In function ‘main’:test.c:8: warning: left shift count >= width of type看到这里,我想问一下大家,这两个value的值都是什么?是否相等呢?我相信会有很大一部分人会说这两个值一样,都是0.因为根据逻辑左移的概念,这个1被移了出去,低位补了32个0.所以值肯定是零。那么让我执行一下,看看吧。[root@Lnx99 test]#./testvalue1 is 0x0value2 is 0x1有些奇怪吧,为什么这样呢。让我们看看汇编代码吧。Dump of assembler code for function main:0x080483c4 : push %ebp0x080483c5 : mov %esp,%ebp0x080483c7 : and $0xfffffff0,%esp0x080483ca : push %ebx0x080483cb : sub $0x2c,%esp0x080483ce : movl $0x20,0x14(%esp)0x080483d6 : movl $0x0,0x18(%esp)0x080483de : mov $0x80484f4,%eax0x080483e3 : mov 0x18(%esp),%edx0x080483e7 : mov %edx,0x4(%esp)0x080483eb : mov %eax,(%esp)0x080483ee : call 0x80482f40x080483f3 : mov 0x14(%esp),%eax0x080483f7 : mov $0x1,%edx0x080483fc : mov %edx,%ebx0x080483fe : mov %eax,%ecx0x08048400 : shl %cl,%ebx0x08048402 : mov %ebx,%eax0x08048404 : mov %eax,0x1c(%esp)0x08048408 : mov $0x8048504,%eax0x0804840d : mov 0x1c(%esp),%edx0x08048411 : mov %edx,0x4(%esp)0x08048415 : mov %eax,(%esp)0x08048418 : call 0x80482f40x0804841d : mov $0x0,%eax0x08048422 : add $0x2c,%esp0x08048425 : pop %ebx0x08048426 : mov %ebp,%esp0x08048428 : pop %ebp0x08048429 : retEnd of assembler dump.汇编代码中红色的代码对应于unsigned int value1 = 1ul 从这些代码可以看出,对于第一个指令,gcc直接计算出了结果的值,然后将其赋给了value1,而第二个指令真正的执行了逻辑左移shl。但是为什么逻辑左移shl运算的结果是1,而不是0呢。这个逻辑左移的结果居然与循环左移ROL的结果是一样的。到此,我有点怀疑是不是编译器的问题,在生成机器码的时候,是否错误的生成了ROL对应的机器码呢。使用objdump -d test查看test的机器码。对应逻辑左移的机器码是d3 e3. 8048400:       d3 e3                   shl    %cl,%ebx为了使用循环左移ROL,只能通过修改汇编代码的方式。那么首先使用gcc -S test.c 生成汇编代码test.s, 然后修改sall    %cl, %ebx 行为roll    %cl, %ebx, 再用gcc -g test.s -o test汇编代码test.s重新生成test。再次使用objdump -d test查看test的机器码。对应循环左移的机器码是d3 c3。8048400:       d3 c3                   rol    %cl,%ebx到此我们可以确定编译器没有问题,使用的就是Intel提供的逻辑左移指令,那么为什么最终的结果与期望的不同呢。难道是Intel的bug?!我们不能轻易下这个结论。因为逻辑左移是一个很基础的指令,Intel会出现这么一个明显的bug吗?让我们去看一下Intel的指令手册吧。SAL/SAR/SHL/SHR—Shift (Continued)——32位机DescriptionThese instructions shift the bits in the first operand (destination operand) to the left or right bythe number of bits specified in the second operand (count operand). Bits shifted beyond thedestination operand boundary are first shifted into the CF flag, then discarded. At the end of theshift operation, the CF flag contains the last bit shifted out of the destination operand.The destination operand can be a register or a memory location. The count operand can be animmediate value or register CL. The count is masked to five bits, which limits the count rangeto 0 to 31. A special opcode encoding is provided for a count of 1.这下真相大白了。原来在32位机器上,移位counter只有5位。那么当执行左移32位时,实际上就是左移0位。那么这个1ul 到此,我们虽然已经知道整个儿的来龙去脉了,可是不能不说Intel的移位指令是有着陷阱的。因为在除了在Intel这个手册中说明了这个情况,在其它的汇编语言的资料中,从没有提及过这个情况。有的朋友可能说了,之前gcc已经给了一个“test.c:8: warning: left shift count >= width of type”这样的警告了啊,已经对这个情况做了提示。关于这个warning,如果代码再复杂一些,移位的个数不再是一个常量,gcc肯定是无法检测出来的。所以,当我们需要做移位处理时,一定要注意是否超出了32位(64位机则是64位)。另外,对于gcc的处理,我也有一点意见。当1ul
12-23 01:41