欢迎光临
我们一直在努力

程序员新人怎样在复杂代码中找 bug?

职场新人好多事情都会迷茫,写代码最忌讳的就是bug,那么新人该如何在眼花缭乱的代码中找到那些“不想看到却又不得不找的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

匿名用户

新人干活,少点方法,多点诚意。你知道为什么老大喜欢让新人从修bug做起吗?因为他想让你“跟”。

你知道为什么他想让你“跟”代码吗?因为只有这样才可以熟悉项目。“跟”是一个重要的学习过程,最终目的除了修复bug,更重要的是你在这个过程里注意到了什么。比如大概哪些类参与了你跟的某条执行线路,相互调用关系怎样,结构设计上有什么特点或不足。

修bug不能靠看,被你轻易看出来,能叫bug吗?不如叫错误。跟就是眼手合一,把代码捣碎了,嚼烂了,动它一动,才能发现其中阴谋。新人不应该上来就来断点,哥鼓励所有新人用好语言提供的log调用,java就用好console.log,php就用好error_log或者var_dump,python就用好pprint。

在你对代码一无所知的时候,最重要的是找到程序入口,找到入口以后就可以往代码中添加各种log语句,然后执行这片代码,观察log输出的内容。往下跟,塞进去log语句,观察变量命名是否和手上的bug有关系,这是猜的本事,是直觉,有时候对有时候错,但猜错和猜对一样是重要的知识,因为下一次你就知道“哦,那条路不行”。在这个过程中,你深深地注视了一回代码,会明白很多东西。不要假设这只是要修个bug,而因此把全部注意力放在调试工具上。你的老大绝不是这个意思。

3

张恂老师

首先,找 bug(如 Testing)与去 bug(Debugging),这在软件工程里是两个截然不同的任务。发现 bugs,测试并非唯一手段,其他还有 Code Review、Inspection 等等。找 bugs 通常有几个相对快速的办法。

比如,为 code base 编写/添加更多的自动单元测试,这是一种白盒测试。你 Mentor 让你“先从找 code base 中 bug 开始”,除了让新手阅读、熟悉老代码外,大概也有这层意思。找到 bugs 后,怎么快速、高效地掐虫(Debugging)就是另一码事了。Debug 对于新手来说普遍是一大弱项,在这上面常常要耗去大量的调试时间。什么原因?这主要是因为新手编码、读码的量都很少,而且都没经过什么系统性的训练,逻辑推理思维很弱,最关键一点是 —— 你们在大脑里的思考,对于真实的软件结构、软件模型(尤其运行态模型)的理解是混乱的,所以往往很难像经验丰富的老手那样迅速、准确地定位 bug 所在。

先介绍几个相当有效的调试技巧。UML 建模因果图先把你分析到的引起某个 bug 的各种原因画出来、列出来(简单的可以记在心里),然后从可能性(概率)最大的原因开始,做试验,定位错误代码,排除 bug;如果不成功,就通过排除法逐一缩小可能性范围,直到尝试过(排除了)所有可能的原因。

Tracer over DebuggerTracer 有的地方也叫 Logger。Log 是宽泛的说法,有运维日志、调优日志、跟踪日志等等不同的种类和用途,而 tracing log 主要就是用来在开发时掐虫的,系统日常运行时一般会关闭。

有人说:毕生绝学,printf()

是的,就是这个意思,tracing/logging 是非常有效的。当然,一般不会直接用 printf(),太原始、初级了,而是用专门的Logger 或 Tracer 工具与 API。

我 20 多年的一个主要调试经验是:差不多八、九成以上我遇到的 hard bugs 是通过 Tracing(写跟踪日志)定位、处理掉的,尤其那些最复杂、最隐蔽、最难杀的 bugs。Debugging by Tracing (or Tracer) 在通信行业、通信软件等复杂系统的研发中是非常普遍的,也是一个最佳实践。那种基于 GUI Debugger,通过设断点,戳 F8、F10、F11、Shift+F11 。。。的所谓“单步调试”方式(Debugging by Debuger),我反而很少用,感觉这种方式效率较低、偏慢,主要适合一些少量、简单、位置比较明确、局部的 bugs。排除法见很多人说二分法,怎么没人说排除法、反证法、还原(固定)法隔离法。

