一个CST问题的教训


最近在忙一个CST的问题,所谓CST就是客户那边过来的,需要快点完成。尽管很着急,这个问题还是来来回回的做了将近两个多礼拜,在我近期这么牛逼的状态下这已经是非常不成功的了。

这个CST bug有几个方面的问题导致了长时间无法fix。

最大的问题是对代码不熟悉,这是最致命的缺陷,尽管名义上我们组是做这个软件,可我长期是投入在另外一个项目中,这个软件对我就是全新的。由于不熟悉代码结构,只能从表象入手,采取的方案就是头疼医头脚疼医脚。

这个问题有几个表现方式,一个是程序可能挂起,点什么都没有反映,或者就直接crash拉倒。这个问题在客户那边涉及到大量的Ethernet设备,需要测试设备太多很难重现,只能在公司内想法模拟。反反复复实验,有时候好用有时候就不好用,关键是无法维护一个稳定的测试硬件结构,只能与其他同事共享使用,很难通过某些硬件设备的插拔来确定到底是哪个设备导致问题。

这也得到第一个教训,如果可能的话,其实应该维护一个稳定的硬件环境保证可以准确定位问题原因。问题不怕难,就怕不重现,如果能重现就要缩小问题的范围,以保证能够准确定位到问题原因。这种CST的问题的困难就在于没法确定我们修改的问题和客户的问题是一样的,只能猜。

通过windbg里面的adplus抓取了crash或者hang时候的memory dump,可是另外一个问题出现了。我们编译的代码里面没有debug信息,也没有map文件生成,想要准确定位是一件比较麻烦的事情。当然可以通过map来判断出现问题时候的函数,可是最佳方案还是使用pdb。

教训二,项目建立初期就要想到将来出问题怎么办,要有一个良好的log机制以及崩溃时候的反馈机制(google 有个项目就与此相关,breakpad),通过stackwalk函数以及SEH相关函数可以做到crash实时记录程序调用栈,另外程序的project setting最好设置link时候生成debug文件以及编译时使用program database,某些情况下,只有客户能够重现的时候,通过分析memory dump搭配pdb,基本上可以把握住出问题的那个点。另外,熟悉一些常用的工具加上自己编写一些小程序是开发人员必要的素养,如果没有windbg和adplus,想要修改这个问题就要非常困难了。

想办法整了几天,抓了几个挂起(hang)的dump文件,发现问题横跨了四个组件,调用堆栈达到将近20层,问题定位相当麻烦,需要层层分析。对于代码以及设计的不熟悉导致这时候无从下手,只有从头学起。

教训三,一些问题可以看看代码分析分析调用时候的变量值就找到问题,比如对象指针都是NULL还要调用成员函数。最怕就是看代码没有大问题,需要仔细分析逻辑,这时候要是不熟悉代码几乎很难找到问题,找到了也不容易修改。如何避免此类的问题呢?一般情况下,平时不紧张的时候就应该经常阅读相关项目代码,既可以学习其他人设计的高明或者是教训,fix bug的时候也能做到胸有成竹。一个新手和一个老手在fix bug和添加特性时候的进度是大有区别的。另外读代码也是一个code review的过程,我在修改这个项目翻看代码的时候发现了大量不验证就直接使用指针的情形,指针为空是最常见也最容易避免的错误。一个良性的开发过程坚持做code review以及unit test,其实可以避免后期这种维护上的负担,可惜很多程序员不明白这种投入的价值。

教训四,对于COM接口的滥用是这个项目的一大缺点,必要的时候用接口,不需要用接口的用dll或者lib就可以了,大多数情况下除了自己没人会用那些组件。也不跨进程,就直接调用好了,何必非要跨越一个COM呢?搞得那么复杂,纯粹就是自己玩自己。尽管软件设计的问题都可以通过添加一个抽象层解决,可是别忘了,添加一个抽象层就加入了更多的复杂性,增加了更多出问题的可能,难理解也更脆弱。

挂起有这样几种可能,比如线程死锁,消息阻塞,或者是死循环。在dump定位的代码中没有多线程,也不是界面,而且还有while(TRUE)这种循环,于是乎我加入一些log讯息来判断是不是死循环了。实验的结果让人惊喜,果然是在某个情况下进入死循环,可是那段代码的逻辑几乎让我吐血,反复研读代码和注释还是不理解到底要做什么。

教训五,自己开发代码也好,review其他人的代码也罢,如果某段代码需要反复仔细考虑才能理解,那么这段代码原则上就要重构。KISS的原则要时时在心,过去有位诗人写出诗歌要读给老婆婆听,能听懂才算数。我们写代码也应该有这样的想法,否则将来不管是自己维护还是其他人维护,都会有问题,另外注意的是,不要用大段大段的注释来解释代码做什么,如果代码读的不通顺,再多的注释也白扯,代码注释是为了解释代码中无法包含的东西,比如这段代码遵循了甚么文档,或者实现了哪个功能,简简单单一句两句就足够了。踏踏实实的写代码,不需要也不应该炫技。

这个问题完全重现一次需要大概半个多小时,来来回回的点这个点那个,还要看网络上的设备情况,有的时候运行了半个多小时却还是没有重现,只能重新来。除了前面提到的网络上的设备不固定,没有unit test、无法单独测试某个功能也是问题。

教训六,有没有这样的情况?某个功能必须关联到其他的三三两两的功能,前面少了那些前提条件,这个函数就无法运行,如果你正在写这样的软件,是引入unit test的时候了。个人认为,code review加上unit test,对于一个良好的软件是必须的前提,否则将来一定会付出更多代价。假如这个项目有相关的单元测试,只需要重现一次,然后我就可以根据重现时的状态来构造一个单元测试用例。前面提到这个问题是一个逻辑上的错误导致死循环,这也正好是单元测试应该测试验证的地方。只要把当时的输入条件构造出来,那么重现一次就不需要半个多小时了。如果你在做软件开发却还没有使用unit test、没有code review,赶紧应该想办法在项目中引入,这是非常值得的。

问题其实还没有解决,明天依然需要半个小时半个小时的来测试这个问题,好了,先到这吧。

Memeory leak问题调试常用手段

浅论C++在Windows下的GUI自动测试(Unit Test)


《 “一个CST问题的教训” 》 有 2 条评论

  1. […] 刚写完一个CST问题的教训,发现John Robbins大牛最近也写了一篇博客《Correctly Creating Native C++ Release Build PDBs》(正确地建立原生C++Release Build PDB文件),里面有不少说法跟我那篇文字近似。在这里再介绍一下John博客里面的大意。关于pdb文件的重要性,John也有另外一篇博客介绍PDB Files: What Every Developer Must Know,感兴趣的同学可以去看看。 […]

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注