一个移位操作引发的程序Bug

最近在开发一个有关IPV6的项目,碰到一个非常奇怪的问题,在计算IPV6的子网掩码的时候,网络前缀为8,16,48,80,112能计算正确,当网络前缀为32,64,96计算错误。这个问题花了近一天的时间定位和修改。计算IPV6子网掩码的函数如下:

#include<stdio.h>
#include<string.h>
#define MAX_IP_SEG 4
class CIPV6Address
{
public:
        CIPV6Address()
		{
			memset(m_uiIPSeg, 0, sizeof(m_uiIPSeg));
		};
		void Output(unsigned int uiPrefixLen)
		{
			printf("PrefixLen_%u=%08x:%08x:%08x:%08x\n", uiPrefixLen,m_uiIPSeg[0],m_uiIPSeg[1],m_uiIPSeg[2], m_uiIPSeg[3]);

		}
		void fillNetMask(unsigned int uiPrefixLen)
		{
			unsigned int uiLoc=uiPrefixLen/32;
			unsigned int uiBits=uiPrefixLen%32;
			for(unsigned int i = 0;i < uiLoc; ++i)
			{
                m_uiIPSeg[i] = 0xFFFFFFFF;
			}
			if (uiPrefixLen < 128)
			{
				m_uiIPSeg[uiLoc] = (0xFFFFFFFF<<(32-uiBits));
 			}
			Output( uiPrefixLen);
		};
private :
	unsigned int m_uiIPSeg[MAX_IP_SEG];
};
int main()
{
	CIPV6Address addr;
	addr.fillNetMask(16);
	addr.fillNetMask(32);
        addr.fillNetMask(48);
	addr.fillNetMask(64);
        addr.fillNetMask(80);
        addr.fillNetMask(96);
	return 0;
}

上面程序的输出如下:

可以看到网络前缀为32,64,96的子网掩码计算错误。到底是什么原因导致了这个结果呢?开始怀疑是算法出现了问题,通过检查发现算法没有问题,于是心中充满了各种疑惑,然后写了一个小程序来进行测试。如下:

大家可以不看下面,猜猜上面程序的输出是什么?

通过运行发现,两句打印的结果不同,第一个uiResult是一个非常大的值,而uiRes是0。这里唯一的差别是前面使用变量uiBits,而uiBits的值为0,下面是直接使用0,然后进行移位。如下:

输出如下:uiResult=4294967295

uiRes=0

而后者是我们预期的结果, 这也就是函数fillNetMask计算结果错误的直接原因,但是为什么会出现这种情况呢?

接下来看看两种情况的汇编指令:

第一情况:

unsigned int uiBits=0;

011E344E  mov         dword ptr [uiBits],0

unsigned int uiResult = (0xFFFFFFFF<<(32-uiBits));

mov         ecx,20h

011E345A  sub        ecx,dword ptr [uiBits]

011E345D  or         eax,0FFFFFFFFh

011E3460  shl         eax,cl

011E3462  mov        dword ptr [uiResult],eax

printf("%u\n",uiResult);

011E3465  mov        esi,esp

011E3467  mov        eax,dword ptr [uiResult]

011E346A  push       eax

011E346B  push       offset string "%u\n" (11E5C18h)

011E3470  call       dword ptr [__imp__printf (11E82B8h)]

011E3476  add        esp,8

011E3479  cmp         esi,esp

011E347B  call        @ILT+460(__RTC_CheckEsp) (11E11D1h)

从上面011E3460  shl         eax,cl 可以看出,移位指令向左移动了cl位,这个cl是移位操作数的低4位寄存器。32的低四位为0,其实没有向左移。

第二种情况:

unsigned int uiResult =(0xFFFFFFFF<<(32-0));

00BE344E  mov        dword ptr [uiResult],0

printf("%u\n",uiResult);

00BE3455  mov        esi,esp

00BE3457  mov        eax,dword ptr [uiResult]

00BE345A  push       eax

00BE345B  push       offset string "%u\n" (0BE5C18h)

00BE3460  call       dword ptr [__imp__printf(0BE82B8h)]

00BE3466  add        esp,8

00BE3469  cmp        esi,esp

00BE346B  call        @ILT+460(__RTC_CheckEsp) (0BE11D1h)

