《程序调试思想与实践》.(The.Science.of.Debugging)读书笔记

来源:互联网 发布:淘宝照片拍摄技巧 编辑:程序博客网 时间:2024/05/29 08:36

全书以bug为中心,围绕调试维护进行组织。

1 bug

1.1 关于bug

1、作为一个词,bug被软件开发组织用来描述一些需要修复的问题。

2、Bug的定义

  • 简单定义:bug是未预料到的系统行为。
  •  严格定义:bug是系统中的这样一个现象,即本来应该有的功能却没有。
  • 以人为本的定义:bug是系统没有实现软件开发人员(开发者、测试人员、项目管理者)和用户之间达成一致的行为。

3、软件缺陷(defect)

软件缺陷是一个软件系统中的需求、体系结构、设计和应用上的错误。

bug是软件缺陷的实际证明。

4、调试(debugging)

调试是理解系统的行为以利于去除bug的过程。

5、Bug是如何产生的

  • 对软件做修改,主要原因是对被修改的软件理解力不够。文档是一个书面报告,能够帮助描述软件系统的使用、设计、目标和描述。代码是软件的最后仲裁工具,所以源代码也是文档的一部分。所以修改代码,也应该同步修改代码中相应的注释。
  •  一个不正确的描述会导致不正确的实现,从而导致bug。

6、Bug的种类

  • 大量研究表明,需求阶段的bug修复代价最高,跟踪难度最大。理由很简单,如果一个bug存在于需求阶段,那就没有“问题代码”可以检测。
  • 设计阶段的bug比需求阶段的bug更容易捕捉,但最后的修复代价很高。设计上的bug的修复代价高的主要原因是它们作为一个整体来影响系统。
  • 实现阶段的bug是系统中最普通、最一般、最容易修复的bug。
  • 处理阶段的bug,比如被破坏的数据库。
  • 编译的bug:编译时出现的bug是因为编译过程中出现错误,或编译引擎出现了一个导致产品失效的错误。
  • 配置的bug,比如在一个配置bug中,软件是正确的,编译过程是正确的,但最后的软件安装不正确。
  • 未来计划bug:如果设计者或开发人员能够看得长远一点,则未来计划bug就要可以避免。
  •  文档bug,系统与文档描述不一致。

7、Bug的分类

  • 内存或资源泄漏:指一个内存由操作系统或内部存储器”池”分配,但用完后从未收回。症状往往是系统变慢或者突然崩溃以及其它奇怪的症状。
  •  逻辑错误:即代码的语法正确却没有做到期望的事情。
  • 循环错误:死循环。
  • 条件错误:条件错误可以是对布尔代数的误解或条件嵌套错误造成的。
  • 多线程错误:在一个实现了多线程的程序中,两个线程试图同时访问或修改相同的存储器地址。
  • 存储错误:这是在永久存储设备遇到错误不能再进行时发生的问题。
  • 集成错误:是指两个子系统已经分别测试和验证,组合后在其交互时出现了问题。
  • 转换错误:转换错误发生在将一个类型的数据转换为另一类型发生错误时。
  • 版本bug:版本bug是在软件的两个版本之间改变了功能和文件存储格式时,结果向后不兼容或在将来的发布中没有正确处理。
  • 不恰当地重用bug:大量的bug是因为对代码或组件在新系统中不正确地重用而造成的。
  • 布尔bug:最明显的症状就是程序做了与期望相反的事情。

2 调试工具及使用时机

1.1 测试和调试环境

1、第一个调试工具是测试环境。测试环境包括一套硬件、软件和测试实例,可用于模拟、调查和重现用户所看到的问题。没有固定的方法来判断问题的实质,只能是在黑暗中摸索寻找错误的原因。测试实例应该是让bug最容易重现的实例。根据这个思想,可以利用每一个bug作为新的测试实例。一般情况下,测试实例是根据要求和设计规范写成的。

2、如果想知道应用程序如何工作,以及程序各段如何组织在一起时,使用日志是一个很好的方法。

1.2 中级调试技术

1、内存漏洞检测工具

2、交叉索引及工具用法

3、调试器

4、同用户一起工作

5、编译器:检查代码最好的工具就是在所工作的编译器上使用最高警告级别。

1.3 日志文件

一个日志文件至少要包含以下可用的信息:

