让我印象深刻并很喜欢的一个Bug

发表于:2016-12-16 11:41

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:张书情    来源:51Testing软件测试网采编

  那是在2013年11月初,我和朋友在准备参加一年一度的美国计算机协会(ACM)主办的国际大学生程序设计竞赛(ICPC)区域赛,选择的项目是各种算法和数据结构。据我了解,跳表并不经常用于编程比赛,但是它是一种用户维护有序元素的数据结构。我们认为将跳表添加到自己的库也许是个好主意。(注意:我们选择的编程语言C++已经通过其标准库,提供了平衡二进制搜索树,但是不支持扩增(augmentation),编程比赛中经常需要用到扩增。)
  于是,我们晚上就开始行动,将跳表添加到自己的库。我的朋友找到了旧的实现方法,将低级C编写的程序转换成更易使用的高级C++。完成后就开始测试它,首先我们做了几个小小的手动测试,没有发现问题。然后进行更全面测试,开始生成大量的随机性测试用例(test case),将跳表的结果与C++的set进行比较。看了看一些旧的Github提交代码后,我发现这个程序看起来基本上是这样:
  因代码中某个地方有错误(bug),引起了这个内存损坏。我们花了好长时间分析代码,查找可能解释得通的原因,但是结果一无所获。后来想到也许是转换过程中犯了错误,我们就回过头去检查原始的实现方法,修改了测试生成器,然后使用低级接口再次运行程序。这回运行起来很顺畅,没有错误!我们更加确定是转换过程中犯了某个错误,接着我们回到转换后的代码,更细致地分析代码,但由于毫无进展。最后我们决定掏出大家伙用GDB调试器来运行程序。
  我的朋友在GDB方面比较有经验,就让他来带路。我的记忆有点模糊,但这大约发生了什么。我们观察到的第一件事情是错误出现在一个节点的析构函数中,接着在那里释放一些内存。遗憾的是,并没有查出为何会出现这样的情况。在反复的调试器中,我们得到了一个大突破。跳表的析构函数似乎被调用了不止一次。这就可以解释我们看到节点的析构函数出现奇怪的行为以及内存错误。在修改了代码输出一些调试信息后,我们证实了情况就是这样,但是那也很奇怪。我们没有显式调用析构函数,所以它唯一被(隐式)调用的地方是在程序末尾。
  于是从头查阅代码,终于明白了析构函数为什么莫名其妙地被调用了多次。我还记得,我和朋友同时发现了这点,我们互相对视一秒钟之后哈哈大笑,意识到我们有多可笑。
  根源就出在size()函数上。不是跳跃表的size()函数,而是我在代码开头定义的那个size()函数,如下所示:
  我之前定义这个便利函数是为了消除关于带符号整数和无符号整数之间的比较的一些警示信息,在测试代码中用了它几次。比如说,确保跳跃表中的元素数量 (t1)等于set中的元素数量(t2)时,我们是这么做的:
  那么,这个函数到底错在哪里呢?问题在于,参数x由值传递(这是C++中的默认模式),而不是由引用传递。这意味着每当我们调用函数,给它传递某个对象(这里是跳跃表),就会发生下列情况:
  1. 对象被拷贝
  2. 副本被传递给函数。
  3. 函数使用对象的这个副本来执行;当函数终结时,
  4. 副本(通过调用析构函数)被销毁。
  所以,每当测试代码调用size(t1),它就会使用拷贝构造函数,对跳跃表作一份拷贝,然后调用副本的析构函数。
  未实现拷贝构造函数或析构函数函数时,C++会提供一个健全的默认实现。实际上,如果我们刚使用了默认的实现,就不会有这个错误。然而,当对象分配内存(使用跳跃板就是这样),用户通常想自定义实现拷贝构造函数,以便拷贝已分配内存的实际值(而不是默认的实现那样仅仅拷贝指针)。但是我们刚实现了析构函数,而不是拷贝构造函数。于是当跳跃表的副本被拷贝后(为了size()函数),默认的拷贝构造函数只是将指针拷贝过去。然后,副本的析构函数被调用后,它释放指针的内存。而由于这是跳跃板的原始副本使用的内存,现在它不可以被使用。但测试代码继续下去,因而遇到许多内存错误,最终崩溃。
  但是这不是size()函数引起的唯一不良反应。不妨考虑下面这段代码:
  它创建了100万个整数的向量,然后迭代处理这个向量,将每个值设为42。这通常会在你的普通计算机上运行几毫秒(我的计算机上实际用了4毫秒)。然而,由于ize()的参数由值传递,100万个元素的向量拷贝到循环的每次迭代。又由于有100万次迭代,这要花很长的时间。实际上,这在我的计算机上耗时13分钟38秒。
  不管怎样,不妨回到我和朋友认识到什么引起内存错误的那一刻。我不仅认识到当前编写的size()函数可能有什么影响,我还明白:由于这个函数实际上来自默认C++模板(我将编辑器配置成这样每当创建一个新的C++文件,自动导入该模板),这个错误就会出现在我长期以来编写的基本上所有C++代码里面。至少几个月来是这样!
  当然,这解决起来轻而易举,从提交的这段代码可以看出,我们的测试程序在解决了这个问题后运行起来毫无问题。
21/212>
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号