Intel 移位指令的陷阱转


今天发现了一个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 =  move_step;
    printf(value2 is 0x%X\nreturn 0;
}
编译:
[root@Lnx99 test]#gcc -g test.c -o test
test.c: In function ‘main’:
test.c:8: warning: left shift count >= width of type
 
看到这里,我想问一下大家,这两个value的值都是什么?是否相等呢?
相信会有很大一部分人会说这两个值一样,都是0.因为根据逻辑左移的概念,这个1被移了出去,低位补了32个0.
所以值肯定是零。
那么让我执行一下,看看吧。
[root@Lnx99 test]#./test
value1 is 0x0
value2 is 0x1
有些奇怪吧,为什么这样呢。让我们看看汇编代码吧。
  1. Dump of assembler code for function main:
  2. 0x080483c4 <main+0>: push %ebp
  3. 0x080483c5 <main+1>: mov %esp,%ebp
  4. 0x080483c7 <main+3>: and $0xfffffff0,%esp
  5. 0x080483ca <main+6>: push %ebx
  6. 0x080483cb <main+7>: sub $0x2c,%esp
  7. 0x080483ce <main+10>: movl $0x20,0x14(%esp)
  8. 0x080483d6 <main+18>: movl $0x0,0x18(%esp)
  9. 0x080483de <main+26>: mov $0x80484f4,%eax
  10. 0x080483e3 <main+31>: mov 0x18(%esp),%edx
  11. 0x080483e7 <main+35>: mov %edx,0x4(%esp)
  12. 0x080483eb <main+39>: mov %eax,(%esp)
  13. 0x080483ee <main+42>: call 0x80482f4<printf@plt>
  14. 0x080483f3 <main+47>: mov 0x14(%esp),%eax
  15. 0x080483f7 <main+51>: mov $0x1,%edx
  16. 0x080483fc <main+56>: mov %edx,%ebx
  17. 0x080483fe <main+58>: mov %eax,%ecx
  18. 0x08048400 <main+60>: shl %cl,%ebx
  19. 0x08048402 <main+62>: mov %ebx,%eax
  20. 0x08048404 <main+64>: mov %eax,0x1c(%esp)
  21. 0x08048408 <main+68>: mov $0x8048504,%eax
  22. 0x0804840d <main+73>: mov 0x1c(%esp),%edx
  23. 0x08048411 <main+77>: mov %edx,0x4(%esp)
  24. 0x08048415 <main+81>: mov %eax,(%esp)
  25. 0x08048418 <main+84>: call 0x80482f4<printf@plt>
  26. 0x0804841d <main+89>: mov $0x0,%eax
  27. 0x08048422 <main+94>: add $0x2c,%esp
  28. 0x08048425 <main+97>: pop %ebx
  29. 0x08048426 <main+98>: mov %ebp,%esp
  30. 0x08048428 <main+100>: pop %ebp
  31. 0x08048429 <main+101>: ret
  32. End of assembler dump.
汇编代码中红色的代码对应于unsigned int value1 = 1ul << MOVE_CONSTANT_BITS;蓝色的代码对应于unsigned int value2 = 1ul << move_step;
从这些代码可以看出,对于第一个指令,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位机
Description
These instructions shift the bits in the first operand (destination operand) to the left or right by
the number of bits specified in the second operand (count operand). Bits shifted beyond the
destination operand boundary are first shifted into the CF flag,then discarded. At the end of the
shift 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 an
immediate value or register CL. The count is masked to five bits,which limits the count range
to 0 to 31. A special opcode encoding is provided for a count of 1.
 
这下真相大白了。原来在32位机器上,移位counter只有5位。那么当执行左移32位时,实际上就是左移0位。
那么这个1ul << move_step就相当于1ul<<0。那么value2自然就是1了。
 
到此,我们虽然已经知道整个儿的来龙去脉了,可是不能不说Intel的移位指令是有着陷阱的。因为在除了在Intel这个手册中说明了这个情况,在其它的汇编语言的资料中,从没有提及过这个情况。有的朋友可能说了,之前gcc已经给了一个“test.c:8: warning: left shift count >= width of type”这样的警告了啊,已经对这个情况做了提示。关于这个warning,如果代码再复杂一些,移位的个数不再是一个常量,gcc肯定是无法检测出来的。所以,当我们需要做移位处理时,一定要注意是否超出了32位(64位机则是64位)。
 