1、事件的日期和时间;

2、系统产生事件的函数、方法和过程的名字;

3、事件的严重性水平;

4、被记录问题的描述;

5、事件发生时相关变量的值。

1.4 形成假设

一般情况下,bug属于以下种类:

1、内存错误;

过量的分配用尽了内存,可能是没有释放程序中分配的内存,或某块被分配的内存被重新分配但原来的指针没有清楚等。对这些情况的跟踪,通常是通过使用某种工具或寻找程序中内存的所有释放来检查内存,以保证那些指向内存块的指针被清除。

2、编码错误;

编码错误表示没有在代码中把一些东西做正确,可能是调用函数、过程、方法时使用了无效的参数或向将要加载的外部文件发送了错误的数据。最简单的编码错误是未初始化的变量、没有检查空指针、向函数发送无效参数、没有检查数学错误等。

3、逻辑错误;

逻辑错误只是实际程序逻辑中的错误,而不是代码本身的错误。

4、需求错误;

需求错误是系统的需求文档中的基本错误。必须重新编写代码的错误的核心部分,或者对代码做出纠正,以使得程序按照用户的期望去做。

5、没有检查错误条件和返回值;

不检查返回代码,只会导致难以跟踪、甚至难以重现问题。

6、定时/多线程错误;

如果程序在访问全局变量或共享的内存时发生崩溃,就有可能是遇到了多线程问题。

7、外部产生的错误。

1.5 回归测试

回归测试应采取两种形式,第一,应该有自动程序来测试最通常的方面。第二,整个系统应该确认因修复bug所作的改变没有产生新的bug。

3 分布式系统

分布式系统是在不同的构件上运行的系统,也可能是在不同的机器上运行的系统。分布式系统具有不确定性,因为有很多进入和退出系统的途径。另外,分布式系统经常使用多种语言、主机和操作系统,使用中间设备来连接这些节点。

1.1 中间设备错误

在你自己的系统或不正确调用中间系统的构件中的实际代码中产生了bug,就会出现中间设备错误。在中间设备构件中的bug事很可怕的。因为几乎没有构件的源代码,只有两种调试办法。第一,去掉这个构件,用完成相同功能的构件来代替它,这通常需要重写大量的代码并改变系统的体系结构。第二,围绕问题,去掉那些引起问题的构件的某些方法。在这个情况下,就要通过删除代码来测试系统的功能是否和原来一样。避免中间设备构件bug最好的方法,就是在系统使用构件之前,对构件的每个方法进行全面的测试。

1.2 预测错误

预测就是在给定的条件下推断将会发生的情况。当开发者事先推测部分系统的加载和运行情况,并且写下了依赖顺序的代码就会出现预测错误。如果依赖顺序改变或不像事先预测那样,就会出现很难调试的错误。

1.3 连接错误

连接错误是分布式系统中最普遍的问题。在一个构件中使用另一个分布构件时,这类问题就有可能产生。

1.4 安全错误

当安全策略被破坏或构件试图使用没有授权的资源,就会出现安全错误。在很多情况下,安全错误是部分的错误操作引起的。

1.5 信息数据库

当在分布式网络的多机系统上运行构件时,就会碰到信息数据库问题,这个问题的最普遍形式是windows的注册表问题。

1.6 记录的事后调查分析

解决这类问题的最好资源就是系统日志。对于分布式系统,绝对需要大量的日志系统和记录收集机制。

4 bug跟踪

从管理者的角度,bug跟踪系统能帮助控制bug,理解系统的稳定性,并保持跟踪严重的问题。管理者也需要解决bug的附加信息。以下列出一些可能需要的信息。

1、bug标识符——可以用来对bug进行分类的一些标识符。这些标识符可以用来跟踪因为bug而修改的代码或文件。

2、报告数据——关于bug的数据和输入系统的一些数据。

3、Bug状态——标识bug在系统中运行的状态。一个bug的报告需要一个新的状态。一个分配给调试者的bug可能有一个”指派”的状态,”解决”状态用来标识被解决和证实的bug。

4、Bug分配——标识被分配解决bug的开发者。

5、开始数据——标识调试者开始调试过程的数据。

6、严重性——描述bug的严重性。

7、工程/应用程序名——标识发现bug的工程名。

8、报告者——标识报告bug的人。

