现代计算机是从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)是计算机系统的管理者和软件核心,也是应用软件运行的基础。这一部分以Windows操作系统为例描述了操作系统的调试支持,包括如何支持应用程序调试(第9章和第10章),如何支持调试系统内核和驱动程序(第18章)以及支持可调试性的错误提示机制(第13章)、错误报告机制——WER(第14章)、错误记录机制(第15章)、事件追踪机制——ETW(第16章)、硬件错误处理机制——WHEA(第17章)。第19章介绍了提高测试和调试效率的程序验证(Verifier)机制和有关工具。第11章介绍了中断和异常的分发与管理。第12章介绍了未处理异常和JIT调试。作为以上内容的铺垫,第8章介绍了Windows操作系统的基本知识,包括架构、关键模块和系统进程等。
第4篇:编译器的调试支持(第20~25章)
编译器是软件生产的主要工具,它帮助我们将程序语言翻译为可以被CPU所理解的机器码。支持软件调试始终是编译器的一个设计目标。在编译过程中,编译器会帮我们检查程序中的静态错误(编译期检查)(第20章)。为了帮助发现只有在运行时才体现出来的问题,编译器可以在程序中插入代码并报告运行时的可疑情况(运行期检查)(第21章)。很多软件缺欠是与局部变量、缓冲区和内存使用有关的,对此,编译器设计了很多种检查和保护栈(第22章)及堆(第23章)的机制。编译器对软件调试的另一个重大支持就是调试符号。调试符号是软件调试时的灯塔,是观察数据结构和进行源代码级调试所必需的。第25章详细介绍了调试符号的产生过程、种类、文件格式、和用法。第24章介绍了异常处理是如何编译的,包括C++的异常处理和结构化异常处理(SEH)。在介绍以上内容时,本章还讨论了函数调用规范,栈的布局,以及堆的内部结构等与软件调试密切相关的基础内容。
第5篇:可调试性(第26~27章)
提高软件调试效率是一项系统的工程,除了CPU、操作系统和编译器所提供的调试支持外,被调试软件本身的可调试性也是至关重要的。这一篇,我们先介绍了提高软件可调试性的意义、基本原则、实例和需要注意的问题(第26章)。然后讨论了如何在软件开发实践中实现可调试性(第27章),包括软件团队中各个角色应该承担的职责,实现可追溯性、可观察性和自动报告的方法。
第6篇:调试器(第28~30章)
调试器(Debugger)是软件调试的核心工具。借助调试器,我们可以将软件冻结(中断)在我们指定的位置,然后观察它的内部状态、了解它的运行轨迹和即将执行的操作。根据需要,我们可以分析它的任一条指令,查看它使用的任一个内存单元。分析后,我们可以让它从原来的地方恢复执行,也可以让它“飞”到一个新的地方继续执行,或者干脆将其终止。这一部分分为3章。第28章介绍了调试器的历史、主要功能、分类方法、实现模型、架构和两个公开标准——HPD(High Performance Debugger)和JPDA(Java Platform Debugger Architecture)。第29章分析了WinDBG调试器的架构和主要功能的实现原理。第30章分为18个主题,系统介绍了WinDBG调试器的使用方法。
除了以上6篇,附录A列出了与本书配套的工具和源程序,附录B列出了WinDBG的标准命令。
本书的三条线索
本书的内容是使用以下三条线索来组织的。
第一条线索是软件调试的“生态”系统(ecosystem)。我们介绍了这个系统中的各个成员和它的职责,以及成员间的协作方式。CPU(第2篇)为关键的调试功能提供了硬件一级的支持;操作系统(第3篇)把CPU的支持进行必要的封装,并构建一整套软件调试所需的基础设施,然后以API的形式提供给调试器和应用软件;编译器(第4篇)负责准备便于调试的版本,以及包含调试信息的调试符号文件供调试器使用;被调试软件(第5篇)自身应该努力提高自己的可调试性;调试器(第6篇)负责把所有“人”的努力以简单方便的形式呈现给用户(调试者),让他们利用强大的调试功能随心所欲地探索软件世界。本书6个部分的标题就是按照这个线索而设计的,所以这条线索是“明线”。
第二条线索是异常(Exception)。异常是计算机系统中的一个重要概念,出现在CPU、操作系统、编程语言、编译器、调试器等多个领域,本书逐一对其做了解析。第3章从CPU一级介绍了异常的作用、分类,以及与中断的关系;第10章介绍了Windows操作系统管理异常的方法,包括分发异常的详细规则, 以及Windows的结构化异常处理(SEH)机制;第11章介绍了操作系统处置未处理异常的策略和过程;第24章从编程语言和编译器的角度探讨了异常,包括C++语言的异常处理,以及编译器编译异常处理代码的方法。第30章(30.9节)介绍了调试器“眼”中的异常、调试器处理异常的方法和有关调试命令。
第三条线索是调试器。调试器是解决软件问题最有力的工具,它是逐步发展到今天这个样子的。第1章我们介绍了单纯依赖硬件的调试方法。第4章分析了DOS下的Debug调试器的实现方法。第7章介绍硬件仿真和基于JTAG标准的硬件调试器。第9章介绍了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.aspx。
尽管作者和编辑已经尽了最大努力,但是本书中仍然可能存在这样那样的错误,读者可以通过上面的链接反馈给我们。
关于封面
人们遇到百思不得其解或者难以解释清楚的问题时可能不由自主说:“见鬼了”。在软件开发和调试中也时常有这样的情况。钟馗是传说中的捉鬼能手,因此我们选取他作为本书的封面人物,希望这本书能够帮助读者轻松化解“见鬼了”这样的复杂问题。
免责声明
本书的内容完全是作者本人的观点,不代表任何公司和单位。您可以自由地使用本书的示例代码和附属工具,但是作者不对因为使用本书内容和附带资料而导致的任何直接和间接后果承担任何责任。
感谢
首先感谢Jack B. Dennis教授,他向我讲述了大型机时代的编程环境和调试方法以及他和Thomas G. Stockman为TX-0计算机编写FLIT调试器的经过,并专门为本书撰写了短文。FLIT调试器是作者追溯到的最早的调试器程序。
感谢Windows领域的著名专家David Solomon先生,他回答了作者的很多提问,并为本书题写了序言。
感谢《Showstopper》一书的作者Greg Pascal Zachary先生,他允许我引用他书中的内容和该书的照片。
感谢CPU和计算机硬件方面的权威Tom Shanley先生,他在计算机领域的著作有十几本,他关于IA-32架构方面的培训享誉全球。感谢他允许我在本书中使用他绘制的关于CPU寄存器的大幅插图(因篇幅所限最终没有使用)。
探索Windows调试子系统让我感受到了软件之美,创造这种美的一个主要英雄便是Mark Lucovsky先生,感谢他在邮件中给予我的鼓励。
感谢DOS之父Tim Paterson先生,他向我介绍了他编写8086 Monitor的经过,并允许我使用这个调试器程序的源代码。
感谢Syser调试器的作者吴岩峰先生,我们多次讨论了如何在单机上实现内核调试的技术细节,他始终关心着本书的进度。
感谢我的老板和同事们,他们是:Kenny, Michael, Feng, Adam, Jim, Neal, Harold, Cui Yi, Keping, Eric, Yu, Wei, Min, Fred, Rick, Shirley, Vivian, Luke, Caleb, Christina, Starry(请原谅,我无法列出所有名字)!
感谢我的好朋友刘伟力,我们一起加班解决了一个大Bug后,他感慨地说“断点真神奇”,这句话让我产生了写作本书的念头。
感谢曾华军(我们一起翻译了《机器学习》)和李妍帮助我翻译了Dennis和David所写的序言。
感谢以下朋友阅读了本书的草稿,提出了很多宝贵的意见:王毅鹏、王宇、施佳、夏桅、周祥、李晓宁、侯伟、吴巍。
感谢本书的编辑:周筠、陈元玉,感谢他们给我的一贯支持,以及编辑本书所花费的大量时间!感谢美术编辑胡文佳,她的精心设计让本书的封面如此美丽!
感谢我的家人,在写作本书的漫长而且看似没有尽头的日子里,她们承担了繁重的家务,让我有时间完成本书。
最后,感谢您阅读本书,并希望您能从中受益!
|