通过观察,发现上面的汇编指令中没有shl移位操作指令,编译器在编译阶段发现unsigned int 向左移动32位,可能会溢出,优化了该操作,直接将结果赋值为0.,如下这条赋值语句。

00BE344E  mov        dword ptr [uiResult],0

这就导致了上面两者的结果不同。同时第二种情况编译的时候也会出现编译告警,提示移位操作大于数据类型的最大长度。

由此可以看出,为了保证移位操作的正确性,应该保证移位个数不能大于等于数据类型的最大位数,如果出现这种情况,可能会得到意向不到的结果。

修改方式

由于是溢出导致的结果错误,立马想到的是采用如下下的方式修改

上面的结果会是我们的预期吗?按照我们上面的理论,应该是正确的,我们来看看运行的结果是多少。

编译运行:

发现上面的结果也不是我们预期的,真的好纠结,难道和编译器的行为有关?或者和操作系统有关?或者上面总结的理论错误?各种猜测。 起初自己各种尝试,都得不到正确的结果,后来一个同事跟我说,你编译的是32位程序还是64位,因为32位的程序unsigned long和unsigned int都是四个字节。而g++默认的编译方式是32位的,为了验证这个想法,我以64位的方式进行编译。

g++ -m64 shift.c –o shift

./shift

uiResult=0

发现运行结果正确。

主要原因是在32位程序sizeof(unsigned int)=sizeof(unsignedlong)=4,向左移位32,都可能会溢出。

而64位平台,sizeof(unsigned long)>sizeof(unsignedint)。

为了保证在不同平台上能运行正确,我没有选择上面的修改方式,而是选用了下面的修改方式。

