调试软件
http://advdbg.org/books/swdbg/index.aspx
在写作《软件调试》的过程中,我使用了很多工具,包括十几种调试器和上百个不同用途的观察和监视工具。因为现有的工具不能满足写作书中某些内容的需要,于是我还专门开发了一些工具,以下是其中的几个:
- 符号文件观察器——SymView
- 用户态转储文件观察器——UdmpView
- 与内核调试引擎对话的交谈器——KdTalker
- 监视和记录CPU执行轨迹的CpuWhere
- 观察GDT、IDT的SoZoomer
您在接受以下协议的情况下,可以自由使用以上工具:
- 这些工具是免费提供给《软件调试》一书的读者的。如果你没有购买这本书,那么你应该感谢那些已经购买了这本书的朋友。
- 这些工具可能存在瑕疵,不管是否是工具自身的原因,这些工具的作者都不对使用这些工具导致的任何直接或者间接损失负任何责任。
在将以上工具公开前,我还需要对它们做一些测试和完善工作。比如使其适合更多的平台(CPU和OS),使其界面更友好等。所以我会逐步公开以上工具,请大家耐心等待!
虽然本书的重点不是编程语言和如何编写代码,但是为了不流于空泛,我们还是给出了大量的代码实例。这些实例可以分为如下几类:
- 用于演示相关调试技术的工作原理和实现方法,比如软件断点、硬件断点等。
- 充当被调试目标的靶子程序。
- 用来模拟各种错误的反例。
为本书开发的工具程序在工具栏目中有详细介绍和下载。
编译方法
所有示例程序都是使用C/C++语言编写的。大多数程序都可以使用Visual C++ 6.0编译器编译。个别程序需要使用Visual Studio 2005编译。如果程序的项目文件是以DSW/DSP为后缀的,那么便是VC6的项目文件。如果是SLN或PROJ后缀,那么便需要Visual Studio 2005或者2008。 如果您在编译这些程序时遇到困难,那么请先到问答栏目寻找是否已经存在有关的问答,如果没有那么请通过意见反馈栏目中给出的方式给我们联系。
目录结构
所有示例程序是按照右图所示的目录结构来存放的。BIN目录用来存放编译出的可执行文件和符号文件。源程序文件和项目文件保存在每一章的子目录中。编译时产生的临时文件统一放在与BIN目录并行的TEMP目录(没有画出,会自动创建)中。
下载
首先,以下程序都是与《软件调试》一书中的内容密切相关的,因此您应该在阅读书中内容后按照书中的指导使用这些程序。在任何情况下,作者不对因为使用以下程序而导致的任何直接和间接后果承担任何责任。
第二,您合法阅读和使用以下压缩包中的源程序文件和二进制文件的前提条件是您购买了《软件调试》一书。
- 示例程序的源程序文件和项目文件(2,365KB):swdbgbk_src.zip
- 示例程序的可执行文件和PDB文件(11,825KB):swdbgbk_bin.zip
- 试验使用的转储文件和有关程序文件(157KB):swdbgbk_dmp.zip
程序清单
在《软件调试》的附录A中列出了示例程序的清单,其中也包括了为本书开发的部分工具(见工具栏目)。
程序名称 | 用途 | 正文 |
Err2Fail.exe | 演示在特定条件下才表现出来的错误 | 1.6.1 |
AccKernel.exe | 从用户空间访问内核空间 | 2.5.2 |
AcsVio.exe | 写代码段导致非法访问异常 | 2.5 |
ProtSeg.exe | 应用程序代码不可以直接修改段寄存器 | 2.5 |
Fault.exe | 使用结构化异常器处理除零异常后恢复程序运行 | 3.3.3 |
B2BStep.exe | 分支到分支单步执行 | 4.3.4 |
HiInt3.exe | 在代码中插入断点指令 | 4.1.1 |
DataBP.exe | 手工设置数据断点 | 4.2.8 |
TryInt1.exe | 在用户态代码中插入INT 1指令会违反保护规则 | 4.3.1 |
CpuWhere.exe | 使用CPU的调试存储机制记录CPU的执行路线 | 5.4 |
Bts.sys | 支持CpuWhere的驱动程序 | 5.4 |
LBR.dll | 使用分支记录功能的WinDBG扩展模块 | 5.2.3 |
McaViewer.exe | 读取MCA寄存器 | 6.3.2 |
Breakout.exe | 试验应用程序自己调用DbgUiRemoteBreakin的效果 | 10.6.4 |
DebString | 用于验证OutputDebugString API的工作原理 | 10.7 |
EvtFilter.exe | 用于试验VC调试器的异常处理选项 | 10.5.5 |
HungWnd.exe | 用于观察被中断到调试器后的程序窗口 | 10.6.9 |
MiniDbgee.exe | 用作调试目标的简单Win32程序 | 10.4.2 |
TinyDbge.exe | 用作调试目标的简单控制台程序 | 10.4.2 |
TinyDbgr.exe | 使用调试API编写的简单调试器 | 10.4.2 |
SEH_Excp.exe | 探索SEH的异常处理 | 11.4.3 |
SEH_Trmt.exe | 探索SEH的终结处理 | 11.4.2 |
SEH_Mix.exe | 嵌套使用SEH的异常处理和终结处理 | 11.4.6 |
VEH.exe | 演示向量化异常处理器的用法 | 11.5.3 |
JitDbgr.exe | 一个简单的JIT调试器 | 12.5.3 |
UdmpView.exe | 读取和解析用户态转储文件 | 12.9.4 |
UEF.exe | 触发未处理异常的控制台程序 | 12.1 |
UefWin32.exe | 触发未处理异常的窗口程序 | 12.1 |
UefSndThrd.exe | 在第2个线程总触发未处理异常的控制台程序 | 12.1 |
UefSrvc.exe | 触发未处理异常的系统服务程序 | 12.1 |
UefCSharp.exe | 触发未处理异常的.Net程序 | 12.1 |
UefSilent.exe | 不显示应用程序错误对话框 | 12.4 |
ErrorMode.exe | 观察SetErrorMode API的效果 | 13.6.1 |
HiCLFS.exe | 使用CLFS API创建日志文件和读写日志记录 | 15.6 |
Crimson.exe | 演示Crimson API的用法 | 16.9 |
ETW.exe | 演示使用编程方法控制NT Kernel Logger | 16.7.2 |
RawLog.exe | 不使用清单文件而直接输出日志信息 | 16.9 |
KdTalker.exe | 与内核调试引擎的对话程序 | 18.5.7 |
Verifiee.exe | 探索程序验证器的分析目标 | 19.4.1 |
AllcStk.exe | 演示栈的创建过程和栈溢出 | 22.2.3 |
BoAttack.exe | 缓冲区溢出攻击的基本原理 | 22.10.2 |
BufOvr.exe | 存在缓冲区溢出错误的小程序 | 22.10.1 |
CallConv.exe | 包含各种函数调用协议的小程序 | 22.7 |
CallCV64.exe | 演示64位系统下的函数调用协议 | 22.7.6 |
CheckESP.exe | 不遵守栈平衡原则的小程序 | 22.6 |
HiStack.exe | 用于观察栈的小程序 | 22.3.3 |
LocalVar.exe | 用于观察局部变量的小程序 | 22.4.1 |
SecChk.exe | 演示编译器的安全检查功能 | 22.11 |
StackChk.exe | 演示栈检查函数的工作原理 | 22.8.3 |
StackOver.exe | 通过死循环导致栈溢出的小程序 | 22.8.2 |
StkUFlow.exe | 存在栈下溢错误的小程序 | 22.9 |
MemLeak.exe | 使用CRT的调试支持自动转储内存泄漏 | 23.15 |
FreCheck.exe | 用于分析释放堆块时触发的堆检查 | 23.8.3 |
HeapHFC | 演示Win32堆的释放检查(HFC)机制 | 23.6.2 |
HeapMfc | 演示内存泄漏的MFC程序 | 23.7 |
HeapOver | 演示发生在堆上的缓冲区溢出 | 23.8 |
HiHeap.exe | 用来分析基本内存分配和释放操作的控制台程序 | 23.3 |
SBHeap.exe | 使用CRT的小堆块堆 | 23.11.2 |
Interop.exe | 用于分析在同一个程序中使用两种异常处理机制 | 24.7 |
SehComp.exe | 用于分析SEH异常处理编译方法的调试目标 | 24.5.2 |
SehRaw.exe | 手工注册异常处理器 | 24.4.1 |
VC8Win32.exe | 用于分析异常处理有关的安全问题 | 24.6 |
HiWorld.exe | VC2005产生的典型Windows程序 | 25.4.1 |
PdbFairy.exe | 直接读取PDB文件的小程序 | 25.6.5 |
Sig2Time.exe | 将PDB文件中的时间戳转换为时间 | 25.8 |
SymOption.exe | 试验不同的符号文件选项 | 25.2 |
SymView.exe | 符号文件观察器 | 25.6.8 |
D4D.dll | 演示可调试设计的DLL模块 | 27.4 |
D4dTest.exe | 使用D4D.dll的测试程序 | 27.4 |
PerfView.exe | 演示性能监视程序的工作原理 | 27.5.3 |
MulThrds.exe | 用于演示线程控制命令的调试目标 | 30.13.1 |
Q&A
以下《软件调试》的读者可能会问到的一些问题。简要罗列和回答如下。如果您有其它问题或者觉得回答不够全面,那么可以通过意见反馈栏目列出的方式进一步与作者和其他读者进行讨论。
为什么要重视软件调试?
在作者看来,当一个软件工程师掌握了基本的软件知识和概念后,那么接下来要学习的最重要技能便是软件调试。根本原因有两个:
- 使用调试技术可以更好的控制软件。相对于硬件,软件更加复杂、多变,有时甚至狡黠、事故和邪恶。大的方面讲,计算机历史上的重大灾难大多是软件导致的。从小的方面讲,用户遇到的崩溃、病毒入侵、没有响应等,也大多是由软件导致的。人类在控制和驾驭软件方面的能力还在发展的过程中。目前阶段,以调试器为核心的调试技术是征服软件的最强大武器。举例来说,目前有太多的糟糕软件完全不把自己当作“仆人”,它们自作主张,借助强大的计算机硬件恣意妄为,这时最有效的方法就是使用调试器将其擒获。当我们使用调试器来自由操纵桀骜的软件和被调试的计算机系统时,我们才真正找回了主人的尊严。如果我们不得不重新安装整个系统,那么就好像是为了赶走一个糟糕的仆人而重建“家园”。
- 使用调试技术可以更精确、更直接认识的软件。当我们一步步的跟踪一个软件时,我们对它的认识精确到了指令的级别。指令是软件的本质,是承载思维的载体,软件的好坏和善恶忠奸都在它的指令中,所以在指令一级的认识软件方式对于软件工程师来说是非常重要的。因为时间关系,我们可能无法跟踪软件的每一条语句或者指令,但是我们有必要仔细跟踪关键的操作和可能存在问题的代码片段。
终上,软件调试技术对于认识和控制软件都有着重要作用。一旦掌握了这门技术,便可以在很多方面获益:
- 侦错,即定位和修正瑕疵。
- 学习新的软硬件技术。
- 安全。以研究Rootkit著称的Joanna Rutkowska在接受采访时曾经说她的计算机系统不安装任何“安全软件”,但是她安装了调试器,并且说“MS Kernel Debugger can be very useful tool for system compromise detection”
很多资深的计算机专家都非常重视软件调试,比如以开创MSDN杂志的Under The Hood专栏著称的Matt Pietriek在给本书的短评中就说,调试器是所有系统程序员必须掌握的工具。尽管他没有说所有程序员,但这并不意味着,其他程序员学习调试器没有价值。而只是说,系统程序的复杂度和重要性,要求我们必须用调试器这样的强大工具。编写应用软件时,徒手或者土办法还能应付。对于其他程序员来说,既然有更好和更高效的工具,那我们为什么不用呢?
《Linux内核源代码情景分析》的两位作者在他们的《嵌入式系统》一书中谈到调试曾经说,在软件开发的“生命周期”中,程序调试(debug)以及调试手段的重要性是“怎么强调也不为过”的。
这本书为什么这么厚?
简单来说,因为太多的内容需要写。
软件调试如此重要,但是太多人对它还知之甚少,而且很难找到好的资料来学习。在写这本书之前,我仔细搜索了的关于调试的中外图书。但是没能找到一本我理想中的关于软件调试的书。
- 我希望这本书具有一般性,探讨调试的一般原理和思想,但是又不空泛,对于关键的技术应该讲透、讲彻底。
- 我希望这本书有理论价值,但又不脱离实践,讲的应该是当代主流的软硬件环境和工具,而不是多年前的,也不是实验性的,讨论的应该是“真家伙”,面对的应该“真难题”。因为调试软件是打仗,纸上谈兵似的东西没有用。
- 我希望这本书介绍必要的软硬件基础。打仗要熟悉地形,调试软件要熟悉软件所运行的基础环境,最起码的就是CPU、操作系统和编译器。如果对这三样东西模棱两可,那么打仗时难免晕头转向。
发现还没有这样一本书之后,与其等待,不如自己来,于是我决定写这样一本书。按照上面的“希望”有了构思和框架后,漫长的写作开始了。大约每完成一篇,我会统计一下已经达到的厚度和可能的厚度。在完成第3篇后,我开始有意识的控制篇幅。在重构第3篇时,删除了长达60页的整整一章,那一章花了不只一个月时间,这次心痛的体验让我更重视后面的写作计划。
在开始编辑这本书时,我们也考虑这本书的厚度,排版尽可能紧凑,在不影响阅读的情况下,有效利用空间。比如略微缩小行距,增大字体。在编辑的后期,所有页数统计出来后,我和编辑还是做了一个痛苦的决定,再删!于是又删减了大约70页正文和一个20多页的附录,成为目前的1006页。
这本书为什么没有详细讲Linux有关的内容?
正如前面谈到的,写作这本书的一个基本原则是,有一般性而且有不流于空泛。因此我们选择例子时尽可能使其具有更广泛的代表性。考虑到Windows操作系统的流行性,我们选择了以Windows系统作为本书的操作系统实例。
这本书是否适合初学者阅读?
初学者是个很模糊的概念,因此这个问题很难直接回答。我建议有这个问题的朋友先在网络上免费阅读本书的前言和样章。然后自己判断是否适合阅读这本书。
在做2.7.5节的实验时,遇到的情况为什么和书中讲的不一样?
您使用的系统可能启用了CPU的物理地址扩展(即PAE)功能,如果是这样,那么请您参阅《使用WinDBG观察启用PAE后的分页机制》一文来做这个实验。如果是仍然遇到困难,那么请发电子邮件和我联系或者把问题发到高端调试网站中《软件调试》答疑论坛中。
在VS2002/2003/2005/2008下重新编译第3章的Fault程序后,程序为什么不断的循环?
这是因为编译器在编译除法语句nResult=nDividend/nDivisor时,将除数先放到了寄存器中,这样在异常处理中虽然将除数改为非零,但是寄存器中的除数还是零。请参考下面的讨论做修改: 关于第三章Fault演示程序的疑问。
解读PDB文件的魔码(Magic Code)——表25-6来之不易
http://advdbg.org/books/swdbg/f_pdb_magic.aspx
PDB是Windows系统中使用最多的符号文件格式。最先是由Visual C++的始祖Visual C++ 1.0引入的,时间大约为1992年。
PDB文件的内部格式是不公开的。总的来说,PDB文件是二进制格式。但是有趣的是,在它的起始处总是有一段可读的文本信息。可以用type命令或者任何文本文件编辑器来观察这段内容,比如使用Type命令可以观察到:
c:\dig\dbg\author\code\bin\Debug>type kdtalker.pdb
Microsoft C/C++ program database 2.00
上面显示的这句话被称为PDB签名,它位于每个PDB文件的最开始处,以0x1A结束。因为0x1A在ASCII码中代表文本文件的结束,所以Type命令遇到这个字符后就会停下来,不会再继续显示后面的二进制内容。
如果使用记事本打开,那么可以看到下图所示的景象:
可见除了Type命令显示的签名外,后面还有两个可读的ASCII码,即第二行的JG。这个JG代表什么呢?事实上,它就是我们要说的PDB魔码(Magic Code)。它紧跟在PDB签名后面,长度为四个字节。
上面的PDB文件是VC6产生的,被称为PDB2.0,如果观察VS 2003/2005/2008产生的PDB7.0的文件,那么会发现它的PDB魔码为DS。
为了理解PDB魔码的含义,我开始了艰苦的搜索。搜索很久后,仍一无所获。关于PDB内部格式的文章实在是少。仔细阅读了《Undocumented Windows 2000 Secrets》,也没有答案。但是在搜索的过程中,我发现了Andy Penell的博客。他在名为《“PDB过时了”意味着什么》的短文中介绍了PDB签名。
http://blogs.msdn.com/andypennell/archive/2005/12/09/502267.aspx
读了这篇短文,我立刻意识到Andy可能是给我答案的专家。于是我在这篇博客的评论中先向他提了一个问题,并随便提到DS:
Monday, April 23, 2007 11:11 PM by Raymond Zhang
Andy, could you advise what does MSF mean in the signature. As I know, the signaure is followed by DS.
Andy很快就回复了,对MSF的解释与我猜测的相同。最重要的是他把我暗带的问题也回答了,令我喜出望外:
Tuesday, April 24, 2007 12:08 PM by andypennell