2023年1月

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

调试的步骤

诊断错误

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

确定错误的源发点

发现错 误的地方不一定是错误的源发点 , 应寻找所有与错误有关 的地方 , 从而确定错误的源发点 。 例如程序 :
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 %
  • 纠正错误 时产生新错误 的可能性
  • 修改代 码比 原有的代码更 易出错

由于软件调试是软件质量鉴定工 作必须 具备的前提条件 , 而且软件调试 过程关系到 软件质量的优 劣 , 所以 , 专门讨论软件调试技未的有关内容。

目前 , 软件设计人员中存在着一种错误 的认识 , 即认为软件调试只 是为了证明 自己 设计的软件或怪序的正 确性。 在这种思想指 导下 , 软件设计人 员往往会选择简单的调试 方法 和简单的数据情况 , 往往仅完成主 要 功 能的调试 , 这就造成了调试过程不 全面、 不 完整 , 使软件在投 入实际运行 后无法长期可 靠的工 作。

正确的软件调试作用为: 软件调试是为 了发现 错误而执行软件的过程 。 结果通 过软 件调试发现 了错误 , 并不 是证 明了软件设计 的 失败 , 而 恰好是增加了软件的可靠性和应 用 价值。 所以 , 软件调试是软件开发 中地 位 十 分重要 的一 个工 作阶段。

软件调试工作应遵循的原则

  • 根据软件所具有的功能 、 结构和 数据 类 型来选择调试方案和调试的重 点。
  • 从心理学和调试有效性而论 , 软件设 计者不应调 试 自己 的软件。 如 果没有条件做 到这 一点 , 可以采用 一些集体工作的措施来 弥补 。
  • 在软件调试 中, 不 但 要 对 那些合法的、 正常的情况进 行调试 , 而且要对那些 非 法的 、 异 常的情况进行调试。 调试应 对软件 具有一定的破坏力。
  • 在软件调试 中, 不但要 检查软件是否 完成了应完成的功能 , 而且要 检查软件是否 完成了不 应有的功 能。
  • 任何调试数据、 调试结果和错误 内容 都是极有价值的资料 , 应 认 真 地 分析、 保 存 。 某些 情况可能需要反复调试 、 纠错。
  • 国 外的有关统计资料表明: 已发现较 多错误 的软件与已发现较少错误 的软件相比 较 , 前者仍存在错误 的概率较大 。
  • 国外的 研究资料表明: 要结束一个软 件的调试过程是有条件的 , 是根据诸多因素( 如已发现的错误率 、 查错效率、 调试覆盖 率等 ) 综合分 沂后做出的 。

软件调试的几种工作方式

软件调 试中常见的 几种工作方式 和工作 内容见表 l 。 在适用 范围 一栏中, 根据调试 的规模分了三个等级: 模块级 、 功 能级、 系 统级 。 各等级的任 务如下:

  • 模块调试: 对相互独立的各个模块 在 各种条件下进行运行 , 检查模块内部的运算 和逻 辑关系 、 控制关系的正确性。
  • 程序功 能调试: 对于组成某一功能 的 各个模块进行联调 , 检查 在各种条件下的执 行过 程 和执行结果 , 特别是模块之 间的层次 关系和信息 交换 的正确性 。
  • 系统调试: 将各个功能的程序汇合 , 在实际或模拟的工作 环 境中, 检查输入 、 输 出是否符合要求 , 测定并评价软件的各项性 能 , 提出改进意见。

由于三个等级的调 试内容与规模不同 , 所采用的工 作 方式和工 作方法也不相同。 一 般 地说 , 由数人 组成的 软件调 试小组 可以 达 到相互 启发 , 集思 广议 , 相互检查 , 认真高 效的工 作目的 。 但这要求 软件调 试小组的每 个成员在软件设计与调试技术方面 , 在微机 使用 方面 , 在对整个系统功 能与性能的理解 方面 , 具有较高的水平与深刻的认识 。

软件调试方法

软件调 试中经常采用 的方法见表 2 。 逐 步增长和非增长式两种调 试方法 , 各有优缺 点 , 列举如下:

  • 非增长式调试需 要 完 成 更大的 工作量。
  • 逐步增长式调试 中 , 由于 及时地完成 了模块的组合调试, 模块之间的接 口错误和 由多个模块运行产生的功能错误 , 能及早地发现。
  • 利用逐步增长式调试 , 寻错纠错比较容易。
  • 逐步增长式调试可以 更 彻底 地对软件 进行调试 。
  • 非增长式调试需用的机器时间较少 。
  • 用非增长式调试 , 在整个调试阶段有 可 能并行工作。
  • 在逐步增长式调试法中 , 自顶向下调 试的缺点是很难产生调试条件, 调试的 相当 时间内无法考察结果输出。 而自底 向上调试 的缺点是必须给 出调 用模块 , 在加人最后一个模块之前 , 程序不能作为一个整体存在。
  • 在非增长式调试法中, 黑盒 法的缺点 是寻错纠错极困难 , 白盒法的缺点是工作量 太大 。 综合比较起来 , 逐步增长式调试法是一 种比较好的方法 , 适用于工程应用类 软件 , 建议推广实施。 如果数人同时采用逐步增长 法对同一个软件进行调试 , 就可以 达到相互 补充 、 完整调试的 目的 。

