跳转到内容

计算机编程原理/维护/调试

来自维基教科书,开放的书籍,开放的世界

调试是诊断程序中的错误并确定如何纠正它们的艺术。 “错误”有多种形式,包括:编码错误、设计错误、复杂交互、用户界面设计不良以及系统故障。 因此,学习如何有效地调试程序需要您学习如何识别您所面临的哪种问题,并应用适当的技术来消除问题。

错误在整个软件生命周期中都会出现。 程序员可能会发现问题,软件测试人员可能会识别出问题,或者最终用户可能会报告意外结果。 有效调试的一部分包括使用适当的技术从不同的问题报告来源获取必要的信息。

编程中最常见的错误类型是

  • 不假思索地编程
  • 以非结构化的方式编写代码

详细的错误

[编辑 | 编辑源代码]

那么这些不同类型的错误是什么呢?

对于编码错误,问题来源在于程序员造成的错误或不正确的代码。 编码错误的一些例子包括

  • 无视采用的约定。
  • 调用错误的函数(“moveUp”,而不是“moveDown”)
  • 在错误的位置使用错误的变量名(“moveTo(y, x)”,而不是“moveTo(x, y)”)
  • 未能初始化变量(“y = x + 1”,其中 x 未设置)时绝对需要。
  • 跳过对错误返回的检查。

软件用户很容易看到一些设计错误,而在其他情况下,设计缺陷会使程序更难改进或修复,并且这些缺陷对于用户来说并不明显。 明显的设计缺陷通常由超出计算机限制运行的程序证明,例如可用内存、可用磁盘空间、可用处理器速度以及压倒性的输入/输出设备。 更难的设计错误分为几个类别

  • 未能隐藏复杂性
  • 不完整或模棱两可的“契约”
  • 未记录的副作用

复杂交互错误出现在单个程序的多个部分、多个程序或多台计算机交互的场景中。

用户界面设计不良通常会导致用户以实现与其意图不同的方式使用该程序。 例如,网站的“搜索”页面可能有一个“不区分大小写”搜索选项。 当用户难以找到或看到该选项时,该用户可能会报告一些数据“丢失”的错误,仅仅因为案例敏感搜索未找到该数据。

有时,计算机硬件 simply fails,并且通常以完全出乎意料的方式出现故障。 确定问题不在于软件本身,而在于其运行的计算机(s)通常很复杂,因为调试软件的人员可能无法访问显示问题的硬件。

预防错误

[编辑 | 编辑源代码]

关于软件调试的讨论,如果没有讨论如何首先预防错误,那就不会完整。 无论你写代码写得多好,如果你写错了代码,它对任何人都没有帮助。 如果你创建了正确的代码,但用户无法使用用户界面,那么你可能没有编写代码。 简而言之,一个好的调试器应该对问题可能存在的位置保持开放的态度。

虽然讨论各种避免错误的技术超出了本次讨论的范围,但这里提到的许多技术在事后同样有用,当你遇到错误需要发现并修复它时。 因此,下面将简要介绍一下。

理解问题

[编辑 | 编辑源代码]

为了编写有效的软件,开发人员必须解决用户需要解决的问题。 自然地,用户不会以严格的算法、窗口系统、网页或命令行界面进行思考。 相反,用户可能不会以与开发人员思考问题相同的方式思考问题。

为了解决这种差异,请与目标用户坐在一起,询问他们希望从软件中获得什么。 用户经常希望软件能够提供的比实际所能提供的更多,或者拥有相互矛盾的目标,例如功能更强大的软件,但不需要他们学习任何新东西。 简而言之,询问用户他们的目标是什么。 如果没有这些目标,用户将继续报告没有形成连贯整体的错误。

有效流程

[编辑 | 编辑源代码]

开发工具

[编辑 | 编辑源代码]

单元测试

[编辑 | 编辑源代码]

