wenmo8 发布的文章

在我们对某个问题进行调试前一定要做足准备工作,不然后面的调试工作会面临极大的困难,甚至都无法开展调试工作。

必须要做的准备工作

不管我们是在开发期调试,还是在发布后调试,都要做好如下准备工作:

  1. 充足的心里准备
    这个太重要了,在实际工作中,我见到太多被问题吓怕的人。在还没整清楚问题是什么时就已经打了退堂鼓,把工作和问题给别人。面对任何问题,我们首先要做的就是树立起信心。特别是在计算机的世界里,事出必有因,且一定具有事情的发生的必然逻辑。所以,我们只要有信心就肯定能解决问题。
  2. 编写高质量代码
    程序开发者应该提供高质量的程序代码,包括规 范的代码和必要的注释,对开发的代码进行单元测试, 经过同行严格的代码评审。 这样一是减少问题发生,二是对调试定位问题和问题修改有很大的帮助。
  3. 了解软件的设计和算法,熟悉软件代码
    调试一个软件模块,需要了解它的设计和实现算 法,了解各个函数之间的调用关系,该模块与其他模块 之间的接口关系。
  4. 熟悉软件运行环境
    首先要明白我们软件要求的运行环境。了解用户机的环境,是否满足软件运行要求,排除一些运行环境引起的软件异常。同时随着硬件、操作系统、网络技术、云技术、大数据技术的发展,软件 运行环境越来越复杂,调试者只有熟悉这些环境和环 境配置,才能保证软件正常运行和调试。
  5. 熟悉调试工具
    调试工具提供很多功能来帮助我们调试分析程序,只有熟练掌握调试工具,才能开展我们的调试工作。
  6. 足够的日志输出
    日志的作用不用我多说,如果日志有足够的信息,我们甚至都可以不用调试器,都能定位问题和解决问题。
  7. 知道用户的操作流程
    某些问题更用户的独特使用习惯和操作步骤有关,如果我们不知道,很有可能复现不了问题从而无法解决。

开发过程中的调试准备工作

还在软件的开发过程中,我们就会遇到许多问题和异常,为了解决这些问题,我们需要做如下准备工作:

  1. 了解我们的工程属性配置
    要知道我们的工程属性做了哪些配置,特别是一些特殊的配置,最好知道没一个配置选项对我们程序行为的影响。要知道发生问题前配置有没有做修改,为什么修改。
  2. 知道代码组织结构
    只有知道代码组织结构,我们才能快速定位代码
  3. 准备充分必要的数据
    相当一部分问题可能是数据引起,我们只有有这样的问题数据,才能引发问题和调试定位问题。
  4. 了解代码里使用的第三方库的原理和使用要求
    如果不知道,可能会发生一些莫名其妙的问题,让我们无从下手。

软件发布后问题的调试准备工作

我们在开发时,严格尊搜了代码规范,进行了单元测试,也进行了组织内部的严格测试,但是,当我们的软件发布,功能上线后,用户还是会反馈很多问题,有的问题还非常致命。为了调试这些问题,我们要做如下准备工作:

  1. 做好代码发布分支管理
  2. 准被好符号文件
    如果是Windows上C++/.net开发的,一定要生成好符号文件,甚至是Map文件,且要做好符号版本管理
  3. 做好发布包版本管理
  4. 充分收集用户的相关信息
    应用版本、操作系统、硬件信息,使用流程、日志、注册表等一切相关信息

软件调试和版本管理的关系非常密切:

  • 在软件调试过程中可能有多种算法都可达到 预期的目标,但只能选择其中一种,这时需要保留各种 有价值的算法版本;软件调试完成后,需要进行代码优 化,在代码优化的过程中需要保留各种不同的版本;软 件调试完成后,需要增加功能和提升性能,在此基础上 开展下一步调试工作,需要保留调试好的软件版本。
  • 在多人开发同一个软件系统的过程中,需要 通过版本管理调试解决代码冲突问题。
  • 软件产品实际上是某个版本的软件产品,从 某种意义上来讲,软件产品打补丁和开发软件的新版本是更高层次的软件开发调试工作。
  • 某个具体问题,肯定是发生在某个具体版本上,当我们调试时,一定需要某个版本的源代码、发布包和符号文件

调试的关键在于推断程序内部的错误位置及原因,可以采用以下方法:

1、分析和推理

设计人员和开发人员根据软件缺陷问题的信息, 分析和推理调试软件。

(1)根据软件程序架构自顶向下缩小定位范围, 确定可能发生问题的软件组件。

(2)根据软件功能,软件运行时序定位软件问题。

(3)根据算法原理,分析和确定缺陷问题发生的 根源。

2、归纳类比法

归纳法是一种从特殊推断一般的系统化思考方法,归纳法调试的基本思想是:从一些线索(错误征兆)着手,通过分析它们之间的关系来找出错误。该方法主要是根据积累的工作经验和案例处理调试工作。