调试工 作中应注意的几点

  • 调 试前应认真地查阅有关设计资料 , 避免 出现对软件设计任务 书的理解不准确 、 不完整而造成的软件设计中的缺陷。
  • 调 试中应认真地检查程序结构 ( 是否 采用模块结构 ) 和指令使用 ( 如运算指令使 用不 当 , 转移指令使用 太多等 ) 。
  • 调试中一定要 对 程 序 进行破坏性试 验 , 考核程序的可 靠性。
  • 调试结束前 , 应 把以 前完成的各个调 试情况复现一遍 , 避免由于 纠正某个程序错 误而出现 了其他的错误 或缺陷。
  • 调试完 成后 , 应尽 可能的对程序进行 优化 , 提高程序质量。

软件调试技术中几 个研究专题

为了更有效 、 更经济地开 展软件调试工 作 , 有必要 在以 下两 个方面进行研究 :

  • 应重视软件调试数据与结果的收集、 分析工作。 目的 有两 个: 第一 , 统计软件出 错类型和产生错误 的原因 , 以 便于 在今后的 软件开发 中加以克服 , 第二 , 分析各种调试 措施或数据条件对各类错误的有效性 , 建立 起一套高效的调试原则。
  • 利用 积累的调试经验 , 建立软件可靠 性分析 , 软件错误预测的模型 , 为寻找结束 调试的最佳时机提供参考或依据。

软件调试工作是软件开 发 中最 复杂、 最 具有技术性和技巧性的工作 , 所以 , 应大力地并展研究和提高工 作 , 保 证软件具有优 良 的质量与性能。

随软件向大型化和复杂化方向发展 . 软件调试的难度 也在不断增大。 对于一 些小的软件 我们可 以不讲究什么方法 . 只要通过插入print语句等简单手段就可 以解决问题 但是如果是要调试一个比较大的系统 . 不讲究必要的调试 技巧就会多花费很多时间甚至根本行不通了。
那么如何掌握调试技巧 , 提高调试效率呢 ?学习基本的调试原理是第一步 . 试想如果我们不 了解调试工具的工 作机制 , 那么怎么可能最大限度地发挥其功能呢。 如果我们根本没听说过硬件断点 . 那么我们怎么能利用它解决普通软件断点无法完成的任务呢 ?
从 宏观来看 . 软件调试是调试工具 、 系统软件 (操作系统)和C P U 这三者密切 配合、 相互协作的一个复杂过程。 简单来说CP U 为软件调试提供了硬件一级的支持 , 是很多调试功能的根 本基础: 操作系统负资协调管理 CPU 所提供的硬件支持 ,并为各种调试工具提供服务;调试工具与调试人 员直接交互 ,使操作系统和CPU所提供的调试支持真正 可用。

下面 , 便以 IA 一3 2 处理器 《CP U ) 为例介绍 CP U 对软件调 试的支持。 IA 一 3 2 处理器是指英特尔3 2 位架构 ( l n t e l ? rA c h i一tc e t u r e 3 2一b it ) 处理器 . 即从 38 6 开始的 x 8 6 处理器 . 包括i3 86 、i4 86、奔腾、p 6 系列和奔腾 4 系列处理器。

