如何在复杂代码中寻找BUG

来自知乎问答,看了觉得很有益,就整理过来学习一下。

原始提问:快毕业的通信学生,之前正式代码经验几乎零。目前在已经给Offer的公司实习安卓开发。Mentor说先从找code base中bug开始。但是我感觉我们的codebase好复杂,这几天突然没什么进展。uml之类的也画了不少。想问问前辈们有什么建议?


解答:

1:来自姚冬,哥写的不是代码,是梦想

我曾经做了两年大型软件的维护工作,那个项目有10多年了,大约3000万行以上的代码,参与过开发的有数千人,代码checkout出来有大约5个GB,而且bug特别多,open的有上千,即使最高优先级的showstopper也有上百。
分享下我的debug的经验

1. 优先解决那些可重现的,可重现的bug特别好找,反复调试测试就好了,先把好解决的干掉,这样最节约时间。

2. 对于某些bug没有头绪或者现象古怪不知道从哪里下手,找有经验的同事问一下思路,因为在那种开发多年的大型系统里,经常会反复出现同样原因的bug,原因都类似,改了一处,过一阵子另外一处又冒出来,而且无法根治。
比如:我那个系统里有个特别危险的API,接口参数比较难用,一旦有人用错了某些情况下就会出诡异的现象,解决很简单,找到调用这个API的地方把调用方式写对就好了。为什么不根治呢?因为要保持兼容性不能改接口了。Windows系统里就好多这种烂API。
问下老员工吧,说不定他们都遇到过好多次了。

3. 放大现象,有些bug现象不太明显,那么就想办法增大它的破坏性,把现象放大。这只是个思路,具体怎么放大只能根据具体的代码来定。
比如:美剧《豪斯医生》里有一集,怀疑病人心肺有问题,就让病人去跑步机上跑步,加重心肺负担,从而放大症状。

4. 二分法定位,把程序逻辑一点点注释掉,看看还会不会出问题,类似二分查找的方法,逐步缩小问题范围。

5. 模拟现场,有时候我会问自己,如果我要实现bug描述的现象我要怎么写代码才行?
比如:我遇到一个死锁问题,但是检查代码发现所有的锁都是配对的,没有忘记解锁的地方,而且锁很简单就是一个普通的临界段,保护几行附值语句而已。这样的代码怎么写才能让他死锁呢?
我想如果让我故意制造这样一个现象,只有在上锁的时候强制杀掉线程了。
既然这样就可以去看看有谁强杀线程了没有。

6. 制作工具,针对某些bug编写一些调试辅助工具。
比如,我那个系统没有完善的崩溃报告,虽然也有dump,但是分析出来的callstack经常不准。于是我为解决崩溃问题编写了个工具,会自动扫瞄代码,在每个函数入口和出口插入log,以此来定位崩溃点。

7. 掩盖问题,虽然这样做有点不厚道,但是有时不得不这么做。有些bug找不到真正的root cause,但是又要在规定时间内解决,那么我们就可以治疗症状而不去找病因。比如用try catch掩盖一些奇怪的崩溃。不到万不得已不要这么干,未来可能会付出更大代价。

我在做这份工作的时候也在追美剧《豪斯医生》,豪斯大叔解决病症的思路和debug差不多,对我很有启发。


2:vczh,专业造轮子 http://www.gaclib.net

我在sqlserver的时候,有一次让我去修改一个用C++后来转MC++后来转C++/CLI和C#混合写的sql profiler的05年的一个replay什么东西的trace的bug。软件巨大无比,当初创建这个bug的人都不知道为什么要这么写了。于是我静下心来,肉眼看了3个星期的代码(!!!!!),整个软件的脉络都理清了,最后稍微运行了一下确定位置(因为启动debug实在是太麻烦了),然后修掉了。

所以说,只要老板不逼你,肉眼看代码找bug是毫无问题的。


3:RefuseBT,IT/IOS开发/ACG/小散/求交往