9、系统名——标识出现bug的应用程序运行的软、硬件系统。

10、构件名——标识出现bug的应用程序的软硬件构件。

11、文件修改——标识为解决bug而进行修改的文件。

12、重现——标志在可重现bug的步骤。这些步骤将成为测试用例,可以作为回归测试程序的一部分。

13、环境——标识硬件平台、操作系统和其他相关环境参数。

14、描述解决——标识用来解决问题的方法,应当用自然语言来书写。

15、解决数据——标识用来解决bug的数据。

16、测试者——标识验证解决的测试人员。

17、测试数据——标识用来验证解决的测试数据。

18、测试结果——标识验证的结果。

19、回归测试需求——标识回归测试所需要的一系列需求。

20、负责回归测试的测试人员——标识负责回归测试的测试者。

21、回归测试结果——标识回归测试的结果。

5 需求中的bug预防

需求文档是软件产品的客户和开发团体之间的第一个文档。它帮助客户同意软件的正确行为。如果一个动作和需求阶段的要求不同,那么这个行为就是一个bug。

一个典型的软件开发过程包括需求收集阶段、设计阶段、开发阶段和测试阶段。

软件工程师不仅仅是一个程序员,他们还是体系结构设计师、设计者、开发者和问题的解决人员。

需求是软件的可测试行为,一个好的需求和测试计划没什么不同,这就意味着测试者特别适合协助和评估需求文档。在这种情况下,测试者应该充当一个检查者的角色,依靠他们的经验和知识使需求更加具体。

6 堆栈溢出错误

当一个函数被调用,那么函数的参数和局部变量将被置入栈,当函数退出时这些参数和局部变量也从栈中移出。堆栈存储的容量通常是有限制的,当堆栈满时,因为没有地方存储局部变量就会出现堆栈移除错误。

递归的一个显然风险就是堆栈溢出错误。因为每个递归将会使堆栈的层次增多。结果,将会用完堆栈空间,程序崩溃。

7 测试

没有测试,就只能解决很少的一些bug;没有调试,测试者就发现不了系统性能的提高。也没有新的测试内容。测试是发现bug的过程,相反,调试是解决bug的过程。两者缺一不可。

从一个专业调试者的角度出发,有三个不同类型的测试:单元测试、验证测试和QA测试。前两个测试包含在调试者的领域,第三个测试是一个外部种类的测试。

1.1 单元测试

单元测试在单个模块中由模块的初始开发者完成。单元测试是向开发者证明单元是按设计工作的。总的来说,单元测试和任何其他的软件模块都没有关系。单元测试只要求模块包含了被测试的真实模块。

单元测试不仅仅是用来证明代码确实在没有遵循适当的调整顺序时正确地报告错误,也用来作为一个文档的例子,解释最后的开发者如何适当地使用代码。

单元测试的另一个重要特征是研究所测试的函数或方法的接口,并证实你不仅选择了合法的输入类型,也清楚使用了非法的数据类型。

单元测试最主要的功能,是证明已经检查代码中的每个分支。通过执行代码的所有分支,确保对代码进行了全面的检查。

1.2 验证测试

验证测试是测试的最基本形式,验证测试就是验证代码是否做了应当做的事。对于一个给定模块的接口,验证测试是简单地证明使用正确的输入,模块将会产生正确的输出。

此外,像单元测试一样,验证测试为调试者、维护者和模块的重用者提供了一个生成文档的好工具。

1.3 质量保证测试

质量保证(quality assurance,QA)测试,QA是由QA人员执行的机构测试,QA测试是为了发现软件是否能够工作,也为了证实软件不会做不支持的工作。关于QA测试,程序员当心软件会不会像说明书那样执行,但是QA人员更关心的是用从来就不希望的方法使用软件。

1.4 测试方法

和调试不同,测试对问题没有简单的答案。在调试时,可以寻找解决方法并解决所有出现的问题(bug)。但在测试中,却无法通过运行简单的测试就能发现问题,并指导测试是合理的。相反,测试是使你对自己证实信服的手段,集中精神在某些可能出现问题的地方,并确定已经完全解决了这些问题。测试系统的真正问题在于:随着系统复杂性的增加,用来测试系统的测试用例也随着增加。所以不管调度和管理是多合理,都不可能运行无限的测试用例,那么就需要使用最有效的技术。

