《应用程序调试技术》以作者自己的多年编程和调试经验着重介绍了各种语言的程序调试工作。其内容包括程序错误和故障的类型、小组调试所需要的基础结构要求和在编写代码时如何进行预先的调试;什么是调试器,并描述了调试器的工作原理,作者还以相当深度讨论了Visual C++调试器和Visual Basic调试器以使您能最大限度地发挥它们的功用;提供了一些在编写基于Windows的应用程序时遇到的常见的调试问题的解决方案。在本书的“附录”部分提供了您在进行调试冒险时会发现十分有用的一些附加信息。\r\n\r\n 对于倍受程序调试折磨的编程人员来说,本书将能成为您不可多得的良师益友。\r\n
\r\n
前言 \r\n\r\n 结论 \r\n\r\n 第l部分 调试概论 \r\n\r\n 第1章 错误:问题出在那里, 如何解决 \r\n\r\n 1. l 错误及其调试 \r\n\r\n 1. 1. l 什么是错误 \r\n\r\n 1. 1. 2 进程错误及其解决方案 \r\n\r\n 1. 1. 3 制定调试计划 \r\n\r\n 1. 2 调试的先决条件 \r\n\r\n 1. 2. l 技能组合 \r\n\r\n l. 2. 2 学习技能组合 \r\n\r\n 1. 3 调试过程 \r\n\r\n 1. 3. l 第1步:复制错误 \r\n\r\n 1. 3. 2 第2步:描述错误 \r\n\r\n 1. 3. 3 第3步:始终假定错误是你自己的问题 \r\n\r\n 1. 3. 4 第4步:分解并解决错误 \r\n\r\n 1. 3. 5 第5步:进行有创见的思考 \r\n\r\n 1. 3. 6 第6步:杠杆工具 \r\n\r\n 1. 3. 7 第7步:开始繁重的调试工作 \r\n\r\n 1. 3. 8 第8步:校验错误已被更正 \r\n\r\n 1. 3. 9 第9步:学习与交流 \r\n\r\n 1. 3. 10 调试过程的决定性秘诀 \r\n\r\n 1. 4 小结 \r\n\r\n 第2章 开始调试 \r\n\r\n 2. 1 跟踪变更直到项目结束 \r\n\r\n 2. 1. l 版本控制系统 \r\n\r\n 2. 1. 2 错误跟踪系统 \r\n\r\n 2. 1. 3 选择正确的系统 \r\n\r\n 2. 2 制定构建调试系统的进度表 \r\n\r\n 2. 2. l 用调试符来连编所有的构件 \r\n\r\n 2. 2. 2 警告与错误同等重要 \r\n\r\n 2. 2. 3 了解在何处装载DLL \r\n\r\n 2. 2. 4 设计发布构件的轻便诊断系统 \r\n\r\n 2. 3 日常连编和冒烟测试是必须遵循的 \r\n\r\n 2. 3. l 日常构件 \r\n\r\n 2. 3. 2 冒烟测试 \r\n\r\n 2. 4 立即连编安装程序 \r\n\r\n 2. 5 QA必须对调试构件进行测试 \r\n\r\n 2. 6 小结 \r\n\r\n 第3章 边编码边调试 \r\n\r\n 3. 1 注意声明 \r\n\r\n 3. 1. l 如何声明, 声明什么 \r\n\r\n 3. l. 2 不同类型的Visual C+十和Visual Basic声明 \r\n\r\n 3. l. 3 SUPERASSERT \r\n\r\n 3. 2 跟踪. 跟踪. 跟踪. 再跟踪 \r\n\r\n 3. 3 注意注释 \r\n\r\n 3. 4 相信自己, 但要校验(单元测试) \r\n\r\n 3. 5 小结 \r\n\r\n 第II部分 高效率的调试 \r\n\r\n 第4章 调试器的工作原理 \r\n\r\n 4. 1 Windows调试器的类型 \r\n\r\n 4. 1. 1 用户模式调试器 \r\n\r\n 4. 1. 2 内核模式调试器 \r\n\r\n 4. 2 Windows 2000操作系统为调试对象提供的支持 \r\n\r\n 4. 2. 1 Windows 2000堆阵检查 \r\n\r\n 4. 2. 2 在调试器中自动启动 \r\n\r\n 4. 2. 3 快速中断项 \r\n\r\n 4. 3 MinDBG:一个简单的Win32调试器 \r\n\r\n 4. 4 WDBG:真正的调试器 \r\n\r\n 4. 4. l 内存读写操作 \r\n\r\n 4. 4. 2 断点和单步执行 \r\n\r\n 4. 4. 3 符号表. 符号引擎和堆栈遍历 \r\n\r\n 4. 4. 4 Step Into. Step Over和Stap Out功能 \r\n\r\n 4. 4. 5 WDBG调试器的一个有趣的开发问题 \r\n\r\n 4. 5 如果需要编写自己的调试器 \r\n\r\n 4. 6 WDBG调试器之后是什么? \r\n\r\n 4. 7 小结 \r\n\r\n 第5章 使用Visual C+十调试器进行强有力的调试 \r\n\r\n 5. 1 高级断点及其用法 \r\n\r\n 5. 1. l 高级断点语法和位置断点 \r\n\r\n 5. 1. 2 在任何函数上快速中断 \r\n\r\n 5. 1. 3 在系统或输出的函数中设置断点 \r\n\r\n 5. 1. 4 位置断点修饰符 \r\n\r\n 5. 1. 5 全局表达式和条件断点 \r\n\r\n 5. 1. 6 Windows消息断点 \r\n\r\n 5. 2 远程调试 \r\n\r\n 5. 3 技巧及窍门 \r\n\r\n 5. 3. 1 设置断点 \r\n\r\n 5. 3. 2 Watch窗口 \r\n\r\n 5. 4 小结 \r\n\r\n 第6章 使用x86汇编语言和Visual C++调试器Disassembly窗口进行强有力的调试 \r\n\r\n 6. 1 CPU的基础知识 \r\n\r\n 6. 1. l 寄存器 \r\n\r\n 6. 1. 2 指令格式和内存编址 \r\n\r\n 6. 2 关于Visual C+十内联汇编器 \r\n\r\n 6. 3 需要了解的指令 \r\n\r\n 6. 3. 1 堆栈处理 \r\n\r\n 6. 3. 2 最常用的几个简单指令 \r\n\r\n 6. 3. 3 常见的序列:函数入口和出口 \r\n\r\n 6. 3. 4 变量访问:全局变量. 参数和局部变量 \r\n\r\n 6. 3. 5 调用进程和返回指令 \r\n\r\n 6. 4 调用约定 \r\n\r\n 6. 5 需要了解的其他指令 \r\n\r\n 6. 5. l 数据处理 \r\n\r\n 6. 5. 2 指针处理 \r\n\r\n 6. 5. 3 比较和测试 \r\n\r\n 6. 5. 4 条约和分文指令 \r\n\r\n 6. 5. 5 循环 \r\n\r\n 6. 5. 6 字符串处理 \r\n\r\n 6. 6 常见的汇编语言结构 \r\n\r\n 6. 6. 1 FS寄存器访问 \r\n\r\n 6. 6. 2 结构和类引用 \r\n\r\n 6. 7 完整的例子 \r\n\r\n 6. 8 Disassembly窗口 \r\n\r\n 6. 8. 1 导航功能 \r\n\r\n 6. 8. 2 在堆栈上查看参数 \r\n\r\n 6. 8. 3 Set Next Statement命令 \r\n\r\n 6. 8. 4 Memory窗口和Disassembly窗口 \r\n\r\n 6. 9 技巧和诀窍 \r\n\r\n 6. 9. 1 Endians \r\n\r\n 6. 9. 2 垃圾代码 \r\n\r\n 6. 9. 3 寄存器和Watch窗口 \r\n\r\n 6. 9. 4 从ASM文件中学习 \r\n\r\n 6. 10 小结 \r\n\r\n 第7章 使用Visual Basic调试器进行强有力的调试 \r\n\r\n 7. 1 Visual Basic P代码 \r\n\r\n 7. 1. l 关于P代码的历史教训 \r\n\r\n 7. 1. 2 使用P代码衍生出来的问题 \r\n\r\n 7. 2 错误陷阶:Break In或Break On选项 \r\n\r\n 7. 2. 1 Break On All Errors功能 \r\n
\r\n
如果按照通常的惯例, 我应该联系有关程序中充满了错误与故障的故事并提醒大家有错误与故障的应用程序是社会麻烦的根源, 以此来开始前言. 然后就滔滔不绝的列出从调查报告中得出的有关每100 LOC(代码行)中就有多少错误与故障的平均统计数. 这些都是正统的主题, 但是我认为读者们已经知道了消除错误与故障的重要性才来读这本书. 这样, 我想在这本书中需要完成三项任务:
. 说明黑客John Robbins是谁
. 说明为什么你需要读这本书
. 讲述有关John Robbins的经典故事
第一次与John相遇是在CompuServe论坛上. 当时他正致力一个国产调试器的研究工作, 并提出了一个关于Microsoft Win32 Debugging API的问题. 他开玩笑地补充说, 如果他完成了这个国产调试器, 也许就能够在NuMega公司找到一份工作. 我留意到了这一消息并给了他答复. 我还顺便提及NuMega公司正在物色人员(难得的精通调试的人才).
现在开始讲述John兴冲冲地到NuMega公司面试的故事. 他身着制服(这也许是他唯一的一套制服), 并带着一叠精心格式化了的磁盘, 磁盘里是他完成的调试器的代码样本. 面试时, NuMega公司的一名领导睡着了(或者是John以为他睡着了. 而事实上那位领导非常专心致志的在听). John以为自己就这样失去了这份工作的机会, 但是事实上, 他给所有人都留下了深刻的印像. John被录用的几个月之后, 他发现我使用的咖啡杯垫正是他的代码样本磁盘. 事实上, 我从与John的交谈中就发现了他是调试方面的中坚人才, 我无须对他的代码进行检验.
那时, NuMega公司的规模还很小, 完全处在致力于为开发人员取得得力的调试工具的阶段. John为这场改革作出了贡献. 他是那么急切地想要投入到NuMega公司的工作中, 所以他马上就从佛吉尼亚搬迁到了New Hampshire来, 在他所找到的第一处租居, 即消防站后面的一所房子里住下来. 每次当消防站的大门呼啸着打开时, John的PC机就会断电. 在工作中, John接受的第一项任务就是编写BugBench, 这是演示BoundsChecker能够找到的所有错误的一个实用程序. 在那段时间里, 我们都善意地称John为世界上错误与故障最多地程序设计师.
几个月后, John的PC机开始差不多每隔一小时就会冻结大约10秒钟. 无论他对网络管理员说多么尖锐刻薄的话都无济于事. 最终, John熟悉了Windows NT的SoftICE(NuMega公司的内核调试器)的一个很早的版本. 没过多久, John就查出了原来是有个工程师使用了一个具有时间判断力的线程, 在随机间隔的时间内将总计达175M的Windows NT业务, 放置到John的机器上. 即使他竭尽全力, 也无法战胜这样的恶作剧. 那时NuMega公司的大部分工作就是派工程师去参加贸易展览. John的外向个性使他成为销售部瞄准的一个天然目标, 销售部急切地想让他也成为销售代表. 哈, 还有一些更无知的想法颇具讽刺意味, 是想让John作为“亚特兰大错误与故障控制中心的研究员”而闪闪发光. John丝毫不想参与这个闹剧, 于是他和另外一个工程师提出了一种完全不同的想法, 这种想法就是举世闻名的“高年级男孩和低年级男孩”幽默剧的起源. John是“高年级男孩”, 这是他在NuMega公司得到的许多外号之一. 我将“Sporkie男孩”的起源留给读者来想像吧.
作为贸易展览的一个脚注, 正是从这里John明白了那些表面看起来充满沙子的重力救援气球里面真正装了些什么. 在这个特殊的例子里, 填充物看起来好像是些胡桃壳的小碎片. 当John将气球举过头顶时, 他设法刺破了气球, 结果弄得我们浑身都是胡桃壳, 不仅如此, 整个旅馆房间里也到处都是. 这些胡桃壳甚至还掉到了地毯和床罩中.
最初John在NuMega公司的一半时间都用来完成BoundsChecker各个方面的工作. 他的工作包括开发调试器循环体. 编写API确认占位程序. 修正符号表的错误与故障. 埋头于COM界面的引用计数算法, 以及将流程序跟踪信息的代码扩展到磁盘上. 一天之内要干这么多事情!对调试工具进行调试, 尤其是那些对操作系统没有严格限制的工具, 这些工作提供了大量磨练调试技能的机会.
我和John在遇到困难的问题时经常不能使用调试器来解决. 我还记得有个很特别的问题, 在发布一个很重要的测试版的前夜, 是我在Microsoft Visual SourceSafe上运行BoundsChecker时遇到的. 开始看起来运行状况很好, 但是几分钟以后, 另一位工程师发现Visual SourceSafe中的数据文件看起来像“@@l?70”. 常规的备份当时还不属于工程进程的一部分, 所以最后几乎完全毁坏了保存着源代码的唯一拷贝的版本控制数据库. 毋庸多言, 当时整个公司都差点要剥了我的皮, 但是我和John费尽心机, 最终发现Visual SourceSafe使用了一些传统的MS-DOS代码, 破坏了我们的文件句柄.
你可能会不明白, “讲这些故事干吗”?我的意思是John并不是学院式或抽像的理论空谈家. John是个十足的实干家. 我和John经常悲叹, 有那么多编程方面的书都是由极其缺乏或根本没有商业开发经验, 并且只编写过代码不超过100行的程序的人编写的. John是几个获奖产品的主要开发者. 在编写软件时, 他是在传达自己从得之不易的经验中所领悟的信息.
当John决定要做什么事时, 他就会全力以赴. 他会对某个问题紧抓不放, 直到完全将它解决. 在John的字典里, 没有“我无法解决”这样的话. 在John开始他的编程生涯之前, 曾主动提出帮我做我所负责的《Microsoft Systems Journal))(MSJ)杂志中一个专栏的几个函数. 我认为他在代码上所花的时间甚至比我还要多. 由于我们都在为MSJ的栏目写东西, 所以就经常交流各自在为栏目内容编写相应的代码时所遇到的令人头痛的问题. 很多次听他描述他为探索一个问题而穷追不舍的情形都令我目瞪口呆.
要写一本书不仅需要了解与主题相关的知识, 还需要了解更多别的知识. 并且还要有一种希望与读者有效地交流这些知识的愿望. 在John的第一篇文章里, 我就注意到了他那简单明了的语言表达方式. 我同时还认为阅读John Robbins所写的任何东西都是一种享受. 当我知道文章的作者真正了解文章所谈的主题, 也很希望能与读者进行交流, 并且不会使用毫无必要的晦涩复杂的表达时, 阅读这样的文章无疑是令人愉快的.
John的写作热情远在他的这些文章. 这一栏目以及这本书之外. John在他所编写的所有代码上都留下了自己的印记, 下面这段文字就摘自我在工作中收到的典型的有关John电子邮件:
John在程序当中东一块西一块的废话讨论到底有什么用?每一条该死的函数标题前都有一整章的关于社会学的暗示, 即如何使用例程以及如果不正确地使用他们会如何地使行星错位的暗示——上个月的MSJ几乎都是一本蠢书, 整本书都充斥这种讨论的废话一——在此书中, 他几乎整本地谈论他的Form Load处理程序. 这个人是一名作家还是工程师?天哪……
正如你将在本书中看到的那样, 问题的答案就是两者都是!
调试问题被一层神秘的面纱覆盖着, 在许多方面都只是一个口头惯例而已. 我们急需好的书籍. 仅有的几本书多数都只是集中讨论无错误与故障的情况, 而没有深入到Windows调试过程中可能会遭遇到的困境. 书籍发行人不时地催促我写一本调试方面的书, 但我都婉言拒绝了. 这并不是因为我对调试方面的知识了解得不够, 而是因为我觉得很难将自己在调试方面的直接经验付诸于文字. 而John却不存在这种问题.
从我的观点来看, 做到有效的调试其实非常简单. 我通常将之归于两条原则:
. 了解可能会出现的问题
. 懂得如何使用工具来查明究竟出了什么问题
如果你能够做到这两点, 调试就很简单了. 问题就在于, 上述两点都不是那么容易就能做到的. 当我说“了解可能会出现的问题”时, 这并不是说你所了解的知识只停留在源代码的水平上. 你不仅要了解大概的内容, 还要熟知所有的细枝末节. 编译器会将代码转换成什么?在API的调用过程中将会发生什么情况?如果不能回答这些问题, 你就是在盲目地进行调试工作.
我也并不是说要进行调试就要通晓所有的指令, 而是说, 在需求不断提高时, 你应该能够将问题分解成越来越小的部分, 就像John在本书第1章中所介绍的那样. 你甚至还要深究不是由你控制的那些部分, 找出程序崩溃的原因. 如果挖掘的深度足够, 你最终肯定会找到原因所在. 跟踪错误与故障的许多工作都需要你了解语言. 操作系统和CPU的错综复杂之处, 这也是John在第1章中谈到的另一个话题. 书中讨论你至少应该了解的X86汇编语言的有关部分的那一章(第6章), 对读者们来说也是极有价值的.
同样地, 知道如何有效地使用工具也很关键. 调试工具已经变得相当成熟了, 很多开发人员还从未有机会(或者有机会也不用)去熟悉这些工具的所有性能. 我想起一位朋友, 他手下有一位年轻的图像处理程序设计师为他工作. 当这位神童的代码发生了程序崩溃时, 他竟然不知道调试器能够显示出调用难栈.
很多开发人员只要了解了足够的调试技能, 能够脱离当前的困境, 他们就满足了. 很多用户常常会忽视调试工具的一些重要功能, 因为需要使用这些功能的情形并没有在恰当的上下文中出现. 如果一项功能一项功能地通读文件, 到后来就会感到厌烦, 最终你只能够了解到一些最基本的功能. John所写的“使用Microsoft Visual C++调试器进行的调试”和“使用Microsoft Visual Basic调试器进行强有力的调试”这两章(第5章和第7章), 将会使你彻底摆脱“一步. 一步. 一步, 然后检查. 然后又一步. 一步. 一步地调试”的沉闷状况.
我说调试是一项口头惯例, 是指多数人只有在他们听说别人使用某种技能或技术解决了某个问题时, 才会学习这种新的技能或技术. 当你在精通调试工具的人旁边观看他工作时, 只需观看半个小时, 就能学到10倍于自己看一天技术资料所学到的东西. John正是这样一位精通调试的人. 他了解他所使用的工具有哪些优点和缺点. 同时, 他还预先投入时间来编写对症的诊断代码, 而不是在出现问题之后才来补偿损失. 将调试当作一项技术而不是什么繁琐之事来不断地提高自己的调试技术, 这样, 你也可以作到这一点. 本书是一座丰富的金矿, 即John关于调试的金科玉律, 会帮助你更快地掌握调试技术.
Matt Pietrek
Hollis, New Hampshire
1999年12月