1、能复现BUG,你就捡到宝了。基本调试一下都能找出问题在那里。
2、排除法很重要,尽可能多的排除掉可能性,将一个复杂环境的问题,缩小到一个特定范围。比如先是找问题模块、再是找问题函数、最后找问题代码,然后排查出现问题的输入、条件等等。
3、怎么改,要看这个问题范围内依赖那些变量、条件。然后查找这些变量、条件被那些地方公用。比如这里面需要变量value,那么你就通过搜索“value =”,找到所有赋值的位置。看看这些地方有没有问题。最后决定解决bug的方案。也就是说你发现问题的地方可能不是应该改bug的地方,真正的原因可能在其他依赖的地方。
4、你要对你所使用的语言、系统有深入了解。比如有些问题是线程同步问题,调用时序的前序后继问题,有些是副作用。这个时候单步调试可能困难比较大,那么你需要在可疑位置打Log进行排查。


4:黄亮,程序员,函数,算法

居然有这么多自以为正确的做法?做了这么多年工作没有反思过?

我的经验:bug没有在第一秒反映出几个原因,只能说明你对软件系统非常不熟悉。这个第一反映的原因当然不一定正确,需要看代码或调试映证自己的推测。一个架构师不能把几十万代码放到心里,一个程序员不能把一两万条代码放到心里是一种能力不足的表现。能力不足的人沉下心来写几万行代码是个不错的选择。

先说说什么是bug,我认为所有不符合客户预期的软件功能和属性就是bug。因此bug有且只有两种,一是没有按需求设计,二是没有按设计实现。前者暂不讨论,我们说说后者。

最痛苦的情况莫过于已经不知道当时的设计是什么,这也很常见,解决方案有两个:

其一,删了它。没错,就是删了它。只要产品还是beta之前,就可以做这个事,删除它,试错,根据客户反馈找到需求,大不了改回来。关键是你找到了需求,有了参考代码,重新设计一下不会太难,再根据设计来简化实现。这就是所谓的“根本不改bug,直接重新实现”。

另一种情况比较难办,已经是维护产品,不能给最终用户发布测试版本出去试错。这就需要clean code中的办法对付这种leagacy code。这是真的水磨功夫,不过对于压力不大的维护版本,还是有时间做的。特殊情况下,又急又紧时,上第一种办法。人都要死了还在乎一块肉,割了。

相对比较好的情况是有一定的设计,但设计文档留下的不完整,或代码与设计已经有较大区别。这时,你需要找一个老员工,听他讲讲这些代码是做什么的,哪里有坑他都知道。这个过程大体上是能补全一个设计思路,特别是当初没有设计好的地方。但是请注意老员工一般有很多低层次的经验和得过且过的想法,不能太当一回事,也不能完全忽视。下面的工作还是重新设计,简化实现。

最好的情况是有一个比较完整的设计。这种情况基本是纯代码问题,确认到函数层次后,基本是照本宣科,常见的错误就哪么些,已经知道错在哪里,一行行的对吧。findbugs什么的也是在这种情况下有用。哪有什么找不到错误,不知道错哪了。一句话总结:”经验不足“,没见过怎么找得到。

举个例子,云风提到一个"崩溃发生在一段红黑树插入节点的代码中,这里的 pathp 指针变成了 NULL"问题。首先你应该马上想到野指针,数组溢出,字符串结尾\0。这就是经验,老程序员的核心价值。然后找到对应的代码,一行行的看,有没有上面的问题。至于这两行代码你能不能找到问题,这又是另一种经验了:

char *buffer = calloc(sz*2+1, sizeof(char));
for (int i = 0; i < sz; ++i) {
    sprintf(buffer+i*2, "%02x", data[i]);
}

答案:溢出了。%x在遇到负数时,会输出补码,这就不只2字节了。从这个角度来说,程序员不在乎对了多少,而在乎错过多少。

对于不得不干这个事的新手,建议你找一个老员工帮你提思路,你来一行一行的验证,对于有疑点的地方找老员工确认。

我在刚进软件行业时,也认为软件系统是一个不确定系统,影响因素太多,以至于测试无法证明它的正确性。不过这几年我发现,软件系统的正确性可以被设计出来(所有属性都可以被设计,只是要权衡取舍)。不考虑物理错误的情况下,软件系统是确定系统,所以bug不过是一个错位了的功能。

P.S. 让新员工独立找bug或者解决bug是一个非常不负责的作法,新员工更适合与老员工结对完成新功能。一方面,他们冲劲足,思想还没有条条框框,实现起来比较快,有更多的创新;另一方面,他们经验不足,还没有团队习惯,不能独立工作。一但无法融入团队,就会造成伤害。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值