第1 章 调试工具简介
许多技术性的书籍和文章都指出了在正确的软件设计和软件工程原则中包含的重要性。有些书侧重于介绍在方法与实践之间的均衡性,而有些书则注重对方法的描述。一些书讨论了面向对象设计、设计模式以及模块化编程等方法,这些方法都能帮助我们编写出更强大的软件。毫无疑问,正确的软件开发方法是所有软件项目获得成功的必要条件。然而,它们却并不是软件项目获得成功的惟一因素。无论我们把软件设计得如何完美,把开发进度安排得如何精确,在软件开发过程中总是出现各种不期而至的问题使我们感到苦恼。例如,不切实际的进度安排、复杂的组件间交互,以及遗留代码等,这些因素使我们无法仅通过一些优秀的开发方法就能做出正确的预测并解决所有的问题。除了这些重要的开发方法之外,我们还必须掌握如何以快速有效的方式来诊断复杂的问题。
本章将介绍一些非常有价值的工具,这些工具不仅在问题的诊断过程中能够带来很大的帮助,而且在处理大多数一般性的问题上也能节省时间和资金。本书讨论的大多数问题都曾因为它们的复杂性而使开发人员们倍感沮丧。有时候,即使开发人员知道如何解决某个具体的问题,但也要付出巨大的努力来跟踪并找出这个问题。然而,大多数开发人员并不知道:其实有一些辅助工具能够在跟踪和解决这些问题时为开发人员带来极大的帮助。除了有助于解决问题之外,这些工具所提供的解决方案也是非常高效的。
本章将介绍在本书中使用到的所有工具。我们将对每种工具给出详细的说明,其中包括一些重要的信息,例如应用场合、安装说明以及关于工作原理的一些背景知识。然而,本章在对工具的介绍中并没有涵盖工具的所有应用场合,而只是给出了这些工具的总体介绍。在这里列出的每个工具分别在本书其他章节中用于解决一些实际的问题。本章可以视作对这组工具的一个初步介绍,在随后的章节中将对这些工具的实际应用进行补充说明。
需要注意的是,本章所介绍的工具都是在撰写本书时的最新版本。当你阅读本章时,有些工具可能已经发布了更新的版本。但这并不会带来问题,因为这些工具的一般行为基本上保持不变。
1.1 泄漏诊断工具
应用场合 内存泄漏检测
当前版本 1.25
下载地址 ftp://ftp.microsoft.com/PSS/Tools/Developer Support Tools/LeakDiag
分析机制 日志文件
泄漏诊断工具(Leak Diagnosis Tool ,LeakDiag )是一种用于检测内存泄漏的工具。它不仅包含了一些基本的功能,例如显示在进程中存在着多少内存泄漏,而且还能提供一些详细的信息,例如执行内存分配的栈回溯(Stack Trace )信息以及内存分配统计信息。
LeakDiag 的安装过程非常简单。从下载地址下载leakdiag125.msi 文件,并且在安装过程中选择默认选项。在默认情况下,程序将被安装到C:\LEAKDIAG 目录下,并且可以以两种模式运行。在程序中包含一个命令行版本和一个图形用户界面(GUI )版本。命令行版本的程序叫做ldcmd.exe ,GUI 版本的程序叫做leakdiag.exe 。这两个版本既可以从命令行启动,也可以通过点击“Start ”按钮,然后依次选择“All Programs ”、“LeakDiag ”来启动。
LeakDiag 的功能是UMDH.exe (参见1.3 节)功能的超集。UMDH 只能显示标准堆管理器的分配信息,而LeakDiag 除了能够显示这些信息外,还能显示COM 分配信息(包括外部的和内部的)、虚拟内存分配信息以及其他的一些分配信息。总而言之,在目前的LeakDiag 版本中共支持6 种不同的分配器(Allocator ):
? 虚拟分配器
? 堆分配器[ 默认支持]
?MPHeap 分配器
?COM 的AllocatorCoTaskMem 分配器
?COM 的私有分配器
?C 运行时分配器
由于LeakDiag 支持上述所有分配器,这使得它在检测内存泄漏时成为一种非常灵活的工具。与其他内存泄漏检测工具相比,LeakDiag 的一个重要不同之处在于,它能够收集与内存相关的行为。LeakDiag 在记录内存分配的栈回溯时不需要依赖操作系统的支持,而是通过Microsoft Detours 技术来拦截对内存分配器的调用。通过这种方式,在使用LeakDiag 时就无需启用操作系统的栈回溯支持。
在图1-1 中给出的GUI 版本的LeakDiag 界面截图。在LeakDiag 界面中包含两个主要部分:当前正在运行的所有进程,以及可用的内存分配器和相应的动作按钮。要启动内存分配跟踪,只需选择其中某个进程以及想要跟踪的内存分配器,然后点击“Start ”按钮,之后再点击“Log ”按钮。当内存泄漏问题重现后,再次点击“Log ”按钮。LeakDiag 将所有信息输出为XML 格式的日志文件。在默认情况下,日志文件将被写入到C:\LeakDiag\logs 目录下,并且LeakDiag 在命名日志文件时将确保每次产生一个惟一的文件名。
与大多数内存检测工具一样,LeakDiag 的工作原理是基于不同时刻内存快照之间的比较。LeakDiag 按照指定的时间间隔来抓取内存分配操作的快照,并且通过比较两次内存快照之间的差异来找出那些没有被释放的内存(潜在的内存泄漏)。你可以通过“Log ”按钮来抓取快照。
LeakDiag 包含了一些选项来定制默认的行为。选择“Tools ”菜单中的“Options ”选项,将弹出“Options ”对话框,如图1-2 所示。
在“Options ”对话框中,你可以修改日志文件的存放路径,指定符号(Symbol )文件的路径等。与许多栈回溯工具一样,LeakDiag 需要借助正确的符号来产生有用的栈回溯信息。如果没有正确地指定符号文件的路径或者符号文件是错误的,那么在栈回溯信息中将只能看到每个栈帧(Frame )的地址。还要指出的是,栈回溯的记录过程是一项开销较高的操作,它将极大地影响程序执行的速度。事实上,有时候这种对执行速度的影响将会使得某些内存泄漏无法被重现出来(如果这个内存泄漏与并发或者时间等因素相关)。幸运的是,你可以通过一个选项来指定在记录时禁止对符号进行解析。在“Allocation size filter ”中可以指定想要记录的内存分配大小。最后,在“Max stack depth ”中可以指定将每个栈回溯中的多少个栈帧输出到日志文件中。
在第9 章中将对LeakDiag 的命令行模式以及日志文件的格式进行详细描述,并且将通过LeakDiag 来分析和找出一个真实的内存泄漏问题。
Microsoft Detours 库
Microsoft Detours 是一种在二进制级别上对现有代码进行修改或者增强的解决方案。通常,如果想修改或者增强现有的功能,往往需要对源代码进行修改并且重新编译。然而,在目前的商业开发领域中,你几乎无法获得某个组件或者产品的源代码。你可以首先通过Microsoft Detours 来拦截二进制函数,然后提供自己的函数来替换原来的函数或者在调用原来的函数之前增加一些代码(通过Trampoline 技术)。这种技术的实现原理是,将原来函数起始位置上的若干指令替换为一条无条件跳转到新函数的指令。需要注意的是,这个替换过程是在运行时动态进行的,而不是持久化的,这就意味着你可以对同一程序的不同实例采用相同的技术,并且每个实例都是相互独立的。
1.2 Windows 调试工具集
应用场合 一组调试器和工具
当前版本 6.6.0007.5
下载地址 http://www.microsoft.com/whdc/ddk/debugging/
Windows 调试工具集(Debugging Tools for Windows )是一个内容丰富的软件包,它包含了一些功能强大的调试器和工具,可以极大地提高开发人员的工作效率。
在下载地址中可以选择32 位或者64 位(Itanium 和x64 )等不同版本。软件包的安装过程非常简单,选择“Express ”安装模式将安装所有必要的工具。这里需要提醒一点:如果你打算开发定制的调试扩展(Debugger Extension ,我们将在第11 章中看到相关的内容),那么必须选择“Custom ”安装模式,并且选中安装SDK 。在表1-1 中给出了这个软件包的所有工具。
显然,最重要的工具就是调试器本身。在第2 章和第3 章中,我们将详细阐述调试器的工作原理、配置过程,以及最有效的使用方式。
在本章中,我们将详细介绍在本书中使用的所有工具。如果在工具的“下载地址”列中包含的信息是“Windows 调试工具集的一部分”,那么就表示需要安装Windows 调试工具集。
表1-1 Windows 调试工具集的工具
程序名 描 述
agestore.exe 一个很方便的文件删除工具,它能够根据最近访问日期来删除文件
cdb.exe 基于控制台(Console )的用户态调试器。基本上等同于NTSD
dbengprx.exe 一个轻量级的代理服务器,用于在两台不同的机器之间转发数据
dbgrpc.exe 用于查询和显示Microsoft 远程过程调用(Remote Procedure Call ,RPC )信息的工具
dbgsrv.exe 用于远程调试的进程服务器
dumpchk.exe 用于验证内存转储文件(Dump File )的工具
gflags.exe 用于启用或者禁止系统配置信息的工具
kd.exe 一个内核态的调试器
kdbgctrl.exe 用于控制和配置内核态调试连接的工具
kdsrv.exe 在内核态调试中使用的连接服务器
kill.exe 一个基于控制台的工具,用于终止进程
logger.exe 记录进程行为(例如函数调用)的工具
logviewer.exe 用于查看logger.exe 生成的日志文件的工具
ntsd.exe 一个基于控制台的用户态调试器。基本上等同于CDB
remote.exe 用于远程操纵控制台程序的工具
rtlist.exe 远程进程列表查看器
symchk.exe 用于验证符号文件或者从符号服务器下载符号文件的工具
symstore.exe 用于创建和维护符号库的工具
tlist.exe 列出所有正在运行的进程的工具
umdh.exe 用于检测内存泄漏的工具
windbg.exe 带有图形用户界面的用户态和内核态调试器
请注意,在撰写本书的时候,Windows 调试工具集的最新版本为6.6.0007.5 。在你阅读本书时,很可能已经发布了一个更新的版本。尽管版本可能不同,但在调试器的输出结果中可能只是存在细微的差异,因此本书的所有内容仍然是适用的。在调试器的下载地址中还包含了一组之前的版本(前2 到3 个版本)。如果你希望使用与本书中相同的版本,那么可以下载版本号为6.6.0007.5 的Windows 调试工具集。
1.3 UMDH
应用场合 内存泄漏检测
当前版本 6.0.5457.0
下载地址 Windows 调试工具集的一部分
分析机制 日志文件
UMDH 是另一个内存泄漏检测工具,它包含了LeakDiag 的一个功能子集。LeakDiag 能够跟踪来自各种分配器的内存,而UMDH 只能跟踪来自堆管理器的内存。此外,在使用UMDH 时需要启动操作系统对用户态栈回溯的支持。
在第9 章中给出了如何通过UMDH 来跟踪内存泄漏的示例。
1.4 Microsoft 应用程序验证器
应用场合 应用程序的一般性故障检测
当前版本 3.3
下载地址 http://www.microsoft.com/downloads/details.aspx? FamilyID = bd02c19c-1250-433c-8c1b-2619bd93b3a2&DisplayLang=en
分析机制 日志文件和调试器
每个有经验的开发人员都应该知道应用程序验证器(Application Verifier )这个工具。通过在进程中启用应用程序验证器,你可以捕捉所有的常规编程错误。这些错误包括无效的句柄、锁、文件路径等。一种良好的编程习惯是,在开发过程中为所有的相关进程启用应用程序验证器。然而,在应用程序验证器中的某些测试设置(Test Setting )可能极大地影响程序的执行速度,这可能使得一些与时间相关的问题无法暴露出来。解决这个问题的方法之一就是让应用程序验证器始终处于启用的状态,然后选择在特定的时刻禁用它,并且再次运行完整的测试集合以确保与时间相关的问题彻底消失。启用应用程序验证器的另一个最佳时机就是在修改软件产品中的错误(bug )时。通过启用应用程序验证器,你可以确保在修改错误时不会引入新的问题。
应用程序验证器的安装过程非常简单,只需使用默认的安装选项。在安装完成之后,你可以通过点击“Start ”按钮,然后依次选择“All Programs ”、“Application Verifier ”来启动程序。在图1-3 中给出了应用程序验证器的启动界面。
图1-3 应用程序验证器的启动界面
在“Applications ”面板中显示了当前正在执行验证的应用程序。你可以通过“File ”菜单中的“Add Application ”来增加应用程序。反之,你还可以通过“File ”菜单中的“Delete Application ”来移除应用程序。
要改变某个应用程序的设置,首先在左边的面板中选择该应用程序,然后选择“View ”菜单中的“Property Window ”。这将在启动窗口的底部增加一个属性设置区,在属性设置区中可以控制以下行为:
?Propagate :当前映像(Image )的测试设置是否应该传递到子进程。如果设置了这个属性,那么当前映像的测试设置将被传递到子进程。
?AutoClr :如果设置了这个属性,那么当应用程序验证器开始运行时,将禁止在这个映像上的所有测试设置。
?AutoDisableStop :如果设置了这个属性,那么对于给定的问题,应用程序验证器只会报告一次。
?LoggingWithLocksHeld :如果设置了这个属性,那么应用程序验证器将记录DLL 的加载和卸载等事件。需要注意的是,这将给应用程序带来一些问题,因为记录事件的动作需要在执行DllMain 函数期间进行I/O 操作。
要想获得每个测试设置的简短描述,可以将鼠标停在相应的测试设置上,直到出现一个提示信息。这个提示信息将告诉你是否需要通过调试器来查看这些测试的结果。
要想获得每个测试设置的详细描述,可以在测试设置上点击鼠标右键,然后选择以下两个选项之一:
?Properties :允许你控制所选测试设置的属性。例如,在Handles 上选择“Properties ”,可以指定在句柄跟踪时记录的信息数量。需要注意的是,并非所有的测试设置都有“Properties ”选项。
?Verifier Stop Options :允许你控制所选测试设置的选项。在图1-4 中给出了在Handles 测试设置上选择“Verifier Stop Options ”时弹出的对话框。
上述对话框可以分为几个部分:
? 在“Verifier Stop ”中包含了该测试设置能够执行的所有验证停顿(Verifier Stop )。在图1-4 中的“Verifier Stop ”部分中显示了,在验证句柄时有6 个可用的验证停顿。在这个窗口中的所有其他内容都是与所选中的验证停顿码(Verifier Stop Code )相关的。
? 在“Description ”中给出了对所选中的验证停顿的详细描述。
? “Inactive ”复选框控制着是否激活所选中的验证停顿。
? 在“Severity ”中控制验证停顿的严重程度。这个选项将直接影响验证停顿的表现形式。例如,如果将验证停顿00000300 设置为“Ignore ”,那么当这个停顿被触发时将不会中断到调试器(Break Into Debugger )。
? 在“Error Reporting ”中可以进一步指定在验证停顿被触发时发生的行为。复选框包括是否采取记录行为(例如是否被记录到某个文件中)以及是否生成栈回溯。单选按钮将指定当触发验证停顿时调试器的行为。你可以选择触发一个断点,抛出一个异常或者不发生任何中断。
? 在“Miscellaneous ”中可以指定验证停顿的触发频率。如果选中了复选框“Stop Once ”,那么这个验证停顿只有在第一次遇到时才会触发。如果选中了复选框“Non Continuable ”,那么当验证停顿被触发时将中断到调试器中,并且不能从验证停顿中恢复过来—这实际上就是使进程不能继续执行。
在启动界面中的Tests 面板(参见图1-3 )中给出了所有的测试设置。如果选中了某个复选框,那么将为在“Applications ”面板中选中的进程启用相应的测试设置。在“Tests ”面板正下方是关于测试设置的一个简短描述。
当对某个应用程序启用验证后,你可以启动这个程序,而应用程序验证器将在后台运行。根据测试设置的配置方式,你可以通过两种方式来查看应用程序验证器的运行结果。第一种方式就是查看相应的日志文件,首先选择菜单“View ”下的“Logs ”选项,然后再选择想要查看的应用程序日志文件。需要指出的是,并非所有的测试设置都是通过日志文件来报告结果。有些测试设置需要通过调试器来查看生成的结果。要想知道哪些测试设置需要通过调试器来查看结果,可以通过将鼠标悬停在测试设置上来获得帮助信息。如果某个测试设置需要使用调试器,那么你必须在调试器下运行程序以查看结果。
当应用程序验证器要求将调试器附加到进程上时,输出结果将遵循以下的格式:
VERIFIER STOP <stop-code> : <process-PID> : <message>
parameter -1: <description>
parameter -2: < description >
parameter -3: < description >
parameter -4: < description >
其中“stop-code ”表示发生了指定的验证违例事件,“PID ”表示发生故障进程的ID ,“message ”则是对故障的简单文本描述。参数列表取决于被执行的验证类型。
例如,在下面的输出结果给出了当进程试图关闭一个无效句柄时应用程序验证器所报告的信息。
=======================================
VERIFIER STOP 00000300 : pid 0xFF0: Invalid handle exception for current stack trace.
C0000008 : Exception code.
0007FBD4 : Exception record. Use .exr to display it.
0007FBE8 : Context record. Use .cxr to display it.
00000000 : Not used.
=======================================
This verifier stop is continuable.
After debugging it use 'go' to continue.
=======================================
通过GUI 模式来为程序启用测试是非常方便的,但有时候我们需要以自动的方式来启动测试。例如,软件产品是在每天晚上构建的,并且在构建完成之后立即启动自动测试。作为测试过程的一部分,质量保证小组要求在测试期间启用应用程序验证器。此时,测试人员无需通过GUI 版本的应用程序验证器在每天晚上手动启动测试过程,而是可以编写一个脚本并通过控制台版本的应用程序验证器来启动测试。应用程序验证器的默认安装路径为:
C:\windows\system32\appverif.exe
如果在启动appverif.exe 时使用了/? 开关时,那么你将看到以下输出:
Application Verifier 3.3.0045
Copyright (c) Microsoft Corporation. All rights reserved.
Application Verifier Command-Line Usage:
-enable TEST ... -for TARGET ... [-with [TEST.]PROPERTY=VALUE ...]
-disable TEST ... -for TARGET ...
-query TEST ... -for TARGET ...
-configure STOP ... -for TARGET ... -with PROPERTY=VALUE...
-verify TARGET [-faults [PROBABILITY [TIMEOUT [DLL ...]]]]
-export log -for TARGET -with To=XML_FILE [Symbols=SYMBOL_PATH]
[StampFrom=LOG_STAMP] [StampTo=LOG_STAMP] [Log=RELATIVE_TO_LAST_INDEX]
-delete [logs|settings] -for TARGET ...
-stamp log -for TARGET -with Stamp=LOG_STAMP [Log=RELATIVE_TO_LAST_INDEX]
-logtoxml LOGFILE XMLFILE
-installprovider PROVIDERBINARY
Available Tests:
Heaps
Handles
Locks
Memory
TLS
Exceptions
DirtyStacks
LowRes
DangerousAPIs
TimeRollOver
Threadpool
LuaPriv
HighVersionLie
FilePaths
KernelModeDriverInstall
InteractiveServices
PrintAPI
PrintDriver
(For descriptions of tests, run appverif.exe in GUI mode.)
Examples:
appverif -enable handles locks -for foo.exe bar.exe
(turn on handles locks for foo.exe & bar.exe)
appverif -enable heaps handles -for foo.exe -with heaps.full=false
(turn on handles and normal pageheap for foo.exe)
appverif -enable heaps -for foo.exe -with full=true dlls=mydll.dll
(turn on full pageheap for the module of mydll.dll in the foo.exe
appverif -enable * -for foo.exe
(turn on all tests for foo.exe)
appverif -disable * -for foo.exe bar.exe
(turn off all tests for foo.exe & bar.exe)
appverif -disable * -for *
(wipe out all the settings in the system)
appverif -export log -for foo.exe -with to=c:\sample.xml
(export the most recent log associated with foo.exe to c:\sample.xml)
appverif /verify notepad.exe /faults 5 1000 kernel32.dll advapi32.dll
(enable fault injection for notepad.exe. Faults should happen with
probability 5%, only 1000 msecs after process got launched and only
for operations initiated from kernel32.dll and advapi32.dll)
要想为指定的程序启用所有的测试设置,可以使用下面的命令行:
appverif.exe -enable * -for myexecutable.exe
除了为指定的程序启用测试设置之外,还可以从调试器中控制应用程序验证器。扩展命令!avrf 正是用于从调试器中控制应用程序验证器。完整的测试设置列表请参见附录A ,“应用程序验证器的测试设置”。
1.5 全局标志
应用场合 配置信息
当前版本 6.6.0007.5
下载地址 Windows 调试工具集的一部分
可执行文件 gflags.exe
全局标志(Global Flags ,gflags )是Windows 调试工具集的一部分,可执行程序(gflags.exe )位于默认的安装路径下。例如,在我的系统上,我通常使用以下的命令行来启动gflags :
C:\gflags.exe
在本书中使用的大部分工具都需要依赖Windows 的支持才能正确地工作。例如,UMDH 要求启用“Create User Mode Stack Trace Database ”这个选项。全局标志(或者说gflags )是一种对各个选项的集中式配置工具。
GUI 模式
大多数的选项要么是针对整个系统(即所有正在运行的进程),要么是针对单个进程。在图1-5 中给出了gflags 的主界面。
在“System Registry ”标签页中包含的是针对整个系统的选项,在“Image File ”标签页中包含的则是针对单个进程的选项。如果你修改了针对整个系统的设置,那么往往需要重启系统。在“Kernel Flags ”标签页中包含的是只对内核产生影响的选项。对于针对单个进程的设置修改,只有在重启进程之后才能发挥作用。
gflags 中的选项包含了操作系统中各个方面的配置信息,这些信息被保存在什么地方以及如何对它们进行解析?答案是:注册表。根据你所修改的设置是针对整个系统还是针对单个进程,相应的信息被保存在注册表中的不同位置上:
? 针对整个系统的设置: