前言
本文作于数电课设时,当时未完成全部,这两天大家都在改ls的bug,或者刷题时遇到的bug,改bug这东西也主要是靠经验(当然还有运气)没有绝对的公式,但还是有一定的套路,希望大家(尤其是小鲜肉们)多思考多总结,形成自己的改bug方法。
正文
昨天和肖孟他们在讨论暑假讲座安排时突然想到可以给学弟学妹们分享一下改bug的经验和方法,晚上回去就在手机上写了写。这两周做数电课设,今天终于做好,回过头来,发现在连板子的过程中也是在改bug,觉得有必要写一写。
先说数电课设
最终效果图
可以看到我的丑的一批。。。但是它依然很给力的能正常使用。。。(最终还是得到了良好哈哈哈。。。)
我的数电课设是要设计一个电路,可以实现8个彩灯的花型变换。
抽象来看,和我们的程序很像,各个模块实现不同功能,同时模块之间几乎都有或多或少的联系。
我的课设就是设计 仿真 连电路。
由于经过了一个学期的遗忘,所以我直接就从晚上找了一个模板,然后在电脑上用仿真软件测试可以正常使用(同时@也采用了这个模板直接成功),然后自己直接连接。
全部联好之后一通电直接BUG了。。。
像不像我们的程序,先设计,自己手算一下可以正确,然后直接用代码实现,写完之后,一运行,BANG,错误了。
所以都是需要我们去找到bug,改正bug,最终正确(我的课设就是由于发的板子有部分短路了。。。坑爹的学校。。当然康康最后还是改好了haha)
改bug一方面要会用调试的工具(比如gdb,比如电压表),但是更重要的是改bug的思路。
改bug的思路
从程序的角度来说
因为我自己的学习情况,所以我在这里主要分享一下我自己的思路,主要是以Linux下的C/C++单线程程序为例。
首先是要定位错误代码
我们的错误很有可能是“一段”,比如100行代码中可能前50行和后30行的处理都是正确的,但是中间的一段写错,那么最终的结果肯定是错误的,但是我们不需要去改正后30行,只要改中间的错误就好了,所以这里要定位错误的“起点”和“终点”。
我们可以先划出一个大致的范围,然后一点点的缩短范围,肯定是先找到错误的终点比较简单。
最简单的终点是能定位的错误结果,相对来说,比较纠结的是段错误,不知道程序是在哪里错误的,我比较喜欢通过gdb+coredump的方式定位错误,之前曾经写过一篇博客介绍如何生成coredump文件
而像输出结果出错,程序进入死循环,这样的错误“终点”很好定位。
然后就是寻找“起点”和缩小范围了。
我主要通过判断程序是否符合预期判断来检测。比如这样的代码
result = get_result();
send_result(result);
我们可以这样判断
result = get_result();
if(result)
print result;
else
return 1
Send_result(result);
或者这样的代码
while(p){
p->num++;
P = p->next;
}
改成
while(p){
p->num++;
P = p->next;
printf("*\n");
}
可以看到程序运行了多少次。
如果不知道从哪里设置这样的检测,可以用二分的思想,先从中间找,如果中间正确说明前面出现错误的可能较小(毕竟这样的检测也不全面)。然后继续缩小范围,直到找到第一个“起点”。
然后就是分析错误
是逻辑错误导致程序的执行流程出错,还是由于内存访问,堆栈溢出等非法问题,又或者是异常情况导致的?
对于逻辑问题,我们可以观察函数的调用过程,按照顺序自己在纸上“运行”一次,看看是否出现错误,当然也可以gdb一步一步,我是比较喜欢设置断点,或者直接在错误结束处通过gdb的bt命令查看
其实对于改bug,最难的就是这一步。古人云:当局者迷,旁观者清。有时候莫名奇妙地写错变量名,把p写成q,改半天都看不出来。这里我有一个特殊的方法(还是在知乎上看到的)
小黄鸭改bug法:
方法很简单,拿一只小黄鸭放在你身边,然后对着它,从头开始一行一行地给它讲你的代码,每句话的作用,当然啦,要用非常通俗易懂的话给它讲。
其实这方法就是让你静下心来,从头到尾,就像你阅读别人的代码一样,一行行地去理解你自己写的代码,把自己的逻辑梳理清晰,这也是为什么有时候别人改你的bug比你自己改更容易,别人更加冷静客观。
道理大家都懂,然而依旧沉不住气,所以平和的心态才是改bug最重要的。
然后就是针对错误去修正
这里就没什么好说的了,改呗。。。
康康的私货
好东西要放到最后嘛哈哈哈,感谢你看到这里,分享一些私货。
1.随时捕捉bug
简单的bug是每次都出现的,而复杂的bug往往难以复现(偶尔才崩的程序让人摸不到头脑)。
所以我把系统设置为每次段错误都生成coredump文件(方法请见如何生成coredump文件)。通过修改bashrc,保证每次编译都增加调试选项(正式开发可能不能这么做,不过我们现在主要是为了方便调试)
在 ~/.bashrc中增加
# gcc default
alias gcc='gcc -g'
# g++ default
alias g++='g++ -g'
这样出bug直接就用gdb开始调试了,出现段错误直接就记录下来。
2.缩短调试时间
之前看到学妹调试课设的时候,为了一个链表排序,每次都要输入很多数据,这样做,既麻烦的同时,让自己容易浮躁和生气,更难静下来去改bug了。
所以当遇到这种需要大量数据输入的bug(典型的例子,链表创建、排序)时,我们可以在代码中先“写死”数据,比如写一个顺序排序,用一个逆序数组去测试,节省了每次调试需要的时间,同时让你能更专注bug本身,不用再费心每次的输入,还可以通过你精心构造的数据观察到测试结果,一举N得。
3.做好版本控制
说的文诌诌,其实就是要记录好每次的修改,防止出现改着改着更错了,这里说版本控制,其实就是用git管理是最好的,当然,小程序,每次只要写好注释,做好备份,也是没有什么问题的。
4.做好模块测试
写代码时不要一次写完再运行,多分成几个模块,把一个模块做好,得到正确结果之后再往下继续开发。用断言提前控制模块的返回结果,
比如
ret = func();
assert(ret); //ret为0 程序直接退出
5.考虑其他因素(运行环境,bug相关的因素)
有些bug的问题就是和运行环境有关(比如多线程程序在多核就bug,在单核上没有,很有可能就是线程同步时出现死锁,而单核相当于默认加锁),有些bug和时间有关(比如程序没有考虑闰年),这些难以复现的bug往往要从各种因素去考虑。
6.及时总结
见的多了,也就有了经验,当然遇到问题也更加冷静更加坦然了,所以bug不可怕,改好了,我们下次就有经验了!