单元测试是指检查当前模块可以进入的所有可能状态中会发生什么。 因此,您应该准备一个“测试列表”,其中定义了当前模块的所有可能输入。
例如:我们有一个程序,它从用户那里获取正数并对其进行处理。 首先,我们需要检查输入是否为数字(它可以是字符),然后我们将检查它是否为正数。 通过检查,我的意思是输入输入并查看会发生什么。
提示:当您开始编写此测试列表时,您会注意到预测所有可能性相当困难; 如果您有选择询问其他人(没有帮助编写模块)来提供帮助,这可能会卓有成效。

代码文档

[编辑 | 编辑源代码]

基本的调试步骤

[编辑 | 编辑源代码]

虽然每次调试体验都是独一无二的,但在调试时可以应用某些一般原则。 本节特别讨论调试软件,尽管这些原则中的许多原则也可以应用于调试硬件。

调试的基本步骤是

  • 识别错误的存在
  • 隔离错误来源
  • 确定错误原因
  • 确定错误修复方法
  • 应用修复并进行测试

识别错误的存在

[编辑 | 编辑源代码]

检测错误可以主动进行,也可以被动进行。

经验丰富的程序员通常知道错误更有可能发生在哪里,这取决于程序部分的复杂程度以及可能的数据损坏。例如,从用户那里获得的任何数据都应该被视为可疑。应该非常小心地验证数据的格式和内容是否正确。从传输中获取的数据应该被检查以确保接收到了整个消息(数据)。必须解析和/或处理的复杂数据可能包含未预料到的值组合,并且没有被正确处理。通过插入对可能错误症状的检查,程序可以检测到数据何时被破坏或未被正确处理。

如果错误严重到导致程序异常终止,则错误的存在就会变得很明显。如果程序检测到不太严重的问题,则可以识别出错误,前提是监控错误和/或日志消息。但是,如果错误很小并且只会导致错误的结果,则很难检测到错误的存在;如果难以或不可能验证程序的结果,这尤其如此。

此步骤的目的是识别错误的症状。观察问题的症状,在什么条件下检测到问题以及发现了哪些解决方法(如果有的话)将极大地帮助其余步骤调试问题。

隔离错误源

[编辑 | 编辑源代码]

此步骤通常是调试中最困难(因此也是最有意义的)步骤。其目的是确定系统中哪个部分导致了错误。不幸的是,问题的根源并不总是与症状的根源相同。例如,如果输入记录已损坏,则在程序处理不同的记录或执行基于错误信息的某些操作时,可能不会发生错误,这可能在读取记录很久之后才会发生。

此步骤通常涉及迭代测试。程序员可能会首先验证输入是否正确,然后验证是否正确读取了输入,是否正确处理了输入等等。对于模块化系统,通过检查不同模块之间接口传递的数据的有效性,此步骤可能更容易。如果输入正确,但输出不正确,则错误的根源位于模块内。通过迭代测试输入和输出,调试人员可以在几行代码内识别出错误发生的位置。

熟练的调试人员通常能够假设问题可能出在哪里(基于与以前类似情况的类比),并测试程序可疑区域的输入和输出。这种形式的调试是科学方法的实例。不太熟练的调试人员通常会按顺序逐步执行程序,查找程序的行为与预期不同的位置。请注意,这仍然是一种科学方法,因为程序员必须决定在寻找异常行为时检查哪些变量。另一种方法是使用“二分查找”类型的隔离过程。通过测试数据/处理流程中间部分附近的区域,程序员可以确定错误是发生在程序的较早部分还是较晚部分。如果未检测到任何数据问题,则错误可能在流程的后面。

识别错误原因

[编辑 | 编辑源代码]

找到错误的位置后,下一步是确定错误的实际原因,这可能涉及程序的其他部分。例如,如果已经确定程序出现故障是因为某个字段错误,则下一步是确定为什么该字段错误。这是错误的实际根源,尽管有些人会争辩说,程序无法处理错误数据也可以被视为错误。

