现代计算机是从20世纪40年代开始出现的。当时的计算机比今天的要庞大很多,很多部件也不一样,但是有一点是完全相同的,那就是靠执行指令而工作。.
一台计算机认识的所有指令被称为它的指令集(Instruction Set)。按照一定格式编写的指令序列被称为程序(Program)。在同一台计算机上,执行不同的程序,便可以完成不同的任务,因此,现代计算机在诞生之初常被冠以“通用”字样,以突出其通用性。在获得通用性带来好处的同时,人们也很快意识到了两个严峻的问题:首先是编写程序需要很多时间;其次是程序在执行时很可能出现意料之外的怪异行为。
程序对计算机的重要性和编写程序的复杂性让一些人看到了商机。大约在20世纪50年代中期,专门编写程序的公司出现了。几年后,模仿硬件(Hardware)一词,人们开始使用软件(Software)这个词来称呼计算机程序和它的文档,并把将用户需求转化为软件产品的整个过程称为软件开发(Software Development),将大规模生产软件产品的社会活动称为软件工程(Software Engineering)。
如今,几十年过去了,我们看到的是一个繁荣而庞大的软件产业。但是前面描述的两个问题依然存在:一是编写程序仍然需要很多时间;二是编写出的程序在运行时仍然会出现意料外的行为。而且后一个问题的表现形式越来越多,可能突然报告一个错误,可能给出一个看似正确却并非需要的结果,可能自作聪明地自动执行一大堆无法取消的操作,可能忽略用户的命令,可能长时间没有反应,可能直接崩溃或者永远僵死在那里……而且总是可能有无法预料的其他意外情况出现。这些“可能”大多是因为隐藏在软件中的设计失误而导致的,即所谓的软件臭虫(bug),或者称软件缺陷(defect)。
计算机是在软件指令的控制下工作的,让存在缺陷的软件控制硬件是件危险的事,可能导致惊人的损失和灾难。2003年8月14日发生的北美大停电(Northeast Blackout of 2003)使50万人受到影响,直接经济损失60亿美元,其主要原因是软件缺陷导致报警系统没有报警。1999年9月23日,美国的火星气象探测船因为没有进入预定轨道而受到大气压力和摩擦被摧毁,其原因是不同模块使用的计算单位不同,使计算出的轨道数据出现严重错误。1990年1月15日,AT&T公司的100多台交换机崩溃并反复重新启动,导致6万用户在9个小时中无法使用长途电话,其原因是新使用的软件在接收到某一种消息后会导致系统崩溃,并把这种症状传染给与它相邻的系统。1962年7月22日,水手一号太空船发射293秒后因为偏离轨道而被销毁,其原因也与软件错误有直接关系。类似的故事还有很多,尽管我们不希望它们发生。
一方面,软件缺陷难以避免;另一方面其危害又很大,这使得消除软件缺陷成为软件工程中的一项重要任务。消除软件缺陷的前提是要找到导致缺陷的根本原因。我们把探索软件缺陷的根源并寻求其解决方案的过程称为软件调试(Software Debugging)。
本书的写作目的
在复杂的计算机系统中寻找软件缺陷的根源不是一个简单的任务,需要对软件和计算机系统有深刻的理解,选用科学的方法,并使用强有力的工具。这些正是作者写作本书的初衷。具体来说,写作本书有三个主要目的。
第一,论述软件调试的一般原理,包括CPU、操作系统和编译器是如何支持软件调试的,内核态调试和用户态调试的工作模型,以及调试器的工作原理。软件调试是计算机系统中多个部件之间的一个复杂交互过程,要理解这个过程,必须要了解每个部件在其中的角色和职责,以及它们的协作方式。学习软件调试原理不仅对提高软件工程师的调试技能至关重要,而且有利于提高它们对计算机系统的理解,将计算机原理、编译原理、操作系统等多个学科的知识融会贯通在一起。
第二,探讨可调试性(Debuggability)的内涵和实现软件可调试性的原则和方法。所谓软件的可调试性就是在软件内部加入支持调试的代码,使其具有自动记录、报告和诊断的能力,从而更容易被调试。软件自身的可调试性对于提高调试效率、增强软件的可维护性,以及保证软件的如期交付都有着重要意义。
第三,交流软件调试的方法和技巧。尽管论述一般原理是本书的重点,本书仍穿插了许多实践性很强的内容。包括调试用户态程序和系统内核模块的基本方法,如何诊断系统崩溃(BSOD)和应用程序崩溃,如何调试缓冲区溢出等与栈有关的问题,如何调试内存泄漏等与堆有关的问题。特别是,本书非常全面地介绍了WinDBG调试器的使用方法,给出了大量使用这个调试器的实例。
总之,笔者希望通过本书让读者懂得软件调试的原理,意识到软件可调试性的重要性,学会使用基本的软件调试方法和调试工具,并能应用这些方法和工具解决问题和掌握更多软硬件知识。
本书的读者
首先,本书是写给所有程序员的。程序员是软件开发的核心力量。他们花大量的时间来调试他们所编写的代码,有时为此工作到深夜。笔者希望程序员朋友们读过本书后能提高调试能力,并自觉地在代码中加入调试支持,使调试效率大大提高,减少因为调试程序而加班的次数。本书中关于CPU、中断、异常和操作系统的介绍,是很多程序员需要补充的知识,因为对硬件和系统底层的深刻理解有利于写出更好的应用程序,对于程序员的职业发展也是非常有帮助的。之所以说写给“所有”程序员是因为本书主要讨论的是一般原理和方法,没有限定某种编程语言和某个编程环境,也没有局限于某个特定的编程领域。
第二,本书是写给从事测试、验证、系统集成、客户支持、产品销售等工作的软件工程师或IT工程师的。他们的职责不是编写代码,因此软件缺陷与他们不直接相关,但是他们也经常受累于软件缺陷。他们不负责解决问题,但他们需要知道找谁来解决。因此,他们需要把错误定位到某个模块,或者至少定位到某个软件。本书介绍的工具和方法对于实现这个目标是非常有益的。另外,他们也可以从关于软件可调试性的内容中得到启发。本书关于CPU、操作系统和编译器的内容对于提高他们的综合能力,巩固软硬件知识也是有益的。
第三,本书适合从事反病毒、网络安全、版权保护等工作的技术人员阅读。他们经常面对各种怪异的代码,需要在没有代码和文档的情况下做跟踪和分析。这是计算机领域中最富挑战性的工作之一。关于调试方法和WinDBG的内容有利于提高他们的效率。很多恶意软件故意加入了阻止调试和跟踪的机制,本书介绍的软件调试原理有助于理解这些机制。
第四,本书也适合计算机、软件、自动控制、电子学等专业的研究生或高年级本科生来研读。他们已经学习了程序设计、操作系统、计算机原理等课程,阅读本书可以帮助他们把这些知识联系起来,并深入到一个新的层次。学会使用调试器来跟踪和分析软件,可以让他们在指令一级领悟计算机软硬件的工作方式,深入核心,掌握本质,把学到的书本知识与计算机系统的实际情况结合起来;同时,可以提高他们的自学能力,使他们养成乐于专研和探索的良好习惯。软件调试是从事计算机软硬件开发等工作的一项基本功,在学校里就掌握了这门技术,对于以后快速适应工作岗位是大有好处的。
第五,本书是写给勇于挑战软件问题的硬件工程师和计算机用户的。他们是软件缺陷的受害者。除了要忍受软件缺陷带来的不便之外,有时软件设计者还可能将责任推卸给他们,推诿是硬件问题或使用不当。使用本书的工具和方法,他们可以找到充足的证据来证明这是软件的问题。本书的大多数内容不需要很深厚的软件背景,有基本的计算机知识就可以读懂。
最后,或许还有不属于上面5种类型的读者也可能会阅读本书。比如,软件公司或软件团队的管理者、软件方面的咨询师和培训师、大学和研究机构的研究人员、非计算机专业的学生、自由职业者、编程爱好者、黑客等等。
前面说过,本书的大多数内容不需要深厚的软件开发背景,但如果读者具备以下基础,将更容易读懂和领会本书的内容:
.曾经亲自参与编写程序,包括输入代码、编译,然后执行。
使用过某一种类型的调试器,用过断点、跟踪、观察变量等基本调试功能。
参加过某个软件开发项目,对软件工程有基本的了解。认同软件的复杂性,即开发一个软件产品与写一个HelloWorld程序根本不是一回事,
尽管本书给出了一些汇编代码和C/C++代码,但是其目的只是在代码层次直截了当地阐述问题。本书的目标不是讨论编程语言和编程技巧,也不要求读者已经具备丰富的编程经验。
本书的主要内容
本书共有30章,分为以下6篇。
第1篇:绪论(第1章)
作为全书的开篇,这一部分介绍了软件调试的概念、基本过程、分类和简要历史,并综述了本书后面将详细介绍的主要调试技术。
第2篇:CPU的调试支持(第2~7章)
CPU是计算机系统的硬件核心。这一部分以IA-32 CPU为例,系统描述了CPU的调试支持,包括如何支持软件断点、硬件断点和单步调试(第4章),如何支持硬件调试器(第7章),记录分支、中断、异常和支持性能分析的方法(第5章),以及支持硬件可调试性的错误检查和报告机制——MCA(机器检查架构)(第6章)。为了帮助读者理解这些内容,以及本书后面的章节,第2章介绍了关于CPU的一些基础知识,包括指令集、寄存器和保护模式,第3章深入介绍了与软件调试关系密切的中断和异常机制。
第3篇:操作系统的调试支持(第8~19章)
操作系统(OS)是计算机系统的管理者和软件核心,也是应用软件运行的基础。第8章介绍了Windows操作系统的基本知识,包括架构、关键模块和系统进程等。然后以Windows操作系统为例,描述了操作系统的调试支持,包括如何支持应用程序调试(第9章和第10章),如何支持调试系统内核和驱动程序(第18章),以及支持可调试性的错误提示机制(第13章)、错误报告机制——WER(第14章)、错误记录机制(第15章)、事件追踪机制——ETW(第16章)、硬件错误处理机制——WHEA(第17章)。第19章介绍了提高测试和调试效率的程序验证(Verifier)机制和有关工具。第11章介绍了中断和异常的分发与管理。第12章介绍了未处理异常和JIT调试。
第4篇:编译器的调试支持(第20~25章)..
编译器是软件生产的主要工具,它帮助我们将程序语言翻译为可以被CPU所理解的机器码。支持软件调试始终是编译器的一个设计目标。在编译过程中,编译器会帮我们检查程序中的静态错误(编译期检查)(第20章)。为了帮助发现只有在运行时才体现出来的问题,编译器可以在程序中插入代码并报告运行时的可疑情况(运行期检查)(第21章)。很多软件缺陷是与局部变量、缓冲区和内存使用有关的,对此,编译器设计了很多种检查和保护栈(第22章)及堆(第23章)的机制。编译器对软件调试的另一个重大支持就是调试符号。调试符号是软件调试时的灯塔,是观察数据结构和进行源代码级调试所必需的。第25章详细介绍了调试符号的产生过程、种类、文件格式和用法。第24章介绍了异常处理代码是如何编译的。在介绍以上内容时,本篇还覆盖了有关函数调用规范,栈的布局,以及堆的内部结构等与软件调试密切相关的基础内容。
第5篇:可调试性(第26~27章)
提高软件调试效率是一项系统工程,除了CPU、操作系统和编译器所提供的调试支持外,被调试软件本身的可调试性也是至关重要的。这一篇,我们先介绍了提高软件可调试性的意义、基本原则、实例和需要注意的问题(第26章)。然后讨论了如何在软件开发实践中实现可调试性(第27章),包括软件团队中各个角色应该承担的职责,实现可追溯性、可观察性和自动报告的方法。
第6篇:调试器(第28~30章)
调试器(Debugger)是软件调试的核心工具。借助调试器,我们可以将软件冻结(中断)在我们指定的位置,然后观察它的内部状态、了解它的运行轨迹和即将执行的操作。根据需要,我们可以分析它的任一条指令,查看它使用的任一个内存单元。分析后,我们可以让它从原来的地方恢复执行,也可以让它“飞”到一个新的地方继续执行,或者干脆将其终止。这一部分分为3章。第28章介绍了调试器的历史、主要功能、分类方法、实现模型、架构和一个公开标准——HPD(High Performance Debugger)。第29章分析了WinDBG调试器的架构和主要功能的实现原理。第30章分为18个主题,系统介绍了WinDBG调试器的使用方法。
除了以上6篇,附录A列出了与本书配套的工具和源程序,附录B列出了WinDBG的标准命令。
本书的三条线索
本书的内容是根据以下三条线索来组织的。
第一条线索是软件调试的“生态”系统(ecosystem)。我们介绍了这个系统中的所有“成员”,描述了每个成员的职责以及成员间的协作方式。CPU(第2篇)为关键的调试功能提供了硬件级的支持;操作系统(第3篇)把CPU的支持进行必要的封装,并构建一整套软件调试所需的基础设施,然后以API的形式提供给调试器和应用软件;编译器(第4篇)负责产生更易于调试的调试版本,以及包含调试信息的调试符号文件;被调试软件(第5篇)则应该努力提高自身的可调试性;调试器(第6篇)负责把所有“成员”的努力化为成果,以简单方便的形式呈现给用户(调试者),让他们利用强大的调试功能随心所欲地探索软件世界。本书6篇的标题就是按照这个线索而设计的,所以这条线索是“明线”。
第二条线索是异常(Exception)。异常是计算机系统中的一个重要概念,出现在CPU、操作系统、编程语言、编译器、调试器等多个领域,本书逐一对其做了解析。第3章从CPU一级介绍了异常的作用、分类,以及与中断的关系;第11章介绍了Windows操作系统管理异常的方法,包括分发异常的详细规则,以及Windows的结构化异常处理(SEH)机制;第12章介绍了操作系统处置未处理异常的策略和过程;第24章从编程语言和编译器的角度探讨了异常,介绍了编译器编译异常处理代码的方法。第30章(30.9节)介绍了调试器“眼”中的异常、调试器处理异常的方法和有关调试命令。
第三条线索是调试器。调试器是解决软件问题最有力的工具。第1章介绍了单纯依赖硬件的调试方法。第4章分析了DOS下的Debug调试器的实现方法。第7章介绍硬件仿真和基于JTAG标准的硬件调试器。第9章和第10章介绍了Windows操作系统下用户态调试器的结构和工作原理,演示了如何使用Windows的调试API来实现一个简单的调试器。第18章介绍了Windows内核调试器的工作原理和实现方法。第28~30章对调试器做了归纳和更全面的介绍。另外,全书很多地方都使用了调试器输出的结果,穿插了使用调试器解决软件问题的方法。
本书的阅读方法
本书的厚度决定了不适合一口气将它看完。以下是笔者给您的建议。
第一,下载并安装WinDBG调试器。如果您还不了解它的基本用法,那么请先浏览第30章,学会它的基本用法,能读懂栈回溯结果。有了这个工具后,您就可以跟着做本书所描述的试验,自己在系统中探索书中提到的内容。
第二,选择前面提到的三条线索中的一条来阅读。如果您有充裕的时间,那么可以按第一条线索来阅读。如果您想深入了解异常,那么可以按第二条线索来阅读。如果您有难题等待解决,希望快速了解基本的调试方法,那么您可以选择第三条线索,从第30章开始阅读。
第三,先阅读每一篇开始处的简介,了解各篇的概况,浏览主要章节,建立一个初步的印象。当需要时,再仔细查阅感兴趣的细节。
以上意见中,第一条是希望您一定遵循的,其他谨供参考。
本书的写作方法
这是一本来自实践的书,它的大多数内容是依靠软件调试技术探索得到的。在笔者使用的系统中,在一个名为Toolbox的文件夹下保存了100多个不同功能的工具软件。当然,使用最多的还是调试器。书中给出的大多数栈回溯结果是使用WinDBG调试器产生的。
写作本书的一个基本原则是从有代表性的实例出发,然后从这个实例推广到其他情况和一般规律。例如,在CPU方面笔者选择的是IA-32 CPU;在操作系统方面选择NT系列的Windows操作系统;在编译器方面是Visual Studio系列;在调试器方面选择的是Visual Studio调试器和WinDBG。
示例、工具和代码
本书的示例、工具和代码可通过以下链接免费下载:http://advdbg.org/books/swdbg/。
尽管笔者和编辑已经尽了最大努力,但是本书中仍然可能存在这样那样的错误,读者可以通过上面的链接反馈给我们。
关于封面
人们遇到百思不得其解或难以解释清楚的问题时可能不由自主地说:“见鬼了”。在软件开发和调试中也时常有这样的情况。钟馗是传说中的捉鬼能手,因此我们选取他作为本书的封面人物,希望本书能够帮助读者轻松化解“见鬼了”这样的复杂问题。
免责声明
本书的内容完全是作者本人的观点,不代表任何公司和单位。您可以自由地使用本书的示例代码和附属工具,但是作者不对因为使用本书内容和附带资料而导致的任何直接和间接后果承担任何责任。
感谢
首先感谢Jack B. Dennis教授,他向我讲述了大型机时代的编程环境和调试方法,以及他和Thomas G. Stockman为TX-0计算机编写FLIT调试器的经过,并专门为本书撰写了短文。FLIT调试器是作者追溯到的最早的调试器程序。
感谢Windows领域的著名专家David Solomon先生,他回答了笔者的很多提问,并为本书题写了序言。
感谢Showstopper一书的作者G. Pascal Zachary先生,他允许我引用他书中的内容和该书的照片。
感谢CPU和计算机硬件方面的权威Tom Shanley先生,他在计算机领域的著作有十几本。感谢他回答了我的很多提问并允许我在本书中使用他绘制的关于CPU寄存器的大幅插图(因篇幅所限最终没有使用)。
探索Windows调试子系统让我感受到了软件之美,创造这种美的一个主要人物是Mark Lucovsky先生,感谢他在邮件中给予我的鼓励。
感谢DOS之父Tim Paterson先生,他向我介绍了他编写8086 Monitor的经过。DOS系统中的Debug调试器来源于8086 Monitor。
感谢Syser调试器的作者吴岩峰先生,我们多次讨论了如何在单机上实现内核调试的技术细节,他始终关心着本书的进度。
感谢我的老板和同事多年来给予我的帮助和支持,他们是:Kenny, Michael, Feng, Adam, Jim, Neal, Harold, Calvin, Cui Yi, Keping, Eric, Yu, Wei, Min, Fred, Rick, Shirley, Vivian, Luke, Caleb, Christina, Starry(请原谅,我无法列出所有名字)!
感谢我的好朋友刘伟力,我们一起加班解决了一个大Bug后,他感慨地说:“断点真神奇”,这句话让我产生了写作本书的念头。
感谢曾华军(我们一起翻译了《机器学习》)和李妍,他们帮助我翻译了Dennis和David所写的序言。
感谢以下朋友阅读了本书的草稿,提出了很多宝贵的意见:王毅鹏、王宇、施佳、夏桅、周祥、李晓宁、侯伟、吴巍。
感谢本书的编辑周筠和陈元玉,感谢他们给我的一贯支持,以及编辑本书所花费的大量时间!感谢责任美编胡文佳,她的精心设计让本书的封面如此美丽!
感谢我的家人,在写作本书漫长而且看似没有尽头的日子里,她们承担了繁重的家务,让我有时间完成本书。
最后,感谢您在茫茫书海中选择了本书,并衷心祝愿您能从中受益!...
张银奎(Raymond Zhang)
2008年4月于上海