4

朱众

从根本上来解决问题。 挑一些简单的bug开始改,因为你的使命是熟悉代码而不是改bug,真是特别复杂的bug都需要改设计改架构的,bug不仅仅是逻辑错误,有时是整个设计出的问题。可是你作为新手,不熟悉软件的架构和原来设计者和架构师的用意,要么改错,要么只能轻轻的放一个dirty fix。比如(if xxx!=null) if(xxx is XX) 甚至 catch(Exception)。所以我建议, 首先找一直重现的bug,不能一直重现的bug, 往往跟好多东西有关(内存、并发), 还得有经验的人脑子里有整个时序图, 才能”猜”出来哪里可能有问题, 然后用工具去验证,这个你暂时做不来。其次, 找容易找到出问题代码的bug, 比如UI或者直接crash的bug。

UI的bug基本都能知道哪里的值不对了, 直接推回去看后台(业务)代码是什么问题。直接crash你一定有办法看到call stack, 就比较轻松了。最后, 找你们组最牛逼的人问问经验,及常用的问题定位方法,比如日志。我理解最牛逼的人很忙, 但是不要去找有时间还热心的人, 那些人的水平根本跟不上, 所以才有时间。他们要么回答不了你的问题然后跟你研究好几天,要么提一些tricky的方法,要学就跟最厉害的人学,他之所以忙也是因为他最厉害,他会愿意花几分钟告诉你如何快速并且高效的找到他负责的软件的BUG的,至于具体的找逻辑错误, 别人回答的已经很好了, 自己摸索自己最喜欢的方式。

