bug的本意是指昆虫、小虫、损坏、缺陷等意思,在互联网时代还有一种引申意义,用来形容某人/物超乎想象的厉害,那简直就是开挂的人生,系统的bug!
一般地,在码农的世界了,bug是在电脑系统或程序代码中隐藏着的一些未被发现的缺陷或问题,可以简称为程序缺陷。从广义上看,还包括软件需要改进的细节、或与需求文档存在差异的功能实现等等。
bug 是如何与程序缺陷联系起来的呢?
Bug的由来
时光回溯到一台计算机可以装满整个房间的时代,大约在1945年9月9日,Grace Hopper发现了Harvard Mark II 计算机的第一个bug。Grace Hopper是数据处理方面的专家,在1952年为UNIVAC开发了第一个编译器,能够把人读得懂的高级语言翻译成计算机能够识别的机器语言。
那一天,Grace Hopper对Harvard Mark II设置好的17000个继电器进行编程后,技术人员正在进行整机运行,它突然停止了工作。于是他们爬上去找原因,发现这台巨大的计算机内部一组继电器的触点之间有一只飞蛾,这显然是由于飞蛾受光和热的吸引,飞到了触点上,然后被高电压击死。死去的飞蛾被夹扁在触点中间,从而“卡”住了机器的运行。
所以在报告中,Grace Hopper用胶条贴上飞蛾,并用“bug”来表示“一个在电脑程序里的错误”。后来,人们把在电脑系统或程序代码中隐藏着的那些未被发现的缺陷或问题,也叫“bug”,同时 把排除程序故障的过程叫 DEBUG,这一“称呼”成为计算机领域的专业术语。
BUG和DEBUG的中文译为“缺陷”和“调试”。“缺陷”可能更反映事物的本质,因为“bug”是从外面爬进去的,并非程序本身有问题。而程序本身存在的问题,是程序原来就具有的。
程序代码中Bug的产生原因
一般地,在程序设计中的术语, Bug是在软件运行中因为程序代码本身有错误而造成的功能不正常、体验不佳、数据丢失、非正常中断、死机等现象。
Bug 的产生原因多种多样,千奇百怪,例如:
- 改错了文件
- 改对了文件,但放错了位置,或者根本忘了保存
- 改对了文件但没有重新编译
- 认为把那个条件变量开启/关闭了,但实际上弄反了
- 运行了错误的版本
- 改正了问题,但忘了提交
- 改正了问题,也提交了,但其他代码都依赖于之前有问题的版本
......
软件系统是一个丰富多彩的世界,总有Bug在里面飞来飞去。任何软件在发布时都不可能是绝对的零Bug,因为谁都不敢保证,自己写的代码没有任何问题。
bug的生命周期和分类
实际上, bug的生命周期可能是这样的:
产生-->被发现-->被解决或者变成了另一个bug。
但是从软件工程尤其是QA的角度看,任何一个bug的一般生命周期包括这样几个阶段:
新建-->指派-->已解决-->待验-->关闭
如果等待检验的bug在验证时没有解决好,则需要重新打开bug,开启循环并继续指派。
由于bug众多,我们在fix bug的时候往往本着要事优先的原则,处理那些影响较大的bug,这需要根据bug的严重程度分类,例如:critical,major,minor,de-effeicency,也就是所谓的P0/P1/P2/P3, 当然粒度也可以分得更细或者粗一点。
根据不同视角,可以对bug有不同的分类,根据bug所影响的领域分类,QA的测试领域可以参见《程序员眼中的测试》。
另外,bug的数量往往被用来作为衡量软件质量的一个指标。在CMM中规定的软件质量标准如下(Bug个数/千行源代码):
- CMM1级 11.95
- CMM2级 5.52
- CMM3级 2.39
- CMM4级 0.92
- CMM5级 0.32
因此,我们往往会在生产bug 和 debug中徘徊,写代码的时间与排错时间的比例有时会高达2:8。既然debug 是我们工作生涯中不可或缺的组成部分,容易混淆的是,debug 尽管在更多时候是一个过程,但有时候指的是一个程序——debugger。
Debugger是个程序
Debugger为一种调试软件,工程师或程序员可以用来验证算法。在一般的硬件设备上,都有着专门用于debug 的接口,在不太遥远的DOS世界里,一行debug 命令可以起到让操作系统初始化的效果。
在windows平台上,WinDbg是微软发布的一款相当优秀的源码级(source-level)调试工具,可以用于Kernel模式调试和用户模式调试,还可以调试Dump文件。
在Linux平台上,一般使用GDB,又称GNU调试器,是用来帮助调试程序的工具。gdb的主要功能如下:
- 启动程序,可以按照自定义要求随心所欲的运行程序。
- 可让被调试的程序在指定设置的断点处停住。(断点可以是条件表达式)
- 当程序被停住时,可以检查此时程序代码中所发生的事。
- 还可以改变程序代码,修正一个BUG产生的影响从而测试其他BUG。
很多跨平台的编程语言都一般会有各自的Debugger。PHP的调试方法最基本的是echo或者var_dump。还有就是使用zend debug 或者Xdebug的调试插件。
pdb是 The Python Debugger 的缩写,是Python标准库的一个模块。pdb模块规定了一个Python程序交互式源代码调试器,支持在设置断点(包括条件断点),也支持源码级单步调试,支持栈帧监视,支持源代码列出,支持任意栈帧上下文的随机Python代码估值。它还支持事后调试,并且能在程序控制下被调用。
例如:
代码语言:javascript复制>>> import pdb
>>> import mymodule
>>> pdb.run('myfoo.test()')
>>>
也可以命令行调用 python -m pdb myfoo.py
。
总之,Debugger是我们在单机debug中非常重要的工具。但是,对于分布式系统而言,这些debugger 都有着很大的局限,虽然也有一些debugger工具,但多是面向特定系统的。
那么分布式系统的debug 主要依赖什么呢?日志。关于日志的重要性,可以参考《全栈必备 Log日志》。
Debug的原则
不论是单机上的应用,还是分布式系统,debug时遵循问题隔离的原则,便于定位问题。
问题隔离可能是所有debug中最强大的核心原则. 问题是否可重现是非常重要的。如果不能重现这个问题的产生方式, 解决起来会变得异常困难。问题隔离使我们拥有了控制变量。
从空间隔离上看,程序代码中具有不同的库或者框架, 并且可以包含许多同事的提交, 其中, 可能有一些已经不再在这个代码库上工作了。问题隔离有助于消除问题的非必要部分, 以便专注于一个解决方案. 问题隔离的目的是弄清楚是发生冲突的根本原因,了解是否存在竞争条件。
从时间隔离上看,避免一次性的大量修改,越少改动代码越好,这样有助于发现问题。连续反馈的一致性结果越多, bug的跟踪就越容易。所以在调试时, 尽量不要安装任何新的软件或组件, 或者引入新的依赖。如果发现每次静态输入却返回了不同的错误, 应该马上提高警惕, 并且全力解决它。
Debug 时的一些雕虫小技
通过二分法去定位问题是Debug的一般方法,即便如此,面对各种不同的编程语言,还有千姿百态的软件系统,我们很难有高效快速的通用方法。
但是, 有一些debug时的关注点,可以看作雕虫小技。
1.环境检查
复现一个Bug,一般从环境检查开始。从底层到应用层逐层检查,要耐心确认。同时,关注帮助文档、手册或数据表,关注工作目录或者运行环境的路径。
2. 日志检查
不要害怕被大量的调试日志,看起来很吓人,但实际上是来帮忙的。大多数时候它会告诉我们真正的问题是什么和在哪里!在25年前,debug C 语言程序的时候,自己好像只能依赖printf来定位问题。
根据日志思考:什么导致了这个错误?错误是如何触发的?...
3. 代码核查
先看看拼写错误吧,typo 或者简单的语法疏忽有时不易发现。有时候括弧或者大括弧的幸运碰撞,有时候context成了content,fetch成了fecth,如果对自己的代码熟视无睹,可以让别人帮着看一眼。
同样,“以终为始”,先检查是否接收了正确的数据类型,一般的防御式编程都可以看到接收的参数或数据。如果接收方合乎预期,跟随调用链的脚步,看调用者的函数,一步一步逼近bug的所在地。
如果定位到了函数,尤其是那些执行的迭代方法(尤其如for、while、do等循环处理),尝试在console上显示一步一步地执行结果。
4. 定向验证
对指定的函数或者逻辑输入固定的数据,或者执行固定的代码来做定向验证,可以是正向的,也可以是逆向的。
如果增加了大量输入还看不到所需输出的话,需要确认一下这些代码真的在执行吗?虽然有了条件语句(如if,switch等),但实际上执行的是在这里么?在条件语句中注入简单的代码,比如打印一个简单的“hello world”,可以帮助您看到程序执行的流程。
当然了,如果有完整一点的单元测试,往往方便的多。
5. 注释代码
如果对于bug发生的逻辑模块位置迟迟不能定位,往往还要回归到通过二分法定位。逐行注释代码的方法可能是可行的,但不是最有效的方法。如果对问题所在没有什么想法的话,这可能是一条必经之路。
6. 手画流程
“好脑子不让烂笔头”,有时候,我们需要一支笔和一张纸,就可以从所有的技术细节中解放出来,就能从视觉上看到事情是如何运作的,把逻辑流程画出来能够帮助我们梳理和定位代码逻辑上的问题。这有点儿像反向工程,尤其对于那些不熟悉的代码,阅读代码并画出流程会提供较大的帮助。
随笔结语
“Zero Bug” 可能是程序员追求的目标,但现实中存在着较大的困难。程序员的日常离不开debug,宽泛一点说是trouble shooting(故障排除)。故障排除在很多时候依赖于经验,反复实践几乎是不二法门,但是,我们可以通过归纳总结自己的经验形成一个“心智模型”,可能是树状的,也可能是金字塔结构,也就是所谓的“套路”。
另外,向经验丰富的程序员学习“套路”也不失为一个有效的途径。
参考资料:
- https://www.techug.com/post/cutting-edge-debugging.html
- 吉冈弘隆、大和一洋、大岩尚宏,《Debug Hacks中文版:深入调试的技术和工具》,电子工业出版社,2011
- 张燕飞, 张春熙, 李宇明, et al. DBugHelper:分布式系统Debug协助工具[J]. 华东师范大学学报:自然科学版, 2016(5):153-164.