可以将 lA 一 3 2 处理器 的调试支持简单概括如下:

  • INT3 指令— 又叫断点指令 . 是软件断点的实现基础 。
  • 标志寄存器 F L A G S 的 TF 标志— 陷阱标志位 . 是单 步执行的实现基础
  • 断点地址寄存器 D R0一 D R 3— 用于设置断点地址 (线性 内存地址或 l /O 地址 ), 是硬件断点的实现基础 。
  • 断点控制寄存器 DR 7— 用来控制和进一步描述四个调 试地址寄存器 (D R O一D R 3 ) 的断点条件 
  • 断点状态寄存器 DR 6— 当断点发生 时 . 向调试器报告该断点的具体情况, 以便调试器区分发生的是哪个断点。
  • 断点异常 (# BP) 一 当 INT3 指令执行时 , 会导致此异常.CPU 转到该异常的处理 程序 。
  • 调试异常 (# DB ) 一 当除 INT 3 指令以外的调试事件 发生时 会导致此异常。
  • 任务状态段 (T S )S 的T 标志 任务陷阱标志 , 当切换到设置了 T 标志的任务时 , 中断到调试器 。
  • 分支记录机制 用来记录上一个分支 、 中断和异常的地址等信息 。

下面我们分几块对以上 内容做进一步讨论:

软件断点

X8 6 系列处理器从其第一代 产品英特尔 8 0 8 6 开始就提供 了一条专门用来支持调试的指令INT 3。 简单来说 , 这条指令的目的就是使 CP U 中断 (陷入 ) 到调试器 . 以供调试者对执行 现场进行各种分析 。
下面通过一 个小实验来感受一下INT 3 指令的工作原理 。
在 V is u a l C + + S tu d io 6.0 ( 以下简称 v C 6 ) 中创建一个简单的He l l o w o r l d 控制台程序HIn t 3 然后在m a i n () 函数的开头通过嵌 入式汇编插入 对INT3指令的调用 :

 

 

当在 V C 环境中执行以上程序时 . 会得到以下对话框 , 点O K 按钮后程序便 会停在 N I T 3 指令所在的位t 。 由此看来我们 刚刚插入的一行 (asm INT 3 ) 相当于在那里设 了一个断点 .

 

 

这正是通过注入代码手工 设盆断点的方法 , 这种方法在调试某些特殊的程序时还 非常有用。
w id n o w s 操作系统还提供了相应的 A P I 用于手 工断点 . 例 如用户模式T 的De bu g B re a k ( ) 和内核模式下的DbgBreakPoint(),DbgBreakPointWithStatus()。 把刚才的小程序中的对 INT 3 的直接调 用改 为调用 Win do w s A PI  De bug B r e a k ( ) (需要在开头 Include< w id n o w s.h> ) . 然后执行可 以看到产生 的效果是一样的。 通过反汇编很容易看出这些 AIP 在x 8 6 平台上其实都只是对INT 3指 令的简单调 用。

  1. 在 windbg 中启动本地内核调试 (参见 w in d b g 帮助文档 ) 然后使用u命令进行反汇编。 提示符 Ikd> 的含义是 “ Lo c a l ke r n e l d “ 。 本地内核调试需要Wind o w s XP或 以上操作系统才支持。
  2. 用来对齐的,没有实际意义。3 2 位 C户 U 通常需要 内存和可执行文件 以 4 字节对齐。
  3. DbgBreakPointWithStatus()允许向调试器 传递一个整型 参数 。

那么C PU是如何从被调试程序调到调试器 的呢 , 这一机制的全部工作过程因操作系统和被调试程序的执行模式 (用户模式还是 内核模式 ) 的不同而有所不同 。目前我们可 以作出如下简单理解 :
C p U 把 INT 3 指令处理 为一种软件异常 . 当执行INT 3指令 时 C PU 会把当时的程 序指针 ( C S 和 EIP) 压入堆栈保存起来,然后通过 中断向量表调 用 lNT 3 所对应 的中断例程。 当我们在调试器 中运行程序时 . 调试器会直接 ( DOS 时代 ) 或间接(通过操作系统的 A P I ) 注册这个中断服务 因此 当 INT 3 中断发生时 . 调试器的代码会被调用而执行 。在实模式下CPU 的执行逻辑如下  :

 

  1. 这是针对实模式的情况 保护模式下会更复杂 . 但 原理 ’类似。
  2. 对于INT 3指令 ,v e c t o r _ n um b e r 为 3.这个操作过程本适用于所有软件 中断和异常。
  3. # G P 即 Ge n e r a l  Protection Exception , 常规保护性错误。也就是说 当中断向量表 的长度 (Lim i t ) 不足 以包 含本向量时,C P U便会产生常规保护异常。
  4. IF语句的结束语句
  5. 当堆栈不足以容纳接下来要压入的6字节内容时,便产生堆栈异常

下面考虑一下调试器是如何设置断点的。当我们在调试器中对代码的某一行设断点时,调试器会先把这里的本来的指令的第一个字节保存起来。然后写入一条INT 3指令。因为INT 3指令的机器码为0xCC,仅有一个字节,所以设置和取消断点时也只需要保存和恢复一个字节。这是设计这条指令时便考虑好的。顺便说一下,虽然VC6是把断点的设置信息(断点所在的文件和行位置)保存在和项目文件相同位置且相同主名称的一个.opt文件中。但注意,该文件并不保存每个断点处应该被INT 3指令替换掉的那个字节。因为这种替换是在启动调试和调试过程中动态进行的。这可以解释有时我们在VC6中,在非调试状态下,我们甚至可以在注释行设置缎带你。当开始调试时,会得到一个图2所示的警告信息。这是因为当用户在非调试状态下设置断点时,VC6只是简单的记录下该断点的设置信息。当开始调试时,VC会一个一个的取出OPT文件中的断点记录 . 并真正将这些断点设置到目标代码的内存映像中。 也就 是要将断点位置对应的指令的第一个字节先保存起来 , 再替换为C C . 即 INT 3 指令 . 这是如果 VC 6 发现某个断点的位置根本 对应不到目标映像的代码段 , 那么便会发出图 2 所示的警告 。

 

下面说说INT 3 断点被触发时的悄形 . 我们仍以V C 6 为例 .也就是使用 VC 6 调试一 个普通的 3 2 位 W in d o w s 应用程序 。 当Cp U 执行到 INT 3 指令时 . 由于 INT 3指令的设计目的就是 中 断到调试器 . 因此CPU 执行该指令的过程也就是准备产生断点异常 (Breakpoint exception简称# B P)并转去执行异常处理例 程的过程。 W in d o w s下所有异常和中断都是先由内核例程处理的. 因此应用程序中的 INT 3会导致 C U P 从用户模式转入内核模式并执行nt!KiTrap03例程。 接下来经过几个内核函数的处理 .因为这个异常是来自内核模式的. 而且该异常的拥有进程正在 被调试 (内核函数可以得到这些信息 ) . 所以内核例程会把这 个异常分发给用户模式的调试器 . 这里也就是VC 6 。 接下来V C 6会根据异常的发生位置 (记录在每个异常的附属数据结构中) 试图寻 找一个与其匹配 的断点记录。 如果找不到 . 那么就说明
导致这个异常的INT 3 指令不是 v C6 动态替换进去的 , 因此会 显示一个图 1 所示的对话框. 意思是说一个 “ 用户 “ 插入的断 点被触发了。 另外值得说明的是 . V C 6 在每次中断到调试器 时 .会先将所有断点处替换为 INT 3的指令恢复成原来的指令 , 然 后再把控制权交给用户 。 所以在调试器下 . 我们是看不到动态插入的 INT 3指令的.

还想介绍一个有趣现象 。当我们用 VC 6 进行调试时 , 常常会观察到一块刚分配 的内存或字符串数组里 面被坟充满了CC。如果是在中文环境下 . 因为x o C CC C 恰好是汉字 ` 烫 ` 字 的简码 . 所以会观察到很多 ` 烫烫烫烫烫烫… ’ . CC 正好是 INT 3 指令的机器码 . 这是偶然的么? 答案是否定的 . 因为这是有意为之 . 为了辅助试 调试版本的运行库会用0xCC 来填充 刚刚分配的缓冲区 . 这样如果 因为缓冲区或堆栈溢出时程序指针意外指向了这些 区域 . 那么便会因 为遇到这些 自动填充的 INT 3指令而马上 中断到调试器 。 另一方面 . 编译器也经常用 INT 3指令来填充函数或代码段末尾的空 闲区域。 这也可以解释 为什么有时我们没有手工插入任何对 INT 3的调用 . 但是也会遇到图 1 所示的对话框。因为使用  INT 3 指令产生 的断点是依靠插入指令和软件中 断机制工作的 . 因此人们习惯把这类断点成为软件断点 . 软件断点具有如下局限性 :

  • 属于代码类断点 , 即可 以让 C PU 执行到代码段内的某个地址时停下来 . 不适用于数据段和 1 / 0 空 间。
  • 对于在RO M ( 只读存储器 ) 中执行的程序 ( 比如 B I O S 或其它固件程序) . 无法动态增加软件断点 。 因为目标内存是 只读的 . 不能动态写入断点指令 。 这时就要使用我们后面介绍 的硬件断点。
  • 当中断向量表或中断描述表 (IDT) 没有准备好或遭到破 坏的情况下这类断点无法或不能正常工作的。 比如系统刚刚启动时或者IDT被病毒窜改后 。 这时只能使用硬件级的调试工具。

虽然软件断点存在以上不足 . 但因为它使用方便 , 而且没 有数量限制 (硬 件断点需要寄存器记录断点地址 . 有数量限制 ),所以目前仍被广泛应用。
关于 INT 3指令还有一点要说明的是 . IN T 3 指令与当n=3时的 INT n 指令 (通常所说的软件中断) 并不同. INT n 指令对 应的机器码是CD后跟 1字节n 值 . 比如INT 23H 会被编译为CD23 。 与此不同 INT 3 指令具有独特的单字节机器码 CC 。 也就是当编译器看见 IN T 3 时会特别的将其编译为 CC . 而不是 CD 0 3。尽 管没有那个编译器会将 INT 3 编译成 CD 0 3. 但是可以通过某些方 法直接在程序中插入 CD 0 3 。 但是这样做会失去IN T 3 指令所具有的 特殊待遇 (例如在虚拟 8086模式下免受IOPL检查).

从38 6 开始 , IA 一3 2 处理器 内部都含有 8 个 3 2 位的调试 寄存器 DR0一 DR7 (如 图 1 所 示 )。 除了 DR4 和 DR5 保留 外 , 其它 6 个寄存器 分别是 :

  • 四 个 3 2 位的调试地址 寄存器 (DR0~DR3)
  • 一个 3 2 位的调试控制寄存器 (DR7) 
  • 一个 3 2 位的调试状态寄存器 (DR6) 

通过 以上寄存器可 以最多设置 4 个断点, 其基本分工是DR0一DR3 用来指定断点的内存 (线性地址) 或l/ O 地址 。 DR7 用 来进一步定义断点的中断条件。 DR6的作用是当调试事件发生向调试器 《d e b g u g , ) 报告事件的类别和属性 . 以供调试器判断发生 的是何种事 件 (哪个断点 、 单步跟踪 、 断点属性等等 )。 下面分 别详细介绍 D R 7 和DR6 的用法 。

3 2 位的DR7寄存器 中 , 有2 4 位是被划分成 4 组分 别与 4 个调试地址寄存器相对应的.比如 L0 . G0 . R/ W0 和 LEN0这 六位都是 与DR0相对应的 .L1 . G1. R/ W1 和 LEN1 这六位都是与DR1相对应的 依此类推。表 1 列出了DR7 中各个位的具体含义。

调试状态寄存器 DR6 是 当 CPU 检测到匹配 断点 条件的断点时 . 用来向调试器的断点异常处理程序传递该断点异常的具体情况 . 以使调试器可 以很容易 的识别 出发 生的是什么断点 。 比如如果 BO被设置为1, 那么就说 明满足 DR0、 L E N 0、和 R/ W0 所定义条件 的断点发 生了 。 下面的表分 别列出了 DR6 中各个位的具体含义。

通过表 1 的定义可 以看出 . 调试控制寄存器的各个位域提 供了很灵活 的方式 .允许我们通过不同的组合定义出各种复杂的断点条件。 下面我们先进一步介绍一下读写域R/ Wn , 通过 对它的设里 , 我们可 以指定要定义的断点的访问类型 (数据 、代码还是 l/ 0 ) . 即断点的访问条件 :
1、读写内存中的数据时中断 , 这种断点又被称为数据访 问断点 (data breakpointer )。 利用数据访问 断点 可 以监 控对全局变量 , 或局部变量的读写操作。 例如 . 在进行某些复杂的系统级调试 . 或者调 试多线程程序时 , 我们不知道哪个线程在何时修改了某一变量,这时我们就可 以设置一个数据访问断 点。 现代调试器大多还 都支持复杂的条件断点, 比如 当某个变 量等于某个确定的值时中断 . 这其实也是利用数据访问断点实 现的 . 其基本思路是设置 一个数据访问断点来监视这个变量 , 每次这个变量改变时 CPU 都会调用调试器 的中断处理程序 ,调试器检查这个变量的值 . 如 果不 满足规定的条件就立刻返回让 CPU 继续执行 。 如果满足 , 就中断到调 试环境。
2、执行 内存中的代 码时 中断. 这 种断点 又被称为代码访问断点 (Code Br e a k Po in t 》或指令断点 (i n s tr u e t io n b r e a k po in t ) 。 代码访问断点从实现 的功 能上 看与软件断点类似 , 都是 当 C PU 执行到某个地址 的指令时 中断。但是通 过寄存器 实现的代码访问断 点具有一个软件断点无法 实现 的优点 , 就是不 需要 软 件断点那样向 目标代码处插入指令 。 这个优点在某些 情况 下非 常重要 。 例如 . 当我们调 试位于 RO M (只读存储器 ) 上的 代码 《比 如 BIO S 中的 PO S T 程序 ) 时 根本没 有办法向那里 插入软件断点 ( lNT 3 ) 指令 . 因为 目标内存是只 读的 。 另外软件断点的另 一个局限是 只有 当 目标代码被加载进内存后才可 以 向该区域设置软件断点。而调 试寄存器 断点没有这 些限 制. 因为只 要把需 要 中断的内存地址 放入调试地址 寄存器 (DR0一DR3 ) . 并设置好调试控制寄存器 ( DR7 ) 的相应位就可 以 了。

3、读写 I/O(输入输出 ) 端口时中断 .这种断点又被称为I/O访问断点 ( I  /O   b r e a kp oi n t ) 。I/ O 访问断点对于调试设计使用输入输出端口的设备驱动程序非常有用。 也可以利用 I/O 访问断点来监视对I/O空间的非法读写操作 . 提 高系统的安全性 。 因为某些恶意程序在实现破坏动作时 . 需要对某些 I/O 端 口进行读写操作 。

读写域定义 了要监视的访问类型 . 地址 寄存器(DR0一DR3 ) 定义 了要监视的起始地址 。 那么要监视的区域长度呢 , 这便是长度域 L E N n (n= 0, 1, 2,3 . 位于 D R 7 中 ) 的任务。L EN n 位段可 以指定 1,2,4 或 8 字节长的范围。 需要说明的是 :
1、对于代码访 问断点 . 长度域应该为00,代表一字节长 度 。 另外地址寄存器应该指向指令的起始字节 。 也就是 CPU 只会用指令的起始字节来检查代码 断点 匹配 。
2、对于数据和 I/O访问断点 . 有两点需要注意 :
第一 只 要 断点区域中的任一字节在被访问的范围 内 . 都会触发该断点。
第二 , 边界对齐要求 . 两字节区域必 须按字 (w o r d ) 边界对齐 : 四字节区域必须按双 宇 (d o u b l e w o r d ) 边界对齐 : 八字 节区城必 须按四字 (qu a d w o r d ) 边界对齐。 也就是说 . CP U 在 检查断点匹配时会 自动去除相应数量的低位 。 因此如果地址没 有按耍求对齐可能无法实现预期的结果。 例如 假设希望通过将 DR0 设为0 x A 003.L E N 0 设为 1 1 (代表 4 字节长)实现任何对0 x A00 3 ~0 x A c 0 6 内存区的写操作都会触发断点: 那么只有当0 x A 00 3 被访问时会触发断点 . 对0x A 00 4、0x A 00 s 和0x A006 处的内存访问都不会触发断点 。 因为长度域指定的是4 字节 所以CPU 在检查地址匹配时 . 会自动将起始地址0x A003 的低4 位屏蔽掉 , 也就是会被看作是0x A00 )。 表3 给出了更多的例子 用来说明断点的触发条件 。

因为以上介绍的断点不需要像软件断点那样向代码中插入 软件指令 , 依靠处理器本身的功能便可 以实现 . 所以人们习惯 上把这些使用调试寄存器 (DR0~DR7 ) 设置的断点叫硬件断点 ,以与软件断点区别开来 。
l A 一 32 处理器专门分配了两个中断向量来处理调试异常 向量1 和向量3。 向量3 用来处理 INT 3 指令产生的断点异常。 向量1 用来处理调试异常(de b u g e x c e pt io n . 简称 # DB ) 。 硬件断点产生的是调试异常 .所以硬件断点发生时C U P 会执行 1 号向量对应的处理例程 。

硬件断点其 有很多优点 , 但是也有不足 , 最明显的就是数量限制因为只用 4 个调试地址寄存器 . 所以 lA 一3 2 Cpu 允许 最多设置 4 个硬件断点。 这基本可以满足大多数情况下的调试 需要 。

另一点祷要说明的是 . 只有在实模式或保护模式的内核优先级( ring0 ) 下 才能访问调试寄存器 . 否则便会导致保护性 异常。 这是 出于安全性的考虑. 那么像 v i s u a . s t u d i o 这样的用 户态调试器是如何设一硬件断点 (VC 6 支持数据访问断点。 没 必 要也不支持 l/ O 访问断点 , 因为从 Win d o w s 9 8 开始用户态 下不允许进行直接 l/ O 读写 ) 的呢 ? 答案是通过访问线程 的C ON E T X T 数据 (每个线程被挂起时 . 寄存器等上下文信息都被 保存起来 . 当该线程恢复执行时 . 寄存器会被恢复回来) 来间 接访问调试寄存器 。

下面给出一个C + + 例子演示一下如何手工设置 数据访问断点.

 

 

两期我们分别介绍了通过 INT3指令设置的软件断点 和通过调试寄存器设置 的硬件断点。 除了断点 . 还有一类常用的方法使C P U 中断到调试器 . 这便是调试陷阱标志 (debug trap  f l a g ) 。 也就 是 当这些陷阱标志被设置时, 一旦有符合陷阱条件的事件发生,就会陷入到调试器 。 IA 一3 2 处理器所支持的调试陷阱标志可以概括如下

  • 80866就支持的单步执行标志 (标志寄存器E F a g I s 的一位)
  • 3 8 6 引入的任务状态陷阱标志(任务状态段TSS 的T标志)
  • 奔腾Pro引入 的分支到分支单步执行标志 (D e b u gC t !寄存 器 中的BT F 标志)

下面分别详细介绍 。

单步执行标志 (T F )

从 8 0 8 6 C pU 开始 , x 86 系列处理器的标志(EF Ia g s ) 寄存器 中便有一个陷阱标志位 ( b i t 8 )名为Trap Flag 简称T F。当 T F 位为 1 时 CPU 每执行完一条指令便会产生一个调 试异常 《# DB ) . 中断到调试器的调试异常处理 程序。 这就是 通 常所说 的单步执行 。 调试异常的向量号是 1 因此 T F 标志引发的是 1 号异常服例程 。在 80 86 和 2 8 6 时代 , 这个服务 例程是专门用来处理 单步事件的。 从 3 8 6 开始 . 硬 件断点发.生时也会产生调试异常 . 调 用 1 号服务例程 . 但可利用调试 状态寄存器(DR66 ) 来识别发生的是何种事件 。 为了表达方便我们把因 T F标志 触发的软件异常称 为单步异 常。

单步异常属于陷阱类异常。 也 就是 C P U 总是 在执 行完导致此类异常的指令后 才产生该异常。 这意味 着 当因单步异常中断到调 试器 中时, 导致 该异 常的指令已经执行 完毕 了 。 软件 断点 异常(#BP) 和硬 件断点中的数据及I/O 断点 也是 陷阱类异常. 但是硬 件断点 中的指令访 问异 常是错误类异常 . 也就是当由于此异常而 中断到调试器 时 . 相应调试地址寄存器 DRn中所指地址 处的 指令还 没“执行 。 这是 因为CPU是 在取指操作时进行此类断点 匹配的 。

CPU是何 时检查 TF标志的呢?’IA-32手册的原 文是 "while an instruction is begin executed" 也就 是在执行一个指令的过程 中。 尽管没有说过程 中的哪个阶段 (开 始 、 中间还 是末尾 ) . 可以推测应 该是 一条指令即将执行完毕的时 候 。 也就是说 当CPU在 将执行完一条指令的时候检测TF位 ,如果该位为 1那么 CPU会先清除此位 , 然后准备产生异常。 但 是 这里 有个例外 , 对于 那 些可 以设置 TF 位的指令 (例如 POPF) CPU 不会在执行这些指令期间做以上 检查。 也就是说这些 ,旨令不会立刻产生单步异常 . 而是其后的下一 条指令将 产生单步异常。
因 为 C P U 在进入 异 常 处理 例 程前会 自动 清除 T F标 ,因此 当 CPU中断到调 试 器 中后 再观 察 TF标 志 . 它总是0。

既然调试异常的向量号是 1 ,可 不可 以像 INT 3 那样通过在代码 中插入INT 1 这样的指令来实现手工断点呢 , 对于应用 程序答案是否定的。 INT 3尽 管具有INTn的形式 . 但是它具有独特的单字节机器码 . 而且 其作用就是产生一 个断点异常 一(#BP ) 。 因此 系统对其有特别的对待 .允 许其在用户模式下执行。 而 INT 1则不然, 它属于普通的 INTn 指令 机器码为 CD01。 保护模式下如果执行 INTn指令时,当前的 CPL大于引用的门描述符的DPL。那 么便会导致通 用保 护异常 (# GP) 。 在 W id n o w s 20 0 0 和 x p 这样的操作系统下,INT1对用的中断门描述符的DPL 为 0 . 这要求只有内核模式的代码 才能执 行 IN T 1指令,访 问该中断门。 也就是说 , 用户模 式下 的应用程序没有权利使用INT 1指令 。 一 旦使用就会导致一个一般保护性异常 ( # GP) 。W id n o w s会将其封装为一个访 `问违例错误 (如图2一 2 1 所示 )。 在内核模式 下 可 以在代码 (驱 动程序)中写入INT 1指令。CP U 执行到该指令时会转去执行 1号向量对应的处理例程 , 如果在使用 W in D g b 进行内核级调试. 那 么会中断到W i n o b g 中 . W i n o b g 会以为是发生了一个单 步异常 如 图 1 所示 。

下面谈谈调试高级语言时的单步机制 。 由于高级语言的一条语句通常都对应多条汇编指令 例如 , 表 1 中C+ 十的一条语句= i a 十b* “ + d / e + f / g 十 h对应于 1 5 条汇编语句 。 因此容易想到单 步执行这条C+ + 语句有几种可能方法 。 一种是也用一标志一步 步的走过每条汇编指令 , 这种方 法意味着会产生 1 5 次调试异常 . CPU 中断到调试器 1 5 次.不过中间的 1 4 次都是简单的重 新设 皿起 下F 标志 便让 CPU返 回执行 。 第二种方法是在 C + + 语 句对应 的最后一 条汇编指令处动态插入一 条lN 下 3指令 . 让C P U 一下子 跑到那里 然后 再单步执行一次将被替换的那条 指令执行完 . 这种方 法需要 CPU中断到调试器 两次 。 第三种方法是 在这条 C++ 语句的下一条语 句的第一 条汇编指令处( 即行 18 ) 替换入一 个 N I 下 3 . 这样 C PU 中断到调试器一次就可 以了 。
表 1 : 高级语 言的单步执行

 

 

 

 

 

后两种方法较第一种方法速度会快很多 . 但是不幸的是并不总能正确的预测 出最后高级语言对应的最后一条指令和下一行语句开始指令 (要替换为INT 3 的那一条指令)。比如 2 8 行的 e l s e if (b ) 语句就很难判断出它对应 的最后一条汇编语句 和 下一 条高级 语言语 句的起始指令 。 因此 今天 的大 多数调试器在进行高级语言调试时都是使 用第一种方法来实现 单步跟踪的。关于 TF标志还有一点值得注意: . INTn 和 INTO 指令会清除TF标志 , 因此调试器在单步跟踪这些指令时 . 必须做特别处理。

任务状态陷阱标志

除了标志寄存器中的陷阱标志 (TF ) 位.38 6 引入 了一种新 的调试陷阱标志 任务状态段 (TSS ) 中的T标志。任务状态段 (Ta s k一S t a te S e g m e n t ) 用来记录 一个任务 (CPU 可以独立调度和执行 的程序段 ) 的状态 . 包括通用寄存器的值 . 段寄存器 的值和其它重要信息 。 在任务切换时 , 当前任务的状态会被保存到这个内存段里。当要恢复执行这个任务时 . 会先根据这个 记录来把寄存器 的值恢复回来。 T S S 中 , 在宇节偏移为 10 0 的 1 6 位字 (w o r d ) 的最低位是 调试陷阱标志位 . 简称 T标志 。 T 标志被设置为 1, 那么 当CPU切 换到这个任务时 , 便会产生调试异常。 准确的说CPU 是在程序控制权已经转移到新的任务 , 但还没有开始执行新任务的第一 条指令前产 生异常的。 调试 中断处理程序可 以通过调试状态 寄存器 (DR6 ) 的 BT来识别出发生的是否是任务切换异 常。 值得注意的是 . 如果调试器接管了调试异常处理 . 而且该处理例程 属于一个独立 的任务 , 那 么一定不要设置该任务的TSS段中的 T位。否则便会导致死循环。

分支到分支单步执行标志 ( B T F )

在 lA 一 3 2 处理器 家族中 , 所有的 P e n i t u m Pr o  Pe n t l u m II和 Pe n t . u m III处理器 (包括相应的 C e .e r o n 《赛扬 ) 和 X e o n (至 强 ) 版本) 因为都是基于相 同的 6 P 内核 《C o r e ) . 因此经常被 统称为P6处理器 。 P6处理器引入 了一项新 的对调试非 常有用 的功能 : 记录 分支 、 中断和异 常事件 . 和 针对分 支单步执行(s in g le 一s z e p o n b r a n e h ) 的功能。 奔腾 4 处理器对这一功能又 做了很大增强 。 下面具体介绍一下按分支单步执行的功能和使 用方法 。
首先解释一下分支到分支单步执行的含义 。 前面我们介绍过 , 当 ElF a g s 寄存器的T F 位为 1 时 C PU 每执行完一条指 令便会中断到调试器 . 也就是以指令为单位单步执行 。 顾名思义, 针对分支单步执行就是以分支为单位单步执行 。换句 话说 , 每单步 ( s t ep ) 一次 C P U 会一直执行到有分支 、 中 断或异常发生 。 下面 结合表 2 中的代码进行说明。
那么如何启用按分支单步执行呢? 简单来说 . 就是要 同时设置TF和BTF 标志 。TF 标志位干 E F Ia g s寄存器, 大家已经很 熟悉。 BTF 标志位于名为DebugCtlMSR 寄存器中 。

 

 

 

在V C6 的ID E 环境下(系统的 C PU 应该是 P6 或更高 ),先在第 2 2 行设置一个断点 . 然后按F5 运 行到这个断点位置 。 行 1 9是用来启用按分支单步执行功能的 . 也就是设置BTF 标志. 细节我们等一下再讲。 接下来 . 我们按 F10单步执行 , 会发现一下子会执行到行 3 1 . 从第 2 2 行单步一次执行到第 3 1 行 这便 是按分支单步执行的效果。 那么为什么会执行到第 3 1 行呢? 按照分支到分支单步执行的定义 , CP U 会在执行到下一次分支、 中 断或异常发生时停止。 对于我们的例子 . CP U 在执行第 2 0 行对 应的第一条汇编指令时 , CP U 会检测到 TF 标志 (因为我们是按F10单步执行 . 所以 VC 6 会帮助我们设置TF 标志)。

此外 ,P6及以后的 lA 一 3 2 CP U 还会检变 BTF 标志 . 当发现BTF标志也被设置起时 , C P U会认为当前是在按分支单步执行 . 所 以会判断当前指令是否是分支指令 , 如果不是 ,CP U 便会继续执 行。 因为第 2 2 行的第一条汇编指令不是分支指令 . 所以 CP U 会 继续执行。 依次类推 . CP U 会连续执行到第 2 4 行的 i f 语句对应的最后一条汇编指令jne (参见表 3 )。 因为这条语句是分支语句 ,所以 CPU会认为已经满足停止执行的条件 , 在清除 , TF 和BTF标志后,产生单步异常中断到调试器 。 中断到调试器时 . 分支语句已经执行完毕 , 但是跳转到的那条语句 (即表 2 中的行 3 1 } 还 没有执行 。

 

对以上过程还有有几 点需要说明:

第一 如果在从第 2 2 行执行到 2 4 行过程中,有中断或异 常发生 , 那么 C P U 也会认为停止单步执行的条件已经满足. 因 此 按分支单步执行的全称是按分支 、 异常和中断单步执行。

第二 , CPU 认为有分支发生的条件是执行以下分支指令 :JM P (无条件跳转), JCC (包括条件跳转指令 , 如 J A 、 J A E、 JNE等等)、L OO P (循环 ) 和 Call( 函数 / 过程调用 ) 。

第三 , 由于只有内核代码 才能访问MS R 寄存器 (通过RDMSR 和四WRMSR 指令) 所以上面的例子 中 , 在 WriteMSR()函数中 我们使用一个未公开 的 A p l  ZwSystemDebugControl()来设置 BTF 标志。