另外,对于gcc的处理,我也有一点意见。当1ul<<32时,gcc自己预处理的结果与进行运算的结果不符,虽然它更符合用户的期望。但是,当用户开始使用常量时,结果是对的,一旦换成了变量,结果就不一样了。在大型的程序中,这样会让用户很难定位到问题的。
 
注意:常数的默认类型是int,为保证可移植性,在常数计算可能导致越界的场景下,需要对常数进行类型显示处理,比如:
    printf("1ul<<40 = %llx\n",1ul<<40);

转自:http://blog.chinaunix.net/uid-23629988-id-127318.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


首先GDB是类unix系统下一个优秀的调试工具, 当然作为debug代码的工具, 肯定没有IDE来的直观了. 不过, 命令行自然有命令行的有点, 当你无法是使用IDE时, gdb有时会帮上很大的忙. 下面用1个例子来说明用法吧: 建立1个目录testgdb2 1. 编写c文件test.c可以见到在2
1. C语言定义1个数组的时候, 必须同时指定它的长度.例如:int a[5]={1,2,3,4,5}; //合法int b[6]; //合法int c[]; //错误 因为没有指定长度但是下面语句是正确, 它隐形定义了数组的长度, 就是赋值元素的个数int d[] ={4,5,6,7,8,9} /
C++的auto关键字在C+⬑新标准出来之前基本上处于弃用的状态,到C+⬑新标准发布之后,auto关键字被赋予了新的意义,并且功能也变得很强大,此篇文章重点介绍auto关键字的新功能、新用法,以及在C+⬔、C+⬗、C+⬠各版本中对它的使用缺陷和限制不断地修正和增强,当然auto关
const关键字是用于定义一个不该被改变的对象,它的作用是告诉编译器和其他程序员不允许修改这个对象的值或者状态。当程序员看到使用const修饰的代码时就知道不应该修改对应对象的值,而编译器则会强制实施这个约束,任何违反这个规定的代码会在编译期间报错。它可以用于任何函数或者类之后的全局或namespa
文章浏览阅读315次。之前用C语言编过链表,这几天突然想用C++编一下链表,搞了大半天才搞出来,所以就赶紧整理一下记录下来,省的万一时间长了找不到代码哈哈。一、链表代码1、Node.h文件代码#pragma onceclass Node{public: int ID; char alph; Node* next; Node(int ID,char alph); ~Node();private:..._if(current->id==id)
文章浏览阅读219次。碰到问题就要记录下来,防止遗忘吧。文章目录一、VS中的命令行参数二、内联函数和宏三、初始化和赋值一、VS中的命令行参数今天在运行代码的时候,碰都了下面的情况: // 解析命令行参数 if (pcl::console::find_argument (argc, argv, "-h") >= 0) { printUsage (argv[0]); return 0; }..._"if (pcl::console::find_argument(argc, argv, "-f") >= 0)怎么输入参数"
文章浏览阅读1.8k次,点赞11次,收藏37次。因为自己对决策树的机制非常的好奇,所以就研究了一下决策树的ID3算法,在这也做一篇笔记记录一下过程。文章目录一、什么是决策树?二、信息增益2.1信息熵2.1.1定义2.1.2演变2.2信息增益三、ID3算法实现四、小结一、什么是决策树?这个问题是我从一开始就有的疑问,什么是决策树?在看了一些资料之后,因为没有看到书上给出具体定义,所以按照我自己的理解决策树就是通过一个个“决策”而构建的一种树状结构,而且决策树的整个处理机制非常类似于我们人类在面临决策问题时的处理机制,这也可能就是其名字的由来。决_c++id3
文章浏览阅读492次。C++ 设计模式之策略模式
文章浏览阅读683次。我也算是个C++的小白,对于C++中的谓语我第一时间就想到了C#中的委托,但两者又不尽相同,所以想写一篇笔记记录一下。文章目录一、什么是谓语?二、使用谓语一、什么是谓语?谓语是一个可调用的表达式,其返回的结果可以作为条件的值,在C++中其实就是向算法传递函数。这和C#中的委托的概念其实是一样的,都是将函数作为参数进行传递。C++标准库中的谓语主要有两类:一元谓语和二元谓语,也就是有的算法只能..._谓语句 c++
文章浏览阅读225次。又看了一遍操作符的东西,感觉之前对操作符的理解还停留在很浅的认知上(仅仅会用哈哈),所以做一下笔记来加深一下印象。文章目录一、为什么会有操作符重载?二、操作符重载作用的对象一、为什么会有操作符重载?如果要回答这个问题,我们其实应该仔细想一下如果没有操作符重载会怎样呢?这其实很容易就联想到了C语言,因为他就没有操作符重载这一说。虽然C语言中没有类class这一概念,但是他有着和类及其相似的结构..._6-6 我的朋友 - c/c++ 操作符重载分数 15作者 海洋饼干叔叔单位 重庆大学实现frie
文章浏览阅读216次。因为之前碰到了很多关于C++上的问题,现在整理并记录一下。文章目录一、引用一、引用在C++中,引用就是给对象起了另一个名字,也就是“对象别名”。感觉和什么东西很相似,仔细一想不就是类型别名“typedef”吗哈哈。它其实是和原对象形成了一种绑定的一种关系,..._vc++6.0报错:returning address of local
文章浏览阅读565次。因为一直好奇预处理器的工作机制,所以就查了查书,做一下自己看完书之后的笔记。文章目录一、预处理器的作用一、预处理器的作用_c语言预处理器作用
文章浏览阅读1.8k次,点赞3次,收藏10次。最近特别查阅了一下关于C++文件的输入/输出的资料,整理了一下就写一下笔记。文章目录一、什么是流二、什么是缓冲区三、代码实现文件IO3.1 使用文件流对象读取数据3.2重定向一、什么是流当前的计算机具有很多种设备,但是无论是哪种设备都要与数据和信息进行打交道,所以这就牵扯到设备与数据之间的I/O操作。而每种设备又有着不同的特性和操作协议,由于过于复杂,所以我们一般是不会和这些通信细节打交道的..._c++ inpath
文章浏览阅读4.8k次,点赞6次,收藏29次。因为要使用到C++的动态链接库,所以就特意网上找了一下资料实现了一下。文章目录一、lib与dll文件二、创建dll文件三、dll隐式链接四、显式链接五、小结一、lib与dll文件之前我一直以为动态链接库就是指dll文件,这也是C#给我造成的一种印象,因为在C#中建立的类库文件都是dll文件,而且只要简单引用就可以了,但是C++却并不是这样的,这可能是因为C#隐藏了一些细节的缘故吧。在C++中共有两种库模式,一种是包含lib和dll两种文件,这种情况下其中的lib文件包含了函数所在的dll文件和dl_c++调用动态链接库
文章浏览阅读973次。因为遇到了一这个操作符的问题,所以记录一下出现的问题*~*。一、问题描述二、产生原因因为也是第一次出现这个问题,所以就到网上查了一些资料和书籍,现在倒也大概理解这个错误出现的原因了。有时候举个例子可能更容易理解为啥会出现这个错误,就拿一本书中的例子来说一下,如下所示:template<class T> class NamedObject { public: NamedObject(std::string& nameVal, const T objectVal) __copy_assign报错
C语言中的单向链表可以解决数组和结构体在使用时的内存连续性问题,同时还能动态地调整长度。本文介绍了单向链表的结构和基本操作,并给出了一个简单的示例代码。
文章浏览阅读2.3k次。区分'0'、"0"、0、''_0和
文章浏览阅读5.8k次,点赞4次,收藏8次。C语言函数指针详解,微剖本质_c语言指针函数
数组指针和指针数组是代码中常见的定义形式。虽然它们的语法类似,但含义完全不同。对于一维数组而言,数组名即为首元素的地址,不需要取址即可赋值给指针。而对于二维数组,数组名代表首行元素的地址,可以看作是一个指针数组,需要使用取址操作。
文章浏览阅读297次。总结刚入门的新同学C语言编程常见的低级错误