(1)根据工作经验和比对程序设计中类似问题的 处理方式进行调试工作。

(2)咨询相关部门和有经验的相关人员。

(3)查找相关文档和案例,为处理问题提供思路 和方法。 在软件开发过程中,通常对每个缺陷问 题进行跟踪管理,将解决问题的方案和过程详细记录。

(4)收集出错的信息,列出数据,包括输入,输出,归纳整理,发现规律,从线索除法,寻找线索之间的联系。也就意味着:从特殊到一般。归纳调试的步骤可以概括为以下一个图

3、跟踪回朔

在小程序中常用的一种有效的调试方法,一旦发现了错误,人们先分析错误的征兆,确定最先发现“症状“的位置然后,人工沿程序的控制流程,向回追踪源程序代码,直到找到错误根源或确定错误产生的范围,例如,程序中发现错误处是某个打印语句,通过输出值可推断程序在这一点上变量的值,再从这一点出发,回溯程序的执行过程,反复思考:“如果程序在这一点上的状态(变量的值)是这样,那么程序在上一点的状态一定是这样···“直到找到错误所在。

在软件开发通常采用基线与版本管理。 基线为 程序代码开发提供统一的开发基点,基线的建立有助 于分清楚各个阶段存在的问题,便于对缺陷问题定位。 软件版本在软件产品的开发过程中生成了一个版本 树。 软件产品实际上是某个软件版本,新产品的开发 通常是在某个软件版本的基础上进行开发。

(1)开发过程中发现有问题,可以回退至版本树上的稳定版本,查找问题根源。

(2)通过基线版本序列可以追踪产品的各种问 题,可以重新建立基于某个版本的配置,可以重现软件 开发过程中的软件缺陷和各种问题,进行定位并查找 问题根源。

4、增量调试

软件开发大多采用软件配置管理和持续集成 技术。 开发人员每天将评代码提交到版本库。 持续集 成人员完成集成构建工作。 可以通过控制持续集成的 粒度(构建时间间隔),控制开发人员提交到版本库的 程序代码量,从而便于对缺陷问题定位。 通常每天晚 上进行持续集成工作,发现问题时,开发人员实际上只需要调试处理当天编写的代码。

5、写出能重现问题的最短代码

采用程序切片和插桩技术写出能重现问题的最短 代码调试软件模块。

(1)程序切片
程序切片是通过在特定位置消除那些不影响表达 式计算的所有语句,把程序减少到最小化形式,并仍能 产生给定的行为。 使用切片技术,可以把一个规模较 大并且较复杂的软件模块转换成多个切片程序。 这些 切片程序相对原来的程序,简单并且易于调试和测试。

(2)程序插桩
程序插桩方法是在被测程序中插入某些语句或者 程序段来获取各种信息。 通过这些信息进一步了解执 行过程中程序的一些动态特性。 一个软件组件的独立 调试和测试需要采用插桩技术,该组件调用或运行需 要桩模块。 在软件模块的调试过程中程序切片和程序插桩可 以结合起来使用。

6、日志追踪技术

日志是一种记录机制,软件模块持续集成构建过 程中,日志文件记录了有用信息。 若构建失败,通过查 看日志文件,将信息反馈给相关人员进行软件调试。

7、调试和测试融合的技术

(1)测试驱动开发

测试驱动开发是一种不同于传统软件开发流程的 开发方法。 在编写某个功能的代码之前先编写测试代 码,然后编写测试通过的功能代码,这有助于编写简洁 可用和高质量的代码。

(2)开发与测试融合

程序开发人员除了进行程序代码的编写,白盒测 试,也要完成基本的功能测试设计和执行。 这样有助 于程序开发人员更好地开展调试工作。 程序开发人员 可以通过交叉测试来解决测试心理学的问题(不能自 己测试自己)。 采用这种模式测试人员的数量会减少,专业的测 试人员去做其他复杂的测试工作。 研发中的很多低级 缺陷会尽早在开发过程中被发现,从而减少缺陷后期 发现的成本。

8、强行排错

这种调试方法目前使用较多,效率较低,它不需要过多的思考,比较省脑筋。例如:

(1)通过内存全部打印来调试,在这大量的数据中寻找出错的位置。

(2)在程序特定位置设置打印语句,把打印语句插在出错的源程序的各个关键变量改变部位,重要分支部位,子程序调用部位,跟踪程序的执行,监视重要变量的变化

(3)自动调用工具,利用某些程序语言的调试功能或专门的交互式调试工具,分析程序的动态过程,而不必修改程序。

应用以上任一种方法之前,都应当对错误的征兆进行全面彻底的分析,得出对出错位置及错误性质的推测,再使用一种适当的调试方法来检验推测的正确性。

9、演绎法调试

