围绕如何实现高效调试这一主题,本书深入系统地介绍了以调试器为核心的各种软件调试技术。本书共30章,分为6篇。第1篇介绍了软件调试的概况和简要历史。第2篇以英特尔架构(IA)的CPU为例,介绍了计算机系统的硬件核心所提供的调试支持,包括异常、断点指令、单步执行标志、分支监视、JTAG和MCE等。第3篇以Windows操作系统为例,介绍了计算机系统的软件核心中的调试设施,包括内核调试引擎、用户态调试子系统、异常处理、验证器、错误报告、事件追踪、故障转储、硬件错误处理等。第4篇以Visual C/C++编译器为例,介绍了生产软件的主要工具的调试支持,重点讨论了编译期检查、运行期检查及调试符号。第5篇讨论了软件的可调试性,探讨了如何在软件架构设计和软件开发过程中加入调试支持,使软件更容易被调试。在前5篇内容的基础上,第6篇首先介绍了调试器的发展历史、典型功能和实现方法,然后全面介绍了WinDBG调试器,包括它的模块结构、工作模型、使用方法和主要调试功能的实现细节。
本书是对软件调试技术在过去50年中所取得成就的全面展示,也是对作者本人在软件设计和系统开发第一线奋战10多年的经验总结。本书理论与实践紧密结合,选取了大量具有代表性和普遍意义的技术细节进行讨论,是学习软件调试技术的宝贵资料,适合每一位希望深刻理解软件和自由驾驭软件的人阅读,特别是从事软件开发、测试、支持的技术人员和有关的研究人员。
第1篇 绪论 1
第1章 软件调试基础 3
1.1 简介 3
1.2 基本特征 6
1.3 简要历史 8
1.4 分类 12
1.5 调试技术概览 15
1.6 错误与缺欠 20
1.7 与软件工程的关系 24
1.8 本章总结 26
第2篇 CPU的调试支持 27
第2章 CPU基础 29
2.1 指令和指令集 29
2.2 IA-32处理器 32
2.3 CPU的操作模式 38
2.4 寄存器 40
2.5 理解保护模式 46
2.6 段机制 50
2.7 分页机制(Paging) 55
2.8 系统概貌 62
2.9 本章总结 64
第3章 中断和异常 65
3.1 概念和差异 65
3.2 异常的分类 67
3.3 异常例析 69
3.4 中断/异常优先级 72
3.5 中断/异常处理 73
3.6 本章总结 74
第4章 断点和单步执行 75
4.1 软件断点 75
4.2 硬件断点 83
4.3 陷阱标志 95
4.4 实模式调试器例析 100
4.5 本章总结 105
第5章 分支记录和性能监视 107
5.1 分支监视概览 107
5.2 使用寄存器的分支记录 108
5.3 使用内存的分支记录 113
5.4 DS示例:CpuWhere 117
5.5 性能监视 123
5.6 本章总结 132
第6章 机器检查架构(MCA) 133
6.1 奔腾处理器的机器检查机制 134
6.2 MCA 135
6.3 编写MCA软件 141
6.4 本章总结 145
第7章 JTAG调试 147
7.1 简介 147
7.2 JTAG原理 149
7.3 JTAG应用 154
7.4 IA-32处理器的JTAG支持 156
7.5 本章总结 161
第3篇 操作系统的调试支持 163
第8章 Windows概要 165
8.1 简介 165
8.2 进程和进程空间 167
8.3 内核模式和用户模式 176
8.4 架构和系统部件 184
8.5 本章总结 192
第9章 用户态调试模型 193
9.1 概览 193
9.2 采集调试消息 196
9.3 发送调试消息 200
9.4 调试子系统服务器(XP之后) 203
9.5 调试子系统服务器(XP之前) 210
9.6 比较两种模型 219
9.7 NTDLL中的调试支持例程 221
9.8 调试API 224
9.9 本章总结 226
第10章 用户态调试过程 227
10.1 调试器进程 227
10.2 被调试进程 231
10.3 从调试器中启动被调试程序 234
10.4 附加到已经启动的进程 240
10.5 处理调试事件 243
10.6 中断到调试器 251
10.7 输出调试字符串 259
10.8 终止调试会话 266
10.9 本章总结 271
第11章 中断和异常管理 273
11.1 中断描述符表 273
11.2 异常的描述和登记 280
11.3 异常分发过程 284
11.4 结构化异常处理(SEH) 290
11.5 向量化异常处理(VEH) 302
11.6 本章总结 308
第12章 未处理异常和JIT调试 309
12.1 简介 309
12.2 默认的异常处理器 311
12.3 未处理异常过滤函数 318
12.4 应用程序错误对话框 328
12.5 JIT调试和Dr. Watson 334
12.6 顶层异常过滤函数 340
12.7 Dr. Watson 343
12.8 DRWTSN32的日志文件 347
12.9 用户态转储文件 351
12.10 本章总结 357
第13章 硬错误和蓝屏 359
13.1 硬错误提示 359
13.2 蓝屏终止(BSOD) 366
13.3 系统转储文件 371
13.4 分析系统转储文件 374
13.5 辅助的错误提示方法 380
13.6 配置错误提示机制 384
13.7 防止滥用错误提示机制 389
13.8 本章总结 390
第14章 错误报告 391
14.1 WER 1.0 392
14.2 系统错误报告 395
14.3 WER服务器端 397
14.4 WER 2.0 399
14.5 CER 403
14.6 本章总结 404
第15章 日志 405
15.1 日志简介 405
15.2 ELF的架构 406
15.3 ELF的数据组织 409
15.4 察看和使用ELF日志 413
15.5 CLFS的组成和原理 414
15.6 CLFS的使用方法 416
15.7 本章总结 420
第16章 事件追踪 421
16.1 简介 421
16.2 ETW的架构 422
16.3 提供ETW消息 424
16.4 控制ETW会话 425
16.5 消耗ETW消息 427
16.6 格式描述 428
16.7 NT Kernel Logger 432
16.8 Global Logger Session 436
16.9 Crimson API 440
16.10 本章总结 443
第17章 WHEA 445
17.1 目标和架构 445
17.2 错误源 450
17.3 错误处理过程 452
17.4 错误持久化 457
17.5 注入错误 459
17.6 本章总结 459
第18章 内核调试引擎 461
18.1 概览 462
18.2 连接 465
18.3 启用 475
18.4 初始化 478
18.5 内核调试协议 483
18.6 与内核交互 492
18.7 建立和维持连接 502
18.8 本地内核调试 509
18.9 本章总结 511
第19章 Windows的验证机制 513
19.1 简介 514
19.2 驱动验证器的工作原理 515
19.3 使用驱动验证器 521
19.4 应用程序验证器的工作原理 526
19.5 使用应用程序验证器 533
19.6 本章总结 537
第4篇 编译器的调试支持 539
第20章 编译和编译期检查 541
20.1 程序的构建过程 541
20.2 编译 543
20.3 Visual C++编译器 544
20.4 编译错误和警告 549
20.5 编译期检查 551
20.6 标准标注语言 555
20.7 本章总结 558
第21章 运行库和运行期检查 559
21.1 C/C++运行库 559
21.2 链接运行库 562
21.3 运行库的初始化和清理 565
21.4 运行期检查 569
21.5 报告运行期检查错误 574
21.6 本章总结 580
第22章 栈和函数调用 581
22.1 简介 581
22.2 栈的创建过程 585
22.3 CALL和RET指令 590
22.4 局部变量和栈帧 595
22.5 帧指针省略(FPO) 604
22.6 栈指针检查 606
22.7 调用协定 609
22.8 栈空间的增长和溢出 616
22.9 栈下溢 623
22.10 缓冲区溢出 624
22.11 变量检查 628
22.12 基于Cookie的安全检查 636
22.13 本章总结 642
第23章 堆和堆检查 643
23.1 理解堆 644
23.2 堆的创建和销毁 646
23.3 分配和释放堆块 649
23.4 堆的内部结构 654
23.5 低碎片堆(LFH) 661
23.6 堆的调试支持 662
23.7 栈回溯数据库 666
23.8 堆溢出和检测 670
23.9 页堆 677
23.10 准页堆 683
23.11 CRT堆 688
23.12 CRT堆的调试堆块 692
23.13 CRT堆的调试功能 698
23.14 堆块转储 700
23.15 泄漏转储 704
23.16 本章总结 709
第24章 异常处理代码的编译 711
24.1 概览 711
24.2 FS:[0]链条 713
24.3 遍历FS:[0]链条 716
24.4 执行异常处理函数 721
24.5 __try{}__except()结构 724
24.6 安全问题 732
24.7 本章总结 737
第25章 调试符号 739
25.1 名称修饰 739
25.2 调试信息的存储格式 742
25.3 目标文件中的调试信息 745
25.4 PE文件中的调试信息 753
25.5 DBG文件 762
25.6 PDB文件 764
25.7 有关的编译和链接选项 771
25.8 PDB文件中的数据表 775
25.9 本章总结 780
第5篇 可调试性 781
第26章 可调试性概览 783
26.1 简介 783
26.2 Showstopper和未雨绸缪 784
26.3 基本原则 787
26.4 不可调试代码 792
26.5 可调试性例析 794
26.6 与安全、性能和商业秘密的关系 798
26.7 本章总结 799
第27章 可调试性的实现 801
27.1 角色和职责 801
27.2 可调试架构 804
27.3 通过栈回溯实现可追溯性 808
27.4 数据的可追溯性 815
27.5 可观察性的实现 821
27.6 自检和自动报告 830
27.7 本章总结 832
第6篇 调试器 833
第28章 调试器概览 835
28.1 TX-0计算机和FLIT调试器 835
28.2 小型机和DDT调试器 837
28.3 个人计算机和它的调试器 841
28.4 调试器的功能 845
28.5 分类标准 852
28.6 实现模型 853
28.7 经典架构 859
28.8 HPD标准 862
28.9 本章总结 866
第29章 WinDBG及其实现 867
29.1 WinDBG溯源 867
29.2 C阶段的架构 872
29.3 重构 875
29.4 调试器引擎的架构 881
29.5 调试目标 887
29.6 调试会话 892
29.7 接收和处理命令 899
29.8 本章总结 904
第30章 WinDBG用法详解 905
30.1 工作空间 905
30.2 命令概览 908
30.3 用户界面 911
30.4 输入和执行命令 916
30.5 建立调试会话 923
30.6 终止调试会话 927
30.7 理解上下文 930
30.8 调试符号 933
30.9 事件处理 944
30.10 控制调试目标 951
30.11 单步执行 955
30.12 使用断点 962
30.13 控制进程和线程 969
30.14 观察栈 973
30.15 分析内存 978
30.16 遍历链表 987
30.17 调用目标程序的函数 992
30.18 命令程序 994
30.19 本章总结 997
附录A 示例程序列表 999
附录B WinDBG标准命令列表 1003
索引 1005
2007年,我和Raymond(张银奎)第一次在上海见面。他对Windows操作系统的浓厚兴趣给我留下了深刻的印象,他的兴趣遍及有关Windows的所有细节,包括这个产品背后的人们及其演变过程。
Raymond已经在软件开发岗位工作了十几年。现在他把多年的经验和对Windows操作系统的深刻理解结合起来,创作了这本关于调试的惊世之作。调试是计算机领域中最耗费时间和充满挑战的任务之一,也是许多软件工程师都需要提高的一个领域。
这本书所覆盖主题的广度是惊人的。从最低层硬件对调试的支持落笔,Raymond带你遍历了系统中支持调试的所有层面——从用户态到内核态。此外,他还全面深入地介绍了编译器的调试支持、调试工具和各种基础设施。
据我多年在VMS操作系统开发团队工作的经验,我发现有些工程师掌握调试技术,而有些工程师并不具备这样的能力。利用可用工具插入合适的断点和分析追踪信息都需要很特殊的技巧。
细细品味这本书,会帮助你获得这些重要的软件开发技巧,增强你控制软件和编写代码的能力。
我多么希望这本书是用英文写的!
— David Solomon
co-author, Windows Internals (Microsoft Press)
President, David Solomon Expert Seminars, Inc.
www.solsem.com
前言
现代计算机是从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章),如何支
无封面