找BUG记

—— By EXP 2014-03-22


令人头痛的陈年老BUG(序章)

前几天,码农朋友甲(下文简称“甲”)拿着我5年前发表在某博文的代码问我:“这段代码是有bug吧?”下面就是他给我指出来的一段C++代码,大家可以先尝试能不能找到甲看到的bug:

void solve::Initial(void)
{
    TimeStamp = 0;              // 时间戳
    DFN = new int[N+1];         // 搜索次序
    Low = new int[N+1];         // 能够回溯的最早次序号
    setIntArrayVal(DFN, 0, N+1);
    setIntArrayVal(Low, 0, N+1);

    SCC_id = 0;
    SCC = new int[N+1];         // 辅助栈
    Status = new int[N+1];      // 辅助栈状态
    setIntArrayVal(Status, 0, N+1);

    sp = new Shrink_point[N+1]; // 缩点(极大强连通分量)
    return;
}

void solve::setIntArrayVal(int* array, int val, int len)
{
    memset(array, val, sizeof(int)*len);
    return;
}

诚然,突然要我查一段几年前写下的代码是否有bug,我内心是比较抗拒的——尤其是我自己写的代码(我对自己还是有相当自信的)——毕竟人的弱点就是不善于揭发自己的短处。不过这都只是次要的心理因素。

归根结底,所谓打铁趁热,bug也是越早发现越好,新代码的bug总是要比历史代码的bug更容易处理。而面对这个陈年老bug,我已经完全忘记了我在5年前写这段代码的思绪,所以要我马上就应付甲的质疑是不可能的。与其再花费一番周折琢磨我自己的代码,我干脆直接就举手投问:“所有测试用例运行可以通过,是哪里有bug呢?”

因注释而蔓延

甲告诉我,是memset函数使用错误:在C++中,函数memset的作用是对一段连续的内存块赋值,即赋值的单位是字节,换而言之memset只能用于字节数组,但int数组不是字节数组。

void solve::setIntArrayVal(int* array, int val, int len)
{
    memset(array, val, sizeof(int)*len);
    return;
}

老实说,我很高兴甲会如此仔细的看我5年前的代码。而且毫无疑问,他的观点是正确的。但是也不见得我就是错的。因为早在那时我就已经知道memset函数的局限所在,但我坚持要用这个函数做数组的初始化,是因为我看中了它的效率——

相对于逐个赋值的方法初始化数组元素、memset的效率要高得多,因为从寻址次数来看,前者的时间复杂度是O(n)、后者是O(1),更何况当时所解决问题的n是上千万级别的。虽然我把memset用在非字节数组,只要我保证初始化的值只为0就不会有任何问题。事实上也是如此。

于是我自信满满地告诉甲,单纯断章取义地看我这个方法,确实是一个bug。但如果整体地去看我的代码就恰恰相反,我只是利用了bug,并得到了更高效的处理

但是甲之后的一席话确实值得我深思:

“或许对目前的这份代码而言,这个bug是被你巧妙地利用了,但是我觉得真正的bug或许不是你的代码,而是你没有文字注释去说明你的想法。不要忘记你已经共享了你的代码,当更多人看到这段程序时,如果他们不了解menset的原理就照样搬用,那么你就无异于在别人的代码中散播了bug,因为你不能把他们代码中的val限制为0。”

最危险的组合

不得不承认,甲是对的。即使我有足够的自信在5年后仍然记得利用这个bug的前因后果,但在这5年间早已误了不少别人的子弟......

不过话说回来,先不论这个bug的蔓延性,甲能够如此深入琢磨我的历史遗留物、并发现这个bug实属难得——在软件中有一种bug是最难被发现的:组合式的bug。组合式的bug有两种类型:相辅相成型、相互弥补型——甲在我代码中发现的bug就属于后者。

相辅相成型:举例而言,一个bug是楼梯很滑,另一个bug扶手坏了,但除非这两个bug同时存在,否则只有其中一个bug是不足以让人摔下楼梯的。

相互弥补型:它与相辅相成型刚好相反,只有两个bug同时存在(或不存在)程序才会正常运行。若只修正了其中一个bug,另一个bug就会曝露出来,而且会让人有误以为自己改错了的假象,因为修改之前程序是可以正常运行的。

之所以说它难以发现,因为组合bug几乎无迹可寻,尤其是相互弥补型。除非是编译原理的狂热爱好者、抑或出现了非常极端的运行环境。存在组合bug的程序,其通常状态无异于正常程序,而且可能正常运行了很长时间都没有曝露出来

回到我的代码,它已经正常运行5年了。如果甲没有向我质问他心中的疑惑,而是擅自修改了他所发现的bug,那么我的程序就无法正常运行了——而甲就很可能会因此陷入怀疑自己的正确性的境地。

令人头痛的陈年老BUG(终章)

事实上,不是所有bug都需要解决掉的。很多时候我们明明知道正在为代码引入一个bug,但是我们却依然保留它。因为回避它的代价太大了,我们宁愿限制它的前提条件不让它轻易发生、或者将其“圈养”起来(如try-catch)不让它暴走——如何容忍bug也是一门学问

不过也总有一些技术葩喜欢另辟蹊径,誓言要代表月亮消灭所有bug维护代码界安全——先不说甲就是这种人,反正我是不会去消灭一个几年前就已经知道的bug的。如果要消灭它,我当时就做了,何必等到现在。

这前面提到的“新bug更易于旧bug被解决”是一个原因,但我真正担忧的是我或许会引入更多不可控的bug——代码的历史太久远了,我已经近乎忘记了它的逻辑,我一旦盲目修改,完全有可能采用了更危险的方法去解决那个稳定了5年的bug。

很多时候,我们写完一段代码,只要程序能够编译运行、完成需求功能就算完成了,鲜有考究bug的可能性,大部分的bug都是通过日后使用时再去发现和解决的。其实解决bug的黄金时间在于代码刚被编写的时候,这时候我们往往只需看到异常提示,就可以马上定位异常原因,因为潜意识中我们已经隐约觉得哪个位置会报什么异常了。

所以当我们在面对一些陈年老bug的时候,其实早就已经错过了解决它的最好时机。这时候不妨将其圈养起来,可能相比于消灭它,会令代码更安全。

Copyright © EXP 2020 all right reserved,powered by Gitbook最后修改时间 : 2020-04-22 11:20:44

results matching ""

    No results matching ""

    results matching ""

      No results matching ""