1、路径测试

路径测试就是使用最少的测试用例,测试系统中的每条路径,确保每条路径都正确执行。这就意味着每个条件、每个循环和代码中的每条路径都必须执行,保证它们的正确性。

2、事务处理测试

事务处理是一系列可能发生也不可能发生的事件。在事务处理中,重要的是事件什么时候失败,系统在事务处理之前是否能恢复到它的原来状态。

3、输入验证

如果程序没有接收任何的输入,那么系统中的bug不仅会大大减小,这些bug也会更容易被发现和解决。因为有很多不同的输入,也无法预料到用户的输入,输入是程序问题最主要来源。输入验证包含两个阶段。第一,正确的输入必须产生正确的输出、执行正确的过程,这是最简单的输入验证。第二,非法的输入必须不会引起问题。非法的输入应被即使发现并报告。

4、算法测试

不是测试真正意义上的算法,而是测试代码中算法的实现。

5、决策表

决策表是结果和决策的一个简单的交叉引用,在确定了条件的正确或错误之后,就可以知道某个动作是否会发生。使用决策表的好处是可以直接引出程序代码和测试条件。同样,因为测试条件是正确和错误的,那么就可以从简单的矩阵中生成完全的测试程序组。

6、状态机分析

状态机的工作原理是通过机器内部给定的条件,机器从一个特定的状态转移到另一个特定状态。在很多系统中,整个大的系统和单个的功能特征可以很完备地映射到状态机上。

状态机技术是理解程序如何从一个模式转变到另一个模式的有效技术,这一技术对不同特定状态的程序特别有效。如果系统有一系列的显示允许用户完成一个任务,那么这一系列的显示就是状态机。

7、综合测试

综合测试是对模块的接口进行的测试,也测试了因为不正确的通信或错误的测试引起的模块间的缝隙。

8、自上而下测试和自下而上测试

虽然不是真正的测试技术,自上而下测试和自下而上测试的问题经常在软件生命周期的开发、调试测试阶段出现。自下而上测试的思想是:从系统的底层模块开始测试,直到验证了每一层都能正确工作。另一方面,自上而下测试的思想是:从系统的顶层模块开始,通过适当的测试用例,一层层向底层模块进行测试工作。

9、配置调试

不同的配置是开发者和测试者遇到的共同障碍。

10、恢复崩溃和掉电测试

在计算机领域中,掉电是常见的问题。即使使用了不间断电源和存储器的后备电池,也将会看到计算机会经常出现意外掉电现象。如果系统没有进行这种测试,就可能会给用户带来严重的产品问题。

11、安全性测试

安全性测试意味了只有授权的人在需要的时候才能进入系统,而非授权的人因为不被允许就不能进入系统。安全测试有两种形式。第一,测试者通过挫败安全系统或通过使用一些自动工具随机生成密码来登录系统,以达到进入系统的目的。第二种方法是使用后门(backdoor)或程序错误进入系统。安全的缺点可以定义为成功地获得访问权或错误的消息包含了太多系统的信息。

12、第三方测试

有两个基本的第三方工具测试方法。第一个方法是为工具创建一个特定的测试程序,并在你要创建和测试的系统的上下文中之外测试它。在这种情况下,你验证了工具是否按照希望的方法工作,不会出现任何意外。第二个方法就是把这个工具作为系统完整的一小部分,然后编写测试用例已便在你的系统中最好地检查这个工具。

一般情况下,第三方工具出现的问题并不比自己编写的代码中的问题少。

13、多用户测试

考虑多用户测试时,必须运行几个重要的测试。第一,必须考虑在几乎相同的时间里用户做了几乎相同的事情;第二,必须考虑在几乎相同的时间里用户做了几乎相反的事情;第三,考虑在一段间隔里,两个用户做了相同的事。你能够而且应该在超过两个用户时,测试所有这些情况。

14、负载和性能测试

负载测试用来发现系统在完全崩溃前能处理多少的用户和过程。性能测试是测试系统性能随着时间和使用的变化情况。

当要对系统进行负载测试时,就要确定系统的瓶颈。例如,如果要对单用户排序系统的负载性能进行测试,那么系统的瓶颈可能是可使用的存储器容量、可使用的磁盘空间和总的参加排序的数据。另一方面,如果对一个web应用程序进行负载测试,系统的瓶颈是连接到站点的用户数。不知道系统性能的瓶颈,就无法进行正确的负载测试。