voidfillNetMask(unsigned intuiPrefixLen)
           {
                 unsigned intuiLoc=uiPrefixLen/32;
                 unsigned intuiBits=uiPrefixLen%32;
                 for(unsigned int i = 0;i < uiLoc; ++i)
                 {
                    m_uiIPSeg[i] = 0xFFFFFFFF;
                 }
                 if (uiPrefixLen < 128)
                 {
                        if(0 == uiBits)
                        { m_uiIPSeg[uiLoc] = 0;}
                        else
                        { m_uiIPSeg[uiLoc] =(0xFFFFFFFF<<(32-uiBits));
                         }
                 Output(uiPrefixLen);
           };

本文总结一句话:在进行移位操作时,移位的位数不能超过数据类型的最大位数,同时也不能对负数进行移位。同时需要注意32位和64位平台数据类型的差异。

时间: 2024-08-05 04:58:32

一个移位操作引发的程序Bug的相关文章

为什么我没有拔出钥匙 ——开锁引发的程序bug解决方案的思考

http://blog.csdn.net/wojiushiwo987/article/details/8851204为什么我没有拔出钥匙 ——开锁引发的程序bug解决方案的思考 今天中午回宿舍的时候,舍友S开完锁以后,钥匙在锁槽里拔不出来了.以前只是锁不怎么好用,左旋右旋活几下1分钟之内就能打开了.这次,的确可能卡的比较紧吧,室友S旋转了好久没有打开.然后,我接过来弄了好久(几乎同样的方法)也没有打开.此时室友S告诉我,他上午给老师屋开门,没有打开,钥匙断在里面了,刚报修了.此时,我的第一反应就

一个小小的程序Bug竟值400亿?

事件背景 日本公司J-Com在首次公开上市的日子就爆炸式地损失了超过400亿日元的天价损失,虽然日元那面额画得跟冥币似的,400亿日元也还是相当值些银子滴(按照当时的汇率,约为人民币27亿元). 事件的大致经过是由于一位操作员在离开盘还有几分钟的时候接到了一位客户"以61万日元的价格,卖出1股J-Com的股票"的委托,而田中君在接到委托后在交易终端上错误地输入了"以每股1日元的价格,卖出61万股". 至此,大家可想而知,事件继续发展下去会是怎样的灾难.但是,幸运的是

如何成为一个牛逼的程序猿

这个题目的噱头太大,要真的写起来, 足够写一本书了. 本人是过来人, 结合自身的体会和大家交流一下,希望新人能少走弯路. 每个人的情况不一样,我下面的描述可能并不适合每一个看到这篇文章的人. 一.C/C++语言 如果你的基础很差, 建议不要一开始就学C++语言,从C开始学起,对程序有个初步的认识,循序渐进.C语言的书嘛,先买一本 300 页以内的,把书中的每一个例子都通过键盘敲打进去到 Visual studio里面去,然后单步执行,逐行调试去看各个变量的值.或者自行添加一些printf语句去输

程序Bug导致了天大的损失,要枪毙程序员吗?

转自 http://www.cocoachina.com/programmer/20160331/15835.html 号外!号外!走过,路过,不要错过!日本 IT 业的狗血八卦继续独家放送啦!! 2015 年 9 月 3 日,随着东京最高法院驳回瑞穗证券的上诉,维持二审的原判结果,一个长达 10 年的诉讼终于画下了句号.这个判例将对 IT 行业产生深远的影响:如果程序的 bug 导致了巨大的经济损失,应该由谁来承担?用户?运营商?还是系统开发商? bug:计算机程序里的错误 今天故事的主角是,

一个简易的日志程序

在程序开发过程中,我们经常要记录各种操作信息以及系统信息到日志中,以便于以后查找相关记录或者在遇到程序出现错误时查找错误的原因.一般日志存储于两种介质中:一.存储入数据库,另一种存储于文本文档中.我们最常用的插件有log4.net等.但是对我们日常的小程序来说,它可能过重,所以我自己在自己的开发中写了一个简单的小程序以适应小程序的需要. 为方便自己以后查找,记录代码如下: 1 using System; 2 using System.Collections.Generic; 3 using Sy

关于程序员如何减少程序Bug的若干建议

毫无疑问,程序员是善于思考问题的一族.一个程序的编写都是通过:思考.设计.编写.调试.测试以及运行这些基本的阶段. 但大部分程序员都有一个问题就是不太愿意测试自己的代码.他们草草的调式完成以后就认为工作结束,测试那是测试人员的工作. 按照理论上,如果代码存在问题,那么测试人员和最终的用户肯定可以发现这些 BUG ,而等待哪个时候再返回来查找问题到底错在什么地方确实代价不小,其代价有: 1. 影响了程序员自己的声誉 2. 影响了产品的质量 3. 影响了客户的信任度 4. 这个时候再 DEBUG 难

记一个界面刷新相关的Bug

今天遇到一个比较有意思的bug, 这里简单记录下. Bug的症状是通过拖拉边框把我们客户端主窗口拖小之后,再最大化,会发现窗口显示有问题, 看起来像是刷新问题, 有些地方显示的不对了. 这里要说明的是我这里的主窗口是非常复杂的窗口, 里面集成了很多组件(cpmponent),有很多层的子窗口. 这个问题只有在特定条件下才会发生, 正常情况下都是好的. 遇到这种问题,我们怎么处理? 首先当然是观察症状, 究竟是刷新问题, 还是Layout出错了. 我们可以通过Spy++查看窗口层次是不是正确, 窗

一个贴子引发的对回调的思考

一个贴子引发的对回调的思考 网上看到一个贴子:http://topic.csdn.net/u/20080728/20/d60f719a-c103-44b8-8d0c-bc1c818b768a.html 觉得蛮有意思,在学习的工程中又引申出不少东西,真有趣!! 定义在类中方法之外的内部类分为实例内部类和静态内部类. 实例内部类自动持有外部类的实例的引用,即可以访问外部类的所有变量: 静态内部类可以直接访问外部类的静态成员: 定义在方法中的内部类叫局部内部类,该类只能访问被final修饰的局部变量和

程序bug导致了天大的损失,要枪毙程序猿吗?

程序bug导致了天大的损失,要枪毙程序猿吗? 作者: 雷子  发布时间: 2016-03-24 10:34  阅读: 33465 次  推荐: 63   原文链接   [收藏] 文/雷子,来源/公众号:东京 IT 人 号外!号外!走过,路过,不要错过!日本 IT 业的狗血八卦继续独家放送啦!! 2015 年 9 月 3 日,随着东京最高法院驳回瑞穗证券的上诉,维持二审的原判结果,一个长达 10 年的诉讼终于画下了句号.这个判例将对 IT 行业产生深远的影响:如果程序的 bug 导致了巨大的经济损