演绎法是一种从一般原理或前提出发,经过排除和精华的过程来推导出结论的思考方法,演绎法排错是测试人员首先根据已有的测试用例,设想及枚举出所有可能出错的原因作为假设,然后再用原始测试数据或新的测试,从中逐个排除不可能正确的假设,最后,再用测试数据验证余下的假设确是出错的原因。

(1) 列举所有可能出错原因的假设,把所有可能的错误原因列成表,通过它们,可以组织,分析现有数据

(2) 利用已有的测试数据,排除不正确的假设

仔细分析已有的数据,寻找矛盾,力求排除前一步列出所有原因,如果所有原因都被排除了,则需要补充一些数据(测试用例),以建立新的假设。

(3)改进余下的假设

利用已知的线索,进一步改进余下的假设,使之更具体化,以便可以精确地确定出错位置

(4)证明余下的假设

这就个规则来自于书籍《调试九法:软硬件错误的排查之道》,记录下来:

规则1:理解系统

你必须掌握系统的工作原理以及它是如何设计的,在某些情况下还要知道为什么这样设计。如果你没有理解系统中的某个部分,那么这通常是出问题的地方。(这不仅仅是墨菲定律的问题,如果你不能理解你所设计的系统,你的工作可能会变得一团糟)。

如何理解系统呢?

  • 阅读手册
  • 逐字逐句阅读手册,仔细理解每个细节
  • 知道什么是正常的,知道什么是正常的可以帮助你注意到什么是不正常的
  • 知道工作流程,要理解业务,要讲系统的工作过程对应到具体要解决的现实问题
  • 选择合适的工具,选择合适的辅助(监控、插桩)工具可以帮你理解系统
  • 查阅细节,经验有时候会骗人,记忆有时候会出错

规则2:制造失败

这一点比较容易理解,就是问题复现,在日常工作中,你在排查一个问题的过程中,最重要的一步就是复现问题——能复现的问题都能解决。

这里有几个要点需要注意:

  • 引发失败,而不要模拟失败,不要尝试用不同的方式去模拟问题,而要模拟和构建引发bug发生的条件
  • debug的动作,不要影响错误的发生方式,可以影响错误的发生频率
  • 从头开始,需要有一个正常的状态到不正常的状态的过程,从开始正常的状态开始观察,直到问题发生;
  • 终极方案,控制变量法,将可能引发错误的因素依次排除;排除所有可能的原因后,剩下那个答案,无论多么不可思议,都是事实。

规则3:不要想,而要看

亲眼看到底层的失败是非常重要的,如果你猜测失败是如何发生的,那常常会修复一些根本不是bug的问题。

在软件世界里,观察意味着设置断点、添加调试语句、监视程序值以及检查内存;在医学领域,需要测试血样和进行X光透视。

对细节的观察应该到什么程度合适呢?简单的答案是:一直观察,直到把问题的原因锁定在几种可能之内。

在系统设计的时候,就要考虑到将来调试、排查问题的情况,将日志视为系统设计的一部分—打印一些关键日志,或者设计一些打开日志的开关,以便在生产环境针对某个case进行调试。

日常生活中有很多插桩的case:

  • 体温计测量体温
  • 自行车轮胎漏气时,都是将轮胎打满气,然后放在水里检查哪里漏气
  • 天然气中加入了臭鸡蛋的气味

规则4:分而治之

反复将问题分成好的一半和坏的一半,然后缩小搜索范围,然后进一步研究有问题的那一半链路。

规则5:一次只改一个地方

初中就学过的控制变量法。 在修改bug时候,如果某个改动没有修复bug,就应该立即把它改回来。

规则6: 保持审计跟踪

记下你的每步操作、顺序和结果; 魔鬼藏在细节中; 将一些事情关联起来思考; 好记性不如烂笔头;

规则7:检查插头

一些显而易见的假设可能是错误的;是不是运行了正确的代码?是不是打了正确的包?插头是不是掉了?从一些最基本的问题开始确认,很多时候问题就出在这里。对自己使用的工具进行测试,因为工具也是一种软件,难保不会出问题。

规则8:获得全新观点

向别人解释问题的过程,会让你对问题进行重新的梳理和理解,这时候可能发现之前没有发现的问题。

bug发生了,以除掉bug为自豪,而不是非得以自己除掉bug才自豪。

不管你是跟什么人求助,或者需要别人什么样的帮助(征求意见、获取专业知识、听取经验),在向别人描述问题的时候,一定要记住一件事——报告症状、而不是讲你的理论;另外,有些症状你可能不是十分确定,也可以描述出来。

规则9:如果你不修复bug,它将依然存在

如果你不修复bug,它不会自动消失。按照前面的规则解决问题后,要进行一次回归验证,确保已经修复问题,并且没有引入新的问题。

《调试九法:软硬件错误的排查之道》