对系统的良好理解对于成功识别错误的根源至关重要。经过训练的调试人员可以隔离问题的根源,但只有熟悉系统的人才能准确地识别错误背后的实际原因。在某些情况下,它可能是外部的:输入数据不正确。在其他情况下,它可能是由于逻辑错误,即正确的数据被错误处理。其他可能性包括意外值,即最初的假设是给定字段只能具有“n”个值,而实际上它可以具有更多值,以及不同字段中意外的值组合(字段 x 只应该在字段 y 是其他东西时才具有该值)。另一种可能性是错误的参考数据,例如包含与损坏记录相关的错误值的查找表。

确定错误原因后,最好检查代码的类似部分,以查看是否在其他地方重复了相同的错误。如果错误明显是打字错误,则可能性较小,但如果原始程序员误解了初始设计和/或需求,则可能在其他地方犯下了相同或类似的错误。

确定错误修复

[编辑 | 编辑源代码]

确定了问题根源后,下一步是确定如何修复问题。除了最简单的问题外,对现有系统的深入了解至关重要。这是因为修复将修改系统的现有行为,这可能会产生意想不到的结果。此外,修复现有错误通常会创建额外的错误,或者暴露程序中已经存在的其他错误,但由于原始错误,这些错误从未暴露出来。这些问题通常是由程序执行以前未经测试的代码分支或在以前未经测试的条件下执行的代码分支引起的。

在某些情况下,修复简单而明显。对于逻辑错误,尤其如此,在这种情况下,原始设计被错误地实现。另一方面,如果问题揭示了贯穿系统很大一部分的主要设计缺陷,那么修复的范围可能从困难到不可能,需要对应用程序进行彻底重写

在某些情况下,可能希望实现“快速修复”,然后进行更永久的修复。此决定通常是通过考虑问题的严重程度、可见性、频率和副作用以及修复的性质和产品时间表(例如,是否有更紧迫的问题?)来做出的。

修复和测试

[编辑 | 编辑源代码]

应用修复后,重要的是测试系统并确定修复是否正确地处理了以前的问题。测试应完成两个目的:(1) 修复是否现在可以正确地处理原始问题,以及 (2) 确保修复没有创建任何不良副作用。

对于大型系统,最好有回归测试,一系列测试运行来练习系统。在进行重大更改和/或错误修复后,可以随时重复这些测试以验证系统是否仍按预期执行。随着新功能的添加,可以将其他测试包含在测试套件中。

减少调试的步骤

[编辑 | 编辑源代码]

可以采取具体步骤来减少花费在调试软件上的时间。这些在下面的部分中列出。

正确的思维方式

[编辑 | 编辑源代码]

当你开始调试程序时,你能做的最重要的事情可能是意识到你并不理解发生了什么。确信自己的程序应该正常工作的程序员不太可能找到错误,仅仅是因为他们拒绝承认自己的困惑。如果程序按照你认为的方式运行,你就不会调试,程序会正常工作。即使程序看起来正常,如果你以至少存在一个错误并且你会找到它这样的想法来检查它,那么你就更有可能发现程序的错误。

从源头开始

[编辑 | 编辑源代码]

当你最清楚问题可能出现的地方通常是在最初设计和编写代码的时候。通过在程序的不同位置插入完整性检查,程序本身可以检测和报告问题。除了检测问题之外,还应考虑如何最好地处理每个错误。选项包括

  • 报告错误,将无效字段设置为默认值,并继续
  • 报告错误,丢弃与无效值关联的记录,并继续
  • 报告错误,将无效记录转移到单独的文件/表中,以便用户可以检查并可能纠正问题
  • 报告错误并终止程序

怀疑用户输入

[编辑 | 编辑源代码]

来自用户(包括外部系统)的任何数据都应受到怀疑。仔细验证所有此类输入数据,执行语法和语义完整性检查。这种无效数据是编程错误的常见来源。不要只考虑错误输入的数据,还要考虑恶意数据,例如 缓冲区溢出 利用。

