wenmo8 发布的文章

弗雷德里克·布鲁克斯( F r e d e r i c k P·B r o o ks ) 博士在他那篇著名的《没 有 银弹— 软件工程 中的根本和次要问 题》 一文 中 , 将软件项目比作可怕的人狼 (w e r e w o l v e s ) , 并大胆地预言十年内不会 找到特别有效 的银 弹 。 该论文发表的时 间是 19 8 6 年 , 如今整整 2 0 年过去了 , 尽 管不时有人惊呼找到 了神奇的银弹 , 但是 冷静的人们很快发现那只是美好的愿望 。
如果说 软件工业 中与人狼 的战 斗还在持续 , 那么在这些战役 中一 定会有程 序 员的身影 , 笔者 也是 其中的一个。 我的 编程生涯是从使用汇编语言编写 D O S 下 的 T SR 程序开始的。 今天 D O S 操作 系统 已经成为历史 , 在那个年代最值得炫 耀 的 T S R 技术也早 已经过时了 。 十几年 中 ,O W L、V F W、V D X、ISA P I、A e ti v e M o v i e 等技术也被 时间淘汰… …然而 , 在这漫 长的时间当中 , 我最看重的是软件调试技术。它是十几年中我学到的最有用、一直受用、而且 日久弥新的一项技术。
从软件 工程 的角度来讲 , 软件调试 是软件 工程 的一个重要部分 , 软件调试 过程 出现在软 件工程的各个阶段 。 从最初的可行性分析、原型验证、到开发和测试阶段、再到发布后的维护与支持,都有调试过程的参与。通常认为,一个完整的软件调试过程由以下几个步骤组成:

  • 重现故障
    通常是用于调试的系统上重复导致故障的步骤,使要解决的问题出现在被调试的系统中。
  • 定位根源
    及综合利用各种调试工具,使用各种调试手段寻找导致软件故障的根源 (o r o t c a u s e )。通常测试人员 报告和描述的是软件界面或工作 行为中所表现出的异常 , 或者是 与软件需求 和 功能 规约不 符 的地方 , 泛 指软 件 缺 欠(d e f e c t ) 或者故障 (f a i l u e r )。 而这些表面 的缺欠总是 由于一或 多个内在 因素所 导 致的 。 这些内因要 么是代码的行为错误 , 要么是不行为错误 (该作而未作 )。
  •  探索和 实现解决方案
    即根据寻 找到的故障根源 、 和资源情况 、 紧迫程度 等要求设计和实现解决方案。
  • 验证方案 ,
    在 目标环境中测试方 案的有效性 , 又称 为回 归 (e r g e r s s ) 测试。 如果 问题 已 经解决 , 那么 就可以关闭问题。 如果没有解决则回到第 3 步调 整和修 改解决方案 。

这些步骤 中 , 定位根 源常常是最困 难也是 最关键的步骤 , 它是软件调试过 程 的核心 和灵魂 。 如果没有找到故障根源 , 那 么解决方案便很是隔靴搔痒 , 或者 头痛医脚 , 白白浪费了时 间。 对软件调试的另一种更通俗的解释是指使 用调试工具求解各种软件 问题的 过程, 例如跟踪软件的执行过程, 探索软件本身或者与其配套 的其它软件或者硬件 系统的工作原理等 , 这些过程 的 目的有可 能是为了去除软 件缺欠, 也 可能不是 。