负载测试的基本方法是从一个合理的负载数字开始,然后加倍直到系统死机。接着,减小这个负载数目,直到系统重新开始工作。这个结果就是系统在这一性能上的最高负载。负载测试的结果数据应当包含以下一些信息:可以运行系统期望的用户数,影响系统性能的最小系统配置和最大系统配置。

性能测试和负载测试不同,在性能测试中,是从系统固定的配置开始的,然后通过测量来观察系统的性能的好坏。

性能和负载测试不会替代系统的测试和验证。性能测试为系统的修改作出计划并提高关键的部分,不是用来对系统进行任何功能测试。负载测试是用来发现系统功能的。虽然负载测试能发现系统关键性能需求的bug,但并不是真正意义上的测试。

15、测量和统计

度量标准是在测试软件时取得的数字。可以测量在软件中发现bug的数量、软件中模块的数目、甚至每个模块的代码行数。

以下列出的一些测量是在开发应用系统应当考虑的问题。

① bug——发现的时间和数据、bug发生的模块、发现bug的人、负责解决bug的人、解决时间和解决bug的人。

② 模块——复杂度、代码的行数、创建时间、创建者。

③ 屏幕——屏幕上发现bug的数目、屏蔽上的对象个数、支持这个屏幕的代码模块个数。

④ 数据库——行数、列数、每天增加的记录、错误发现数据的数目。

⑤ 性能——用户数、每个请求的平均处理时间、请求数目。

8 维护

维护是软件产品开发的主要工作。软件维护是在软件递交(给用户)后对软件产品的修改,以改正错误、修改性能或其他属性,或者使软件产品适应可修改的环境。

维护任务通常分为三类:

①纠正——解决bug,这就是纠正任务的名字意思。维护者的责任是确定bug的原因并解决它。

②适应——适应任务的目标就是为不同的系统替换系统的功能模块。维护者可能需要在不同的硬件平台、不同的操作系统或不同的编程语言上使用软件。

③完善——完善任务的目标就是要提高系统的性能。维护者的任务是增加一个新的特征、提高效率、改进功能、改进系统设计并实现其他和改进相关的任务。

一些维护任务的分类也包括预防维护。预防维护就是要预防软件系统中可预见的错误和问题。一般情况下,一个公司维护工作的典型工作是在维护阶段达到系统的完备性。在维护的早期阶段,完成纠正维护工作,这是因为软件还没有被完全地使用。到软件的成熟阶段,开始完善维护任务。

典型的,一个完整的维护工作包含了四个阶段:需求、理解、修改和验证。

①需求——维护任务通常是从修改的需求开始的。这个需求可以来自任何地方。

②理解——一般情况下,程序员大约花费47%到62%的时间在理解文档和程序的逻辑结构上。

③修改——如果我们知道怎样进行修改,我们就可以进行软件的实际修改。这是最容易的步骤,因为在这一点上,维护者已经清楚他们要做什么,所做的工作是将要做什么修改并确定这些修改对软件的影响。若不能确信修改的作用,就要重复步骤2。

④验证

验证就是确认我们所做的修改是否达到预期的功能,同时需要确认不会影响修改之外的其他部分。这是维护任务的最重要部分。

引起维护工作的困难主要有如下原因:1软件工业中,人员的流动率很高,这就意味着经常有新的开发人员参加他们从不熟悉的软件开发。2因为软件理解的困难,维护者确定所有需要修改的地方和这些修改的作用通常是比较困难的。显然,不好的软件设计也会使软件的维护变得困难。

维护的困难意味着维护者在进行软件修改时会引起错误,这些错误可分为以下三类:

①明显错误——这些错误在单元测试和回归测试就会显示出来。

②潜在错误——错误不明显。

③概念错误——概念错误不会终止程序,它们也不是错误。但是它们会引起软件的设计的退化。这些类型层次是轻微的修改,并且这些类型不再是可重用或可扩展的。随着软件变得越来越严格和脆弱,随着时间的增长,系统的初始设计出现退化,并且出现漏洞,这样也就无法进行系统维护。

 

 

 

 

 

 

 


原创粉丝点击