如果数据是用户交互式输入的,你可以提供适当的 错误消息 并允许用户更正无效字段。如果数据不是来自交互式源,则应按照上述方法处理错误记录。

使用日志文件

[编辑 | 编辑源代码]

将信息写入日志文件的程序可以提供重要的信息,这些信息可用于分析在遇到问题之前、期间和之后发生的事情。通过创建各种日志文件可以减少要搜索的条目数量,例如为系统的每个主要组件创建一个单独的日志,另外再创建一个专门用于错误的日志文件。每个条目都应进行日期/时间标记,以便可以将来自不同日志的条目关联起来。

测试套件

[编辑 | 编辑源代码]

一组标准测试,可以运行这些测试来执行 回归测试,可以帮助在错误进入生产环境之前找到它们。这些测试用例应尽可能自动化,以减少执行这些测试所需的精力。随着向系统添加新功能,应创建其他测试来测试这些功能。

一次更改一项

[编辑 | 编辑源代码]

进行大量更改时,应增量应用它们。添加一项更改,然后彻底测试该更改,然后再进行下一项更改。这将减少新错误的可能来源。如果同时应用了几个不同的更改,那么就很难确定问题的来源。此外,不同区域的轻微错误会相互作用,产生可能在逐个应用更改时永远不会发生的错误。

撤销没有效果的更改

[编辑 | 编辑源代码]

如果你做了一个更改来修复一个问题,但程序的行为仍然相同,在继续之前撤销这些更改。你的更改没有产生任何效果,表明以下几种情况之一

  • 问题不在你认为的地方
  • 你修改的区域要么没有被调用,要么没有按照你认为的方式被调用
  • 假设你更改的部分没有执行,你可能引入了新的错误,这些错误只有在修复当前错误后才会出现

尝试应用程序的另一个端口

[编辑 | 编辑源代码]

在不同架构下可用的程序(例如,MS Windows、MacOSX、Linux 等操作系统或 Intel Pentium、PowerPC 或 DEC Alpha 等处理器)有时在其他系统上会有不同的反应(尤其是在后续错误方面)。有时在不同的架构上找到错误要容易得多。

考虑类似的情况

[编辑 | 编辑源代码]

当发现一个错误时,考虑可能出现相同错误的其他地方。检查这些地方,看看是否存在相同的问题。

查找用户界面错误

[编辑 | 编辑源代码]

查找设计错误

[编辑 | 编辑源代码]

查找编码错误

[编辑 | 编辑源代码]

并非每种类型的程序都以相同的方式进行调试,并非所有技术都可以用于所有类型的程序。

调试的主要角色是调试器。它是一个与新编写的程序同时运行的软件,允许你暂停程序并读取内存地址、堆栈以及程序的其他通常不可见部分。

另一种调试方法是日志文件。输出某些变量的内容可以提供有关程序如何执行的有价值的信息。输出包含函数名称的字符串(在调用函数时)可以在定位错误引入的位置方面非常有用。为了找到程序崩溃的位置,使用调试器更加实用。

大型程序难以调试,小型程序(相对)容易调试。因此,关键是将大型程序变成许多小型程序来进行调试。这被称为“单元测试”,它涉及将程序的一部分(一个例程、一组相关例程、一个模块甚至一个完整的子系统)与额外的代码一起编译,以允许它在没有其余代码的情况下运行。

全屏应用程序(尤其是游戏)难以调试,因为你无法看到调试器的输出。解决方案在于使用空闲调制解调器电缆、第二台计算机和一个终端程序(例如 Hyper-terminal)。通过空闲调制解调器电缆将调试器的输出管道到第二台计算机。

e.g. In Dos with gdb using a serial null modem cable:
Configure the port with mode: mode COM2: 9600,n,8,1,none
Pipe the output to COM2 by adding >COM2 when you invoke the debugger.

进一步阅读

[编辑 | 编辑源代码]

大多数语言都支持自己的特殊调试技术

某些平台有特殊的调试技术

华夏公益教科书