在了解了软件调试技术的基本概念 以后 , 下面我们来看一 下支撑软件调试 技术的几种基本机制 。

  • 断点 : 即当被调试程序执行到某一 空 间或时间点时将其中断 到调试器 中。 根据中断条件分为如下几种 :
    a、代码断点: 当程序执行到指定 内存 地址的代码时中断到调试器 。
    b、数据断点 : 当程序访问指定内存地 址的数据时中断到调试器 。
    c、I/O 断点: 当程序访 问指定 1 / 0 地址 的端口 时中断到调试器 。
    根据断点的设置方法 , 断点又分为软 件断 点和硬件断点。 软 件断点通常是通过 向指定的代码位置插入专用的断点指令来实现的 . 比如 IA 32 C p U 的 IN 丁 3指令 (机器码 为o x C C ) 就是断点指令。 硬件断 点通常是通过 设置 C PU 的调试寄存器来 设置的。 IA 3 2 C PU 定义了 8 个调试寄存 器 , D R O一 D R 7 , 可以最多同时设置 4 个硬 件断点〔对于一 个调试会话)。 通过调试寄 存器可以设置以上三种断点中的任一种, 但是通过断点指令只可以设置代码断点。
  • 单步跟踪 : 即让应用程序按照某单 位一步步执行。 根据单位 , 又 分几种:
    a、每次执行一 条汇编指令 , 称为汇 编语言一级的单步跟踪。 设置 IA 32 C Pu 标志寄存器的 T F (rT 叩 lF a g , 即陷阱标 志位 ) 位 , 便可以让 C P U 每执行完一条 指令便产生一 个调试异常 (N I T I ) , 中断 到调试器 。
    b、每次执行源代码 (比汇编语 言更 高级的程序语言, 如 C / C料 ) 的一条语句 , 又称为源代码级的单步跟踪 。 通常高级 语言的单步跟踪是通过反复设置 C P U 的 陷阱标志位来实现的, 如果 当前源代码行还没有执行完 , 那么调试器重新设置 陷阱 标志并让程序继续执行, 直到该语句结 束(E I P 指向另一语句 ) 才中断给用 户 。
    c、每次执行一个程序分支 , 又称 为 分支到分支单步跟踪。 设置 IA 32 c Pu 的o bg e t lM s R寄存器的 B T F (B ra n e h T ra pF l g a )标志后 , 便可以启用分支到分支单 步跟踪 。
    d、每次执行一 个任务 (线程) , 即当 一个任务 (线程 ) 被调度执行时中断到调 试器 。 IA 3 2 架构所 定义 的任 务状态段( T Ss ) 中的 T标志为实现这一功能提供 了 硬件一级的 支持 , 但 是很多调试器还 有 提供这项功 能。
  • 栈 回溯 (s ta e k ba e k ta r e e ) : 即通过记 录在栈中的 函数返 回地址显示 (追溯 ) 函 数调用过程 。 在将返 回 地址翻译 成函数 名时需要有调试符号 ( de b u g sy n t b o l ) 的 支持 。 大 多数编译 器都支持 在编译时生 成调试符号 。 微软的调 试符号服 务 器
    ( h t t P: // m s d lm ie r o s o f te o m /d o w n lo a d /sy n t b o ls )提供 了大 多数Wi n d o w s 系统文件 的调试符号 , 是调试和学习w id n o w s操作 系统的宝贵资源。
  • 调试信息输出(de b铭 o u tP u印i r n t ) : 即将程序运行 的位置 、 变量状态等信 息 输出到调试器 、 窗 口 、 文件或者其它可以 观察到的地方 。 这种方法的优点是 简单方便、不依赖于调试器 , 但也有明显的缺点 , 如效率低 , 安全性差 , 通常不可以动 态开启 , 且难以管理等 。 在W i n d o w s 操作 系统中 , 驱动程序可以 使用 D b g r P in t /D b g P i r in E x 来输出调试信息 , 应用程序可 以调用o u tP u tD e b u g s i t r n g API
  •  日志 ( 1 0 9 ) : 将程序运 行的状态信 息写人到特定的文件或者数据库中。 W in d o w s 操作 系统提供 了记录、观察和管理 (删除和备份 ) 日志的功能 。 Win do w s v is a t 新引入 T 名 为 C o m m o n L o g F i le s y s t e m (c L s F . S Y s ) 的内核模块 , 用于进一步加 强 日志功能 。 .
  • 事件迫踪 (e v e n t t r a e e ) : 通 常用来 监视频 繁的复杂的软件过程 , 满足普通 日志机制难以胜任 的需求 。 比如监视大 信息t 的文件操作、 网络通信等。 E T W ( E v e n t T r a c e f o r Wi n d o w s ) 是Wid n o w s 操 作系统内建的事件追踪机制 , Wi n d o w s 内 核本身和很多W in do w s下的软件工具(如 B o v o t i s , T C即 P V ie w )都使用了该机制 。

在以 上机制中 , 断 点和单步跟踪通常必须在有调试 器参与的情况下才能使 用 。 调试器 ( s o f t w a e r de b u g g e r ) 是综合提 供 各种调试功能的软 件工具 。 除了处理断点、单步跟踪、模块映射等调试事件外 , 调试器通常还提供如下功能 :

  • 观察和编辑被调试程 序的内存和数据 , 如全局变量、局部变量、以及程序的栈和堆等重要数据结构 。
  • 观察和 反 汇编被调试程序的代码。 
  • 显示线程栈 中的函数调 用信息 。
  • 管理调试符号 。
  • 控制进程和线 程 , 例如将 被调试 程序中断到调试器中 , 和恢 复其执行 等。

根据调试器 所调 试目标程序的工作模式 , 可 以把调试器分 为用户态调试器 和内核态调试器 , 前者用于调试用户态下的各种程序 (应 用程序、系统服务 、 或者用户态的 D L L 模块 ) , 后者用于调试工 作在内核模式 的程序 , 如驱动程 序和操 作系统的内核 部分 。 Wi n D b g 是微软开 发 的一个免 费调试器 , 它既可以用 作用户 态调试器 , 也可以用作 内核态调试器 , 是调试 Win d o w s 操作系统下的各种软件的 一 个强 有力工 具 。 我 几乎 每天都使 用 w in D bg , 它是我 的计算机 中使用频率最 高的软件之一。

最后 , 简要地描述一下软件调试技 术的几个特征。

系统性— 很多看似简单的调试机制 都是依靠系统 内的 多个部件协 同工作 而完 成的 。 以软件断点为例 , C P U 提供了 指令支持和硬 件级的异常机制 , 操 作系统将异常以调试事件的形式分发 给调试 器 , 调试器响应调试事件并与用户交互。 如果在做源 代码级的调试 , 那 么调试 器 又需要编译器所产生的调试符号来帮忙 。

全局性— 对于一 个软件项 目 , 应该 在项 目的设计 和架构阶段就制 定出全 局的调试支持机制 , 并贯彻实施。 比如 , 所有模块都应 该使用统一的 方法 来输出调试信息、记录 日志 、 报告错误 , 并公开 统一的 接 口 用做单元测 试和 故障诊 断 。 这样不仅可以避免重复工作 , 而且增加T 软件的 可调适性 (d e bu g g a bi一 i ty ) , 有利 于保证产品的质量和进度。

困难性— 《C 语言编程》 一书的作者 B r ia n K e r n ig ha n 曾经说过 , “ 调试天生就 比编写代码难上一倍 , 如果你 写出 了最聪明的代码 , 那 么你的智商就不足以 调试这个代码。 ” 因为 , 要调试一个程序 ,就必 须深刻理解它的 工 作原理 , 不仅要知道 h o w 和表 层的东西 , 还要知道w h y 和 深层次的 内幕。 另外 , 调试需要锲而不 舍的探索精神和坚韧的耐 力 , 这 也让很 多 人望而却步 。


综上所述 , 软件调试技术是与软 件 开发 密不可分的一门技 术 , 其初衷是 为了定位和 去除软件故障 , 但 因 为调试技术所具有 的对软件的 强大控制 力和观察 力 , 其应 用早已 延伸到 了很 多其它领域 , 比如逆向 工程 、 计算机安 全等等。 学习和 灵活运用软件 调试技术 , 不仅可以提 高程序 员的 工作效率 , 而且有 利于提升 对代码的感 知力和控制 力 , 加 深对软件和系统的理解。 此 外 , 调试技术 是解决各 种软件 难题 的一 种有效 武器 。 它直击要害 、 锐不可档 , 相对其 它间接方 法具有明显的优势 。 软件有大美 , 调试 见真功。 在寻找银 弹的 努力还 在继续的时候 , 衷心地 希望所有程序员朋友都学 会使 用调试这把利 剑吧 , 使用它为你披荆斩棘 , 帮你探索前进。 只要你的这把剑依然 锋利 , 那你的软 件青春就永远不老 。

随着计算机技术的飞速 发展和普及 , 越来越多的人直接与计算机打交道 , 因而产生 了研 究计算机操作者心理状态 的一 门综合性很强的边缘交叉科学 - 一软件心 理学。 软件心 理学 把实验心 理学 的技术和 认知心理学的概念应用于 计算机和信息科学 , 它 主要研究了计算机 科学 中人机系统和软件设计中人类心 理和 行为活 动的规律。 对软件心理 学的研究 , 在我 国还 是刚刚起步 , 本文对这一领域进 行了探讨和尝试 , 探讨 程序调试过程中人机对话对人的心 理 的影响 , 探讨了人同机器会话的心理 感受 , 探讨程序与 机器速度 的关系 , U N IX 分时系统终端数与模块大小的关系。 进而探讨如 何确定合适的程序模块, 合理的选择机型 , 使程序员不致因为环 境因素造成软件调试过程中的心理 压力 。

心理承受能力实验

首先把用户 (程序员 ) 对机器响应的承 受能力分成七个等级 : 极快 (1 ), 快 (2 ), 较快 (3) ,中等 (4 ) , 稍慢 (5 ), 慢 (6 ), 无 法忍受 (7) 。 本文 主要讨论如下几个问 题:

  • PC 机及兼容机 上编译 C 语言程序时的心理 承受能力分析;
  • U N IX 分时操作系 统环 境下 , 编译 O 程序的 过程 中如何影响程序员的心理 状态 ;
  • 人机会话期间 , 中间信息的输入/ 输 出对程序员的影响。
  • P C 机编译 C 程序的实验 选择 P C /X T 和 C 8 6 2 . 0 编译器 , 把给定的 6 0 个大小不 等的程序共

P C 机编译 C 程序的实验

选择 P C /X T 和 C 8 6 2 . 0 编译器 , 把给定的 6 0 个大小不 等的程序共分成 6 组 , 侮组 」 0 个程序 , 分组 原则是第一组 语 句 2 1 ~ 3 0 行 , 第二 组 3 1 ~ 4 0 , 第 三组 4 J ~ o 6 , 第四组 6 1 ~ 阳 , 第五组 8 1” 1 0 0 , 第六组 10 0 ~ 20 0 。 对每组程序进行编译测试 , 得出的时间求平均位 , 对该 组程序的承受级别求平均值。 以第四组 程序为 例说明测试 的过程和测试结果 : 参加测试的四名程序员是计算机软件专业 本科 四年级的学生。 A , B 男同学 , O , D 女 同学。 测试 过程如下: 先 给 10 个程序编号 P 丸 P Z , … , P I O。 A , O 一 组 , B , D 为 另一 组。 以A、C 组 为例 。 C 开始编译 一个程序 iP , 记录 下开 始 时间 T l 和结束时间 T Z , 当程 序 曰 编 译 结 束时 , A 说出感 受级 别, 记录下 编译时间 口、2一甲1 、 , 0 J 个 程 序都测试一 遍 。 人 . e 角 色 互换再 测试 一 遍 , 得出 O 的承受级别 , 同样测 出 B , D 的 承 受级 别

平均编译时间 2 6.89 秒 , 平均承受级别3 . 8 ( 中等)。 其中二名男同学的平均承 受 级 别4.0 , 二名女同学 3 . 6 。 同样的方法 得 出 6 组程序平均编译时间 、 平 均承受级别如 表1.

 

 顺便说一下 , 每组程序测试的结果都表明, 女同学的承受级别普遍小于男同学10% 左右 , 这 点说明了在调 试过程中, 女同学更有忍耐性 , 更适合做调试工作 (而不是设计)。

 

不 同机型编译 C 程序

选择第六组程序进行实验 (实验过程类似上面实验 1 )。

 

有屏幕输出信 息的 心理测试实验

对 于小程序 , 看屏幕输出与不看屏幕输出对程序员心理 无 明显影响 , 对于 第六 组 稍大一 点的程序 , 看屏幕输出时, 在 S U P E R P O/ X T 机上, 平均编译时间仍为 38 . 2 9 秒 , 但用户平 均感受级 别 4 , 在 P O /X T 2 8 6上 , 平均感受级别仍是 3 。 这一结果指 出了 中间信息的输出 有 时对改善程序员心理有较大作用 , 有时则不 明显。
UN I X 分 时系统编译 实验 ( 680 00 机 )

进行 680 0 0 机编译实验时 , 增补一 些较短的程序 , 被测程序语句行 1 0 ~ 2 0 0, 实验结果如下 :

4 台终端同时工作 , 每个程序编译时间4 0 ~ 62 秒 ;

6 台终端同时工作 , 每个程序编译 时间7 0 ~ 11 0 秒;

9 台终端同时工作 , 最短程序编译时间接近 3 分钟。

从中看 出, 分时系统进行编译或运行 , 对用户心理 压力最大的因素来 自同时工 作终端数 ( C P U 速度) , 其次才是程序规模。 因此 , 安排 学生 在分时系统上 机时 , 应考虑这点。

程序调 试中个体差 异 的实验

本实验是让二名受试者背对机器 , 编译开 始时 , 二名受试者被告之 开始 (事实上 , 根本就 没有编译程序 , 只是发出指令信号 ) , 被测者根据自己感觉的时间 , 说出心理 感受级别, 下 面 是记了结果

 

实验分析与结论

这 里讨论的一 些结论全 部基 于心理承 受级别不能超过 4 ( 中等) , 承受级别 5 ~ 7 时 , 对 程序员的程序调试有很大的心理 和身 体影响。 长期的心理 压力也影响程序调 试 的 准 确 性 , 增加出错率 , 导致程序调 试的逆反心 理。
1、机器速度对程 序员心理 的影响 表 1 中数据表明大于 8 1 行语句的程序在 S U P E R P O / X T 上编译时, 承受级 别 5 , 而在 邹 6 _ ! 几编 译至少 级别是 3 。 这说 明了大于 8 1 一 行的程序模块至少 要在 2 86 上调 试 。 P C 机 及 卫C / x T 的低档机只 可 做简易 的实习用。
2、屏幕输出对程 序 员心理 的影响 实验 3 的结论指 示 了增加屏幕输出有助于 改善调试 程序的心 理压 力 , 但 太 多 的 增 加 I / O 信息又 延长编译时间 , 是否 有数据可 参考呢 ? 实验 3 数据表明 3 8~ 4 0 秒的 界 限是心理 危机的开始, 因而编译程序应在运行 3 8 秒左右时提供一 些 信息。 如 p O T O O I J 一工具进行磁 盘O O p Y 时 , 有磁道的一些信息输 出。 用户 自身的程序运行时, 3 8 秒同样是个界值 , 用户 应 在相应的程序处加上 1 / 0 信息 , 由于机器主频不一致 , 用户 自己根据情 况处理 。 爪 l ) J E R 卫o / x T 一般 10 0 ~ 20 0 语句 , 2 8 6 一般 ` 1 0 0~ 加 0 等 , 这只是笔者之 建议 , 供参考 。

3、速度祖的机器 运行大程 序心 理调整 由于 P O 及 兼容机 (1 . OX ) 速度较慢 , 运行大一点程序 (1 0 0 行 ) , 用户的心理压 力就很 大 , 一般应做心 理 调整或更换编译器。 表 2 中的数据表明更换编译器 可使 承 受 级 别 提 高1 、 2 级。 另 外一点是在上机前就应“ 明确” , 程序很大时 , 编译时间长 。

4、UN I X 分 时 系统影响 用户 心理 的讨 论 前面 已讨论过 , 分时系统对用户的心 理影响主要是机器 速度 , 其次是编译软件。 因而 除 更换 编译软件和心 理 自身调 节外 , 就必 须对机器更新换代了。 另外 , 笔者在 A ST 2 3 6 上调 试 1 0 0 0 ~ 2 0 0 0 行的模块时 , 发现 仍有较大的心理 压力 , 承 受级别 4 ~ 5 , 这也说 明心理 压力 的调节主要靠硬件更新 。
5、音响、 图像对心理 因素的影响 笔者对 4 名受测者测试 , 给受试者 M S一 O 和 T U R B O 一 O 二 种编译器 (受 试者 以前从未 使用过 C ) , 受试者都愿使用 T U R B O O , 理 由是 画面直观 , 操作方便。 但当笔者介绍 了 M S哪 . 0 的 O O D E V I E W , M A K E 和 Q i l t c k一 O 等后 , 受试者表示也愿意选择 M S 一 O 。

心理调节能力与衡量标准

表 1 中数据和实验 5 中相比较 , 可 以看出表 1 中承受级别的时间明显 高于实验 5 中数 据。 图 4 是 图 1 , 图 2 和图 3 的合成图。

 

 

在看代码之前我们先连简单的看下try的处理流程吧

  • 函数入口设置回调函数
  • 函数的异常抛出使用了__CxxThrowException函数,此函数包含了两个参数,分别是抛出一场关键字的throw的参数的指针,另一个抛出信息类型的指针(ThrowInfo *)。
  • 在异常回调函数中,可以得到异常对象的地址和对应ThrowInfo数据的地址以及FunInfo表结构的地址。根据记录的异常类型,进行try块的匹配工作
  • 没找到try块怎么办?先调用异常对象的析构函数,然后反汇ExcetionContinueSearch,继续反回到SEH继续执行。
  • 找到了try块?通过TryBlockMapEntry结构中的pCatch指向catch信息,用ThrowInfo结构中的异常类型遍历查找相匹配的catch块,比较关键字名称,找到有效的catch块。
  • 然后进行栈展开。
  • 析构try块中的对象
  • 跳转到catch块中执行
  • 调用_JumpToContinuation函数,返回catch语句块的结束地址。

上面的步骤,就是典型的异常处理的顺序。

光看文字多无趣,上代码 - 实例分析,我们来跑一遍:






1

1 引 言

软件规模 日趋庞大 , 软件调试 中 , 发现 、 定位、分析错误的工作量也相应增长 。 因此 , 人们 开发 了 C o d e iV e w 等调试工具以 深 入错 误发 生时的程序执行环 境 , 使效率大增 。 不过对于 下 面的循环结构 ( 以 C 语言为例 )仍有为难之处 :

 

 

假定该循环共执行 1 0 0 0 0 遍 , 第 3 0 0 0 次执行死 机了 , 那么用调试工具判定死机发生在该循环 中很 容易 , 但再进一步分析则不可能 。 因为 , 若 设 断点 于 循 环 内 , 则 每 次循 环 皆被 中断 , 至 3 0 0 0 次运行才能发现错误所在 , 以 后欲 分析错 误也是每 3 0 0 0 次运行方可进入 出错环境一次 , 这绝对无法 容忍。

2 调试方法

在这里笔者介绍一种在调试软件过程中改 进了的方法 , 可解决这个问题 。 对于上面的实例 只需增加几条语句即可 , 如下所示 。 其 中 fp 为 文件 指针 , n u m b e r 是 初 值 为零 的整 型 变 量 ,
d o o n h t in g 为一空 函数 , 它 们都为调试而 设立 。 具体的调试方法是将不设断点的程序先执行一 次 , 然后 阅读 r e c o r d . da t 找 出错 误发 生 时的n u m be r 值 , 再设 br e a k p o in t 为该 n u m be r 值 , 置断点于 d o n o t h i n g o 这 一行上 , 即可使 程序 非常方便地运行到 出错处停住 。

 

这里文件先 用 “ w ” 方 式打开 , 就 自然清 除 了上次执行形成的 r e c o r d . da t 。 在循环中用“ a " 方式将每次循 环 中的 n u m h a r 值等关键参数逐 次记入文件尾 部 。 切记一定要在循环内打开文 件 , 写 入信息 , 再关 闭文件 , 这可保证切实形 成 文件 ; 否则 (在进入循环 前打开 , 结束循环后关 闭) , 一旦循环 内出现死机等严重间题 , 文件就 不能形成 。 对于复杂的循环 , 记录于文件 中的信 息应包含一些除 n u m b e r 外 的其 他重 要参 数 , 既 利于发现错误 ( 参数异常就是出错 , 不必非死 机等重大问题才知出错了 ) , 又有利于了解循环 执行过程 而分析错误 , 因此 , 这些参数选择的好 坏 直接 影响调试效率。 在这个例子 中设 do_mai n _ w o r k 为循环 中的实质所在 , 又 很复 杂 , 其余仅是简 . 单工作 , 则应记录它的参数 ( 假设参 数 1 为整型 , 参数 2 为双精度型 ) 。

n u m b e r 系一附加 变量 , 如循环 中有一 不 断 增大或不断减小的变量可用 , 则也可用该变 量代替 n u m b e r 的作用 。 不过本例适用于 任何 循环 , 则是标准的方法 。 另外 i f 语句中的相等 关系也可用适当的不等关 系取代 , 如本例中用 不小于关 系 , 则 n u m b e r 不小于 b r e a k p o i n t 后 的每一 次循环 中断点有效 。 b r e a k p i o n t 最好不 要用一常量 ( 以免常常修改 ) , 而采用一变量 , 它 可在进入循环 前读入或 由命令行参数传入 , 如 此则程序无须改 动而 可停在循环的任意次数 上 。 b r e a kp o in t 类型 自然与 n u m b e r 或其他替 代者相同 。

3 结 语

这种方法有利于 发现错误 , 以 后 利用调试 工具又极易进入 出错时的环境 , 而且为调试而增加的程 序是固定不 变的 , 故大大提高了效率 。 不过除了死机 、 除零等中断程序运行的错 误一 定发生在 r e c o r d . d a t 的最后 一行记录写入 后外 , 其余错误往往 比较含蓄而要查找一番 , 如 关 键参 数出错 , 则可能需要认真 阅读 r e c o r .d
da t , 对于一些不影响关键参数的小错则可能需 要另 想办法 。 另外发现的可能是表面错误 , 如果 死机 由前面 某次循 环中的错误 埋下祸 根 , 则需 先 由 死 机 处 仔 细 分 析 , 发 现 疑 点 , 再 重 设
b r e a k p o f n t 去分析疑点 , 深挖根源 . 所 以使用该 法虽减小了工作量 , 但软件调试仍是一项艰 巨 的任务 .

软件 的调试 也称纠错或排错 , 它是孤立并纠正错误的一种技巧性过程 。 软件错误的外部表现形式与内部 原 因之间往往没有 明显 的联系 , 所 出现的差错并非直接就能找 出原 因 。 因此 , 调试 既要对 错误 的性 质及 程序 本身进 行系统的研 究 , 在某种程 度上也要靠直觉与经验 。 到 目前为止 , 调试还 没有 一套经 得起检验 的完 整而 系统 的理论方法 , 排错时所采用的方法和时 间都不能事先确定 . 这样 , 通常认为调试是 困难的 , 是软件开发过 程 中最为艰 巨 的一种脑力劳动 。 本文拟就 调试的方法 、 技术与技 巧进行探讨 .

调试的步骤

诊断错误

或是 系统报 错 , 或是输 出结果与设 想的不 同 , 或是 陷入死循环等 , 都认为程序存在错误 .

确定错误的源发点

发现错 误的地方不一定是错误的源发点 , 应寻找所有与错误有关 的地方 , 从而确定错误的源发点 。 例如程序 :
1 0 F O R I= 1 T O 1 0
20 R E A D A ( I )
30 N E X T I
4 0 D A T A 15 , 1 6 , 2 5 , 27 , 2 8
R U N
O U T O F D A T A 1 N 2 0
错误发生于 2 0 行 , 但与第 4 0 行有关。

改正错误

确定错误及 位置后 , 针对错误 的具 体类 型进行改正 。 在纠错过程 的两方面即确定错误及位置和改 正错误 中 , 第一方面 的工作大 约相当于整个工作的 9 5 % , 为 排 错的关键 。 故本文重点探讨错误的诊断方法与技巧 。

诊断 错误的实验方法

静态调试

静态调试指对程序进行人工书面检查。 静态调试时要仔细阅读程序及其文档 , 经过结构分析 、 功能分析 、 逻辑 分析 、 接 口 分析 、 语 法分析以 及逐行检查 , 以 便找出并改正错误 。 通常 有下 面两种方法 。

  • 检查语法错误
    产生语 法错误 的原因 主要有两 个 , 一个是 键入错误 , 此错误如 同写文章 时的“ 笔误” ; 二是 由于对语法规则 不熟悉 , 如书后的错误 信息 、 各种限制 、 全局变量与局 部变量 、 先左后右 的原则等 , 这些 虽不是 系统的规定 , 但也是语法的一部分 , 应作为专项予 以检 查。
  • 跟踪程序流程
    此时的跟踪程序 流程 , 即将 自己 当做计算机 , 给 定一 组输 入数据后 , 顺序执行每 条语句 , 考察所得结果 . 寻 找错误 . 此方法需 花一定时间 , 但这是 最基本的方法 , 用 其它方法难 以查 出问题时 . 可以试 用此法 。 顺便说一句 , 学习编 程技术的主要途径是读 别人的 程序 , 对 较难懂的地方 , 也只 有跟踪程序才能读懂 , 也 就是常说的阅读能力提高 的途径 。 对程序 的流程图也可采取此方法检查 。 一般提倡应尽可能将 各种错误 消灭在静 态调试 阶段 。

动态调试

动态调试 , 是指实际 上机运行程序进行调试 . 经过静态调试 后 , 仍 留在程序中的错误便都十分隐蔽。 为找 到这些错误 , 首先需捕 获一些与错误有关的线索 . 即进行错误侦察 , 此时需充分利 用计算机系统提供的调试手段。

  • 试通
    源程序上机 运行 , 语 言系统及操 作系统会在程序有故 障时给出信息 , 这些 信息反映 了如下几种故障情况 :
    ①没 有通过 编译 对解释型的高级语言来说 , 如 B A S I C 语言 , 程 序出现语法错误 , 系统便使程序在出错点 中断 , 并指 出错误 的类型 和 位置。 对 编译 型的高能 语言 , 如 P A S C A L 语言程序 , 编 译系统把程序检查一遍后 , 对语法错误会打 印 出一系列的出错 信息 , 根据这些语法出错信息号 , 可在“ 用 户手册 ” 中查 出原因 。
    ②没有通过连 接编辑 连接编 辑阶段的 错误有 : 公 共数据块长度不一致 , 系统 自动按最长 处理 , 但给 出警 告 ; 某个模块名找不到 所需要 的模块 , 如 数据说 明遗漏 , 连接数组元素引用 当函数引用 , 库函数引用 不符合规格说 明; 内存容量 不够 而需要分节等 。 这些 错误 可参照 “ 用 户手 册 ” 予以改正 。
    ③程序的运行过 程因故障而停止 程序因故障而 停止运 行 , 在多数情 况下会给出出错信 息 , 这类信息在“ 用户手册 ” 中都有解释 。
    ④程序只 输出部 分结果 对这 部分结果进行分析 , 可大致 了解程序被执行的逻辑 , 或程序在什么地方被中断 。
    ⑤程序执行 了很 长时间没有结果 这种情况可能 由三个原因造成 : 一是程序本来执行 时间就 很长 ; 二是程序 内有死循环 ; 三是程序运行时使 硬 件系统“ 死锁” 。
  • 调试工具
    错误的位置可以 通 过在程序 中插入调试 语句 , 也可 以使用机 器提供的调试工 具在程序 中的某一点将有 关数据单元的内容或程序 的执 行路径输 出。 不 同的操 作系统或编译程序提供 不同的调 试工 具 。 调试软件一般 有两 种 , 一种是 交互式调 试程序 , 它 使得 程序员和 执行 中的用 户软件 在联 机方式下相 互作用 , 提 供了中断程 序 、 在程序中设置断点 、 显示并改变符 号项 中的变量 、 逐语句的执行程序等特性 。 如 B I M 公司为 P L l / 的 C C ; D E C 公司为 CO B O L 配的 C ID 等 。 另一种是 程序 语言所提 供 的调 试特性 对语言 的 扩充 。 如 P L l / 提 供了 c H E c K 语句 , F O R T R A N 提供 了作为注释或在编译时 作为正 式语 句解释 的特性等 。 此外 , 为了调 试程序 , 常 常使用操 作系统提供的某些实用软件 , 例如文件或 内存 的转储 , 两个文件的比 较程序 等 , 或是利用测试得到 的 信息 。 然而 , 最有效 的调 试工具 似乎是写程序时写到程序中的调试语 句 , 这 样 , 出错区域可 由程序员定位。 调 试语句是一些不影响程序的功能 , 仅 给调试人员提供如下 信息的语 句:
    ✦活动路径
    ✦统计活动次数
    ✦其它有关信息
    常用的调试 语句有 以下几种 :
    ①利用系统 提供的调试命令和语句 如在 A P P L E S O F T 中以下命令与语句常 用 , S T O P 语 句使程序暂 停 , 设置断点 ; C O N T 语句使程序从断 点继续执行下去 ; T R A C E 逐个行跟踪 , 即逐 次显示计算机执行的语句行号 , 给 定一 组调试 数据后可以检查程 序是否按预想的路径执行及执行的结果是否 正确 ; N O T R A C E 命令取消逐行跟 踪。 在 F O X B A s E 中 , 程序执 行到 S U S P E N D 时能把正在执行的程序挂起 , R E S U M E 能使被 挂起的程序 从断点处继 续恢复运行 ; S E T E C H O O F F / O N , 默认值 为 O F F , 若设置为 O N , 则将 每一条执行过的命令在屏幕上显示 , 由此可确切地掌握当 前程 序运行 的进程 , 帮助 查 出产 生 间题的 位置 , S E T S T E P O F F /O N , 默认为 O F F , 当为 O N 时 , 程序会以单 步形式进 行等。
    ②设 置状 态变 量 例 如 , 在 每个 模块中设置一个状态变 量 , 程序进入该 模块时 , 便给该 状态变量一 个特殊值 , 根 据各状态变 量 的值 , 可以判定程序活动的大致路径 。
    ③设置计数器 在每个模块或基本 结构中 , 设置 一个计数器 , 程序 每进入该结构一次 , 便计数一 次。 这样 , 不仅 可以判断 出程序活动的路径 , 而且 当程序中有死循环时 , 用此方法便能很快确定 .
    ④插入打印语句
    打印语句是最 常用的一种调试语句。 它用起来非常 敏捷 , 能产生许多 有用的信息 . 特 别适用于人机对话 或 调试过程 。 关键是断点的位置和打印哪些变量 的值 。 下面介绍打印语句的几种用法 。
    A.回声打印 ( E C H O P R IN T IN G ) “ 回声 ” 打印的特点是“ 读 了就写” 。 它把打印语句放在紧靠读语句之 (或输入语句 )之后 , 或模块入 口 处 , 及 调 用语句之前后 . 可以帮助调试人员检查数据有没有被 正确地翰入或接 口 处信息传递是否正确 。
    B. 追踪打印
    追踪打印是为提供程序执行的路径信息而设置的打印语 句。 这些打印语句通常设置在下述 位置 : . 模块首部或尾部 . 调用语句前后 . 循环结构 内的第一个语句或最后一个语句 . 紧靠循环结构后面第一个语 句 . 分支点之前 . 分支中的第一个语句
    C.抽点打印
    抽点打印就是选择一些可疑点设置打印语句 , 以便打印有关变量的值 。
    D.成组打印子程序
    即集中一组打 印语句写成一个 专用子程序 , 凡是需 要了解情况处就可调用此子程序。 例 : 考 虑到 层、 块结构 的需要 , 可在一层中编写一个打印子程 序。
    8 9 9 9 R E M C 层成组打 印子程序
    90 0 0 P R I N T “ C $ = ” ; C $ ; “ C C $ = ” ; C C $
    9 0 1 0 P R IN T “ C = ” ; C ; “ C C 一 ” ; C C ; “ C l = ” ; C l ; “ C Z = " ; C Z
    9 0 20 R E T U R N
    可在若干地方调用 此子程序 :
    31 4 5 P R IN T “ 检索部分打印” : G O S U B 90 0 0
    3 5 6 5 P R IN T “ 分类部分打印” : G O S U B 9 0 0 0
    36 7 5 P R I N T “ 求和部分打印” : G O S U B 9 0 0 0
    此方法很有用 , 能动态地 了解程序运行情况 。

预埋技术

预埋技术是在程序 中加入“ 潜伏” 的调试语 句。 前面介绍的打印语句和成组打 印子程序 , 在程序完 成后要将 其删去 . 而预埋技术将调试 语句永久地编入程序 , 其是否起作用 由逻辑软 件开头控制 。
例如:

10 IN P U T “ X = ” ; X

20 IF X ( 1 O R X ) = 1 0 T H E N P R IN T “ N O D E F IN I T IO N ”

30 IF X ) = 1 A N D X ( 3 T 圣IE N P R I N T “ Y = ” ; 5一 CO S ( 8 * X )

4 0 IF X ) = 3 A N D X ( 6 T H E N P R I N T “ Y = " ; E X P ( X )
5 0 IF X >= 6 A N D X ( 1 0 T } {E N P R IN T “ Y = ” ; 1+ S Q R ( X 二 1)
60 E N D

在此例中 , 我们只处理了 X e 〔1 , 10 ) 的正常情况 , 但估计到使用 中出现 的变动可能导致 x ( 1 或 x ) 1 0, 提 前将调试语句放 在程序 中。 这样 , 对于任何情况的输入程序都能 适应。 人是健忘 的 , 如果没有这个调试语句 , 将会花费很多时间去查错 。

错误诊断的推理技术

归纳法排错 ( D E B U G G I N G B Y I N D U C T IO N )

其 荃本思想是 逐步减少和改进 假定的过程 。 在查 出错误后 , 要把一切可 能的原 因和假定都提出来 , 利用 错误数据 排除一部分 , 假 定再从其余 假定中估计可能性最大的一个 。 使确 定错误原 因的范围更集 中 , 下一步 或 许就可证明这一改进后的假定 , 或再作其他选择 .

 

 

演绎法排错

其基本思 想是枚举所有可能引起 出错的原 因作为假设 , 然后利用数据逐一排除不可能发 生的原 因与假设 , 将 余下的原 因作为主攻方向。 演绎法过程如下 图所示 :

 

 

回溯法 ( B A C K T R A C KING)

对于小程序 , 这 种技术极为有用 。 从错误 出现之处 出发 , 沿反 向路径进行检查 , 直到找出错误的原因 。 推理是在取得一 定的实验数据的基础 上进行 的 , 推理 得出的假设 , 要靠实验证 明 并取得 新的数据 , 把搜索 范围缩 小。故错误诊断的 实验方法与推理技术应结合使用 , 互相补充 。

错误修改的原则

不要试着改

不要当只 查到 了一些征兆 , 原 因还没有 查清 , 便想试 着改 动某个语句 。 这 种盲 目行 为成功 的概率很 小 。 因 某些错误征兆 的修改并没有治本 。 有时会把 某些新的错 误掺加到程序 中 , 造 成调试 的混乱 。

修改 了一个错误 , 可能还 会有别的错误

一般错误 是密集 的 , 修改了一个错 误后 , 还应检查它的近邻还有没有别的错误或者在程序 中还有无类似 的错误 。

改变源程序代码 , 不要改变目标代码

当调试一个大 系统 , 特别是用 汇编语 言写的系统纠错时 , 不要直接修改目标代码。 否则 , 当程 序重新编译 或重新汇编时 , 错误 还会再现

修改错误的过程将迫使人们暂时回到设计阶段

修改错误是 程序设计的一个重要 内容和形式 。 一般 说来 , 在设计过程中所使用 的各种方法应 能应 用于错 误修改过程 。

修改完毕 . 需进行 回溯测试

因为 :

  • 纠正错 误的概率 不是 10 0 %
  • 纠正错误 时产生新错误 的可能性
  • 修改代 码比 原有的代码更 易出错