TIPS: IDE里有很多很厉害的功能不要小瞧。我这个小菜鸟就说说VS, 有condition breakpoint, 运行时所有Exception都是是可以配置要不要抛给你并暂停线程的, 可以给变量分配编号 (eclipse可以直接看到地址 – -#), 等等,可以上网查查相关的,磨刀不误砍柴工。祝你成为一个伟大的程序员!

5

匿名用户

毕生绝学.二分调试大法—update:关键就是就是不断迭代缩小范围,最终定位问题的症结所在。具体手段包括但不限于:注释掉部分代码、在不同位置插入试探性代码、对输入数据二分、对代码版本二分、对运行环境二分?

只要是有重现方式的 bug,二分肯定能找到问题所在,要是有啥妙招那也可以用用,不过貌似没有妙招的时候多一点. 如果重现的方式比较复杂,耗时很长,那可能要专门写些程序或脚本来自动做二分。

6

Kenny Lang

同为刚毕业的新人,入职快6个月,非常理解楼主的问题。关于怎么找bug的问题,有人已经回答的很全面了。而且,其实最应该回答这个问题的应该是你的mentor。

首先,从bug入手,了解codebase,应该是平衡mentor和新人之间利益最大的办法。其实要想入手最快,就应该是让mentor24*7的在你旁边手把手教你,但这根本不现实,也没有意义。所以从修改bug入手,通过一个个小bug去了解整个project的结构和design pattern,对新人来说,这种学习既直观又不会被复杂的代码吓死。最主要的是,当你成功fix了一个bug,这种成就感是一个新入职的程序员勇往直前的动力。而且,修了一个bug,最重要的不是你unblock了多少人,或者帮助了多少用户,而是你从这个bug里看到了project怎么样的结构。当初为什么这么设计,为什么会出bug,时不时codebase里还有类似的bug,以后怎么避免。别觉得不好意思去问你的mentor。他也许很忙,但他既然是你的mentor,就有义务帮助你平稳度过最开始的几个月,(而且,往往你的成功可以直接证明他的领导力强大)如果觉得过意不去,就多向你的老板/他的老板讲讲你们的事情。但是,一定要问有意义的问题。

其实很多时候,如果你再多深入地看一页code,再多搜索一次别人写的wiki或者文档注释,就可以得到解答。这种问题,既浪费别人的时间,也不会让你养成深入思考和探索的习惯。久而久之,有事没事去问mentor就取代了你自己思考的过程。mentor再牛,也不可能手把手的教你。但他可以教你的,是学习的方法/习惯、debug的方法/习惯、以及写code的方法/习惯。认真观察他的思路是怎么建立起来的,认真学习他是怎么debug的,认真看他是怎么涉及结构的。学会了,你也就出师了。善用debug工具。不要小瞧debugging的过程。我真的见过已经是很成熟的程序员还在用二分法println debug。这不科学。。。尤其当你的project 变得很大,每次build就要好久,这样特别浪费时间。(尤其android)。设置break point和写有针对性的unit test必不可少。虽然又时候必须要看log,但有很多超级好用的debugger,稍微花时间去学习一下怎么用,或者看你的mentor怎么debug,会大大加快你的效率。(我曾经见过我的mentor用chrome debug。

真的是觉得帅爆了!目瞪口呆”其实新人很多时候都会觉得程序某个地方很“神奇”,明明应该这样,但却那样。千万不要跟你的mentor抱怨“神奇”,因为这两个字在整个代码行业就不存在。我的老板经常给我讲的一句话是,“code never lies”。代码运行异常,一定有运行异常的原因。不要揪着“为什么是这种异常”不放,而要去想“什么样的结果是对的”以及“怎么产生对的结果”。学习framework。project变大了,framework必不可少。但因为framework的存在,让整个project变得更“捉摸不透”。所以,花点时间学学你们用得framework,以及针对这种framework debug的工具,静下心看看文档,自己在动手写一写,其实framework真的很美。

最后,时刻保持跟你mentor之间的sync up。让他知道你的困境,也让他知道你的成就。mentor也是从新人走过来的,没什么不能讲的。我最绝望的时候是在入职两个礼拜。自己拿到了一些bug,也有了starting project。当时觉得自己对着电脑就像看着又大又丑的金刚。我也听说这个过程所有人都有,就算从别的公司跳过来、再资深的工程师,面对一个成熟的project,也都要花时间学习,经历失败、绝望。所以,没必要灰心,也没必要过度担心自己的表现。大家对新人的期待都很现实,所以稍微超出预期,就会得到很好的反馈。祝题主顺利度过开始的几个月,逐步步入正轨,享受工作。

7

黄亮

居然有这么多自以为正确的做法!做了这么多年工作没有反思过?我的经验: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是一个非常不负责的作法,新员工更适合与老员工结对完成新功能。一方面,他们冲劲足,思想还没有条条框框,实现起来比较快,有更多的创新;另一方面,他们经验不足,还没有团队习惯,不能独立工作。一但无法融入团队,就会造成伤害。

8

foruok

谈谈我的理解吧,我都与BUG战斗了十几年了。理解bug关联的业务和逻辑如果能够debug,用debug观察代码流程观察软件的log,理解代码流程加入自己的log,观察流程尽快解决问题(搞得定才有发展)测试你的解决办法会不会引发新的bug(在此过程中可以了解关联业务和逻辑)回顾你在解决bug过程中看到的业务、逻辑、代码、设计,继续读源码、文档,争取以点带面,了解更多。

9

RefuseBT

1、能复现BUG,你就捡到宝了。基本调试一下都能找出问题在那里。

2、排除法很重要,尽可能多的排除掉可能性,将一个复杂环境的问题,缩小到一个特定范围。比如先是找问题模块、再是找问题函数、最后找问题代码,然后排查出现问题的输入、条件等等。

3、怎么改,要看这个问题范围内依赖那些变量、条件。然后查找这些变量、条件被那些地方公用。比如这里面需要变量value,那么你就通过搜索“value =”,找到所有赋值的位置。看看这些地方有没有问题。最后决定解决bug的方案。也就是说你发现问题的地方可能不是应该改bug的地方,真正的原因可能在其他依赖的地方。

4、你要对你所使用的语言、系统有深入了解。比如有些问题是线程同步问题,调用时序的前序后继问题,有些是副作用。这个时候单步调试可能困难比较大,那么你需要在可疑位置打Log进行排查。

看完这些回答,愿所有的代码新人都能够被Bug温柔以待!

未经允许不得转载:BkCoding » 程序员新人怎样在复杂代码中找 bug?

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址