毕业超过十年了,感慨岁月无情。作为从事后台开发多年的程序员(之前在电信领域工作),我想简要分享一些常见的开发心得和调试手段。在互联网这么多年,我积累了很多经验,但总结得却很少。出于互联网精神,我希望这些经验能对你在互联网的另一端有所帮助。由于我主要从事C语言开发,所以本文主要介绍C语言常用的调试手段。
调试对于无数程序员来说都是一件麻烦的事情。几乎没有人能够保证自己写的代码没有任何错误,所以有问题就需要进行调试。那么如何进行调试呢?对于高手来说,可以进行反汇编,查看二进制代码;稍微低级一点的可以使用gdb调试工具,查看统计信息;再低级一点就是添加打印语句。还有更低级的方法吗?当然可以,自己写一个bug,让其他人来查找。调试的方法有很多种,长期掌握下来总能找到适合自己的方法。
而调试的目的是什么?就是要找到问题所在。一个高手曾经做过一个很好的比喻:你在找问题实际上就像福尔摩斯一样,为什么是福尔摩斯呢?想一想,在找问题的现场,合格的程序都有日志、内存转储、计数等基本的现场调查记录吧。嗯,如果什么都没有,那就是让写代码的人自己去查找。找问题就是在众多信息中抽丝剥茧,找到疑点,反复推演程序运行的代码,最终找到问题所在的那一行或几行代码。
这个过程很折磨人,当一无所获的时候,让人心烦意乱,食不知味。但是一旦找到问题,就像打了鸡血一样兴奋,自己也会陶醉其中。只有真正经历过折磨的人,才能体会到修改问题的滋味。
一个程序的开发大致需要经历以下两个阶段,才能最终上线发布。
功能开发阶段
在这个阶段,主要目标是根据业务需求进行程序开发。我们不仅仅是码农,仅仅是写一些if-else语句吗?写程序真的就是这么简单吗?如果是这样,那编程就会变得工厂化、枯燥无味。
做事情都应该有预见性,尤其是在编写程序时更应如此。经典的大学C语言教材中将程序定义为:程序 = 数据结构 + 算法。但在实际生产过程中,我们可以对商业程序进行以下补充定义,我认为更加合适:程序 = 数据结构 + 算法 + 业务逻辑(计算逻辑)+ 框架。
首先来说为什么需要补充业务逻辑,因为有意义的程序本身就是某种业务逻辑(计算逻辑)的抽象。完成这个业务逻辑才是最终的目的,请不要拿一些算法研究的代码和我争论。
实际上,作为开发人员,测试驱动开发(TDD)是一种很好的思考问题的方法。你可能听说过它,也可能有同事使用过,如果你觉得使用不好,我可以告诉你:应该是测试场景 + 场景驱动开发。对,只是在其中加入了"场景"这个宾语,当你进行开发时,就有了目的性和针对性。
任何一个业务逻辑都可以拆分为多个业务场景。逐个解决这些场景,逐个进行测试,开发其实就很简单了。听起来很简单,但整个过程中,需要将50%的时间用于思考解决问题的场景,20%的时间用于编码,30%的时间用于测试。实际上,你可以在任何时间进行思考(在休息时,地铁上,班车上等等),只要能保持足够的宁静,你就能清晰地思考整个业务逻辑,并明确分解为多少个业务场景。对于复杂的业务场景,建议适当地做笔记,从整体的业务逻辑考虑问题:你细化的结论是否符合所有的业务场景?不断修正,直到正确为止。
在具体编码时,根据我们之前的深思熟虑,每个细节已经很清楚了,采用迭代的方式,批量交付小的功能点就可以了。
对于功能开发阶段的总结可以用两个关键词来概括:TDD和迭代。需要更详细了解的同学可以自行使用百度或谷歌进行搜索。
功能调试阶段
调试的手段很多,可以阅读代码、打印日志、使用gdb、查看统计信息、使用coredump等,如果你有足够的精力,还可以进行白盒测试等。测试的目的很明确,就是确认代码是否按照正确的编码意图运行。对于自己编写的代码,自己调试起来是相对容易的,因为你清楚代码的本意以及出现了什么问题。
作为程序员的悲剧之一,就是不知道何时需要定位其他人编写的bug。在进行定位之前,你必须理解另外一个程序员编写这段代码的意图,否则就无法进行定位。了解其他人编写的代码的途径就是通过阅读代码了解大致思路,并通过日志、gdb或统计信息来补充对代码意图的更多细节,或者修正自己对代码的理解。
这个过程可能很枯燥,也可能很具有挑战性。试图通过各种迹象了解另外一个程序员编写代码的初衷和意图,有时候可能让人感觉像窥探别人的隐私一样!
实际上,我之前提到的只是调试的前提和初衷。
一个优秀的程序员会有很多调试技巧,也就是很多调试手段,用来获取所需的信息。信息获取越多,自然就越容易理解程序本身的意图。
关于调试工具的使用细节和说明,你可以自行使用百度或谷歌进行搜索。
我在这里简单地阐述一下我是如何调试程序的,以及我是如何理解各种工具的。欢迎大家指点和交流!
关于日志
如何打好日志绝对是一门学问。日志打印过多会影响后台程序的性能,而打印过少则无法定位问题。更糟糕的是,如果打印到空指针,有可能导致自己的程序崩溃。
因此,日志的技巧就是要少而且内容丰富。
如何做到少?其实就是要合并表达相同意思的打印语句。
能否减少打印语句,但仍能得到相同的表达结果?
能否在关键的异常位置添加统计信息(输出统计)?
能否不进行打印?
能否在内存中记录关键信息,在需要时控制打印时机?
而如何做到内容丰富?就是减少描述性词汇的打印,增加有用的程序运行信息。
方法有很多,大家可以多多思考。打印的优化是一个不断改进的过程,并非一蹴而就。我曾经遇到过一个高手,测试部门提出问题,这个高手从不去定位,他直接告诉测试人员:帮我执行一下软调,将收集的日志交给他分析,问题就能解决。
关于gdb
还有一个大牛说过:“我就是程序,程序就是我。”我经常使用gdb来检验我对程序的理解。常用的gdb功能是打印程序的运行信息、修改内部运行信息以及构造复杂的场景。
实际上很简单,只需要知道程序在什么场景下应该有什么行为,我对程序的理解必须清晰。我需要知道关键变量的信息是否正确,周期性地使用gdb来确认变量的信息是否正确,然后决定程序是否按预期执行。
可靠的程序都有类似的保护机制,但通常需要繁琐的测试条件构造来触发保护机制(例如检测到丢包率很高时进行警告等)。实际上,大多数的保护机制都是在记录一些状态后触发的。
实际上,可以使用gdb构造异常状态,确认警告机制是否起作用。gdb非常好地补充了这方面的测试和验证工作。
关于统计
统计信息是汇集关键信息的最佳例子。数据量少,但信息明了。
在电信软件中,许多模块都通过统计信息来进行自证清白。通过统计信息很容易发现问题出现的位置。
统计的实质就是通过全局变量记录程序正常和异常点的统计信息,然后通过某种手段进行输出。
关于coredump
面对coredump很多人都会感到头痛,但实际上coredump也是定位问题的很好手段。
首先,程序在产生coredump之前会有详细的coredump文件,该文件详细记录了程序在coredump之前的运行信息。使用gdb可以查看coredump文件中的任何信息。这只是简单使用coredump的方法。
如果遇到复杂的问题,难以解决的问题,实际上也可以使用coredump进行定位。
比如程序执行到一个非常罕见的代码分支,然后出现coredump,但目前输出的信息(如日志等)根本无法进一步定位问题。
怎么办?有没有考虑过在复现问题的环节,提供一个调试版本的程序,在异常分支上主动触发内存异常,产生coredump,然后利用coredump信息来确定程序出现异常的方式。
关于代码修改
这也是我经常使用的一种手段,反复比较修改前后的代码,确认修改的准确性和全面性,反思自己的代码修改是否全面?这里使用的工具是Beyond Compare。