2023年12月23日发(作者:)

如何根据程序崩溃时的DMP文件使用WinDbg查找调用堆栈

HOW TO: 查找问题的异常堆栈时出现的 UnhandledExceptionFilter 调用堆栈跟踪中

/kb/313109/zh-cn

察看本文应用于的产品

本页

概要

使用 打开转储文件

使用 确定异常堆栈

参考

展开全部 | 关闭全部

概要

没有异常处理程序定义处理引发的异常时,将调用该 UnhandledExceptionFilter 函数。 通常,该函数会将异常传递给在 为文件其中捕获,并尝试处理设置。

在进程的内存快照所在某些情况下,可以看到锁定点保存到线程的线程的调用

UnhandledExceptionFilter 函数。 在这的种情况下您可以按照本文以确定导致此异常的 DLL。

回到顶端

使用 打开转储文件

下载并安装调试程序。 要下载调试程序,请访问下面的 Microsoft 网站:

Microsoft 调试工具

/whdc/devtools/ddk/

(/whdc/devtools/ddk/)

打开安装调试程序,文件夹,然后双击 启动调试器。

在 文件 菜单上单击 打开的崩溃转储 (或按 Ctrl+D),然后选择要查看该转储文件。

回到顶端

使用 确定异常堆栈

在 ,打开进程的.dmp 文件。

请确保您符号路径指向正确的位置。 有关如何执行此操作,请访问下面的 Microsoft Web 站点:

如何获得符号

/whdc/devtools/ddk/

(/whdc/devtools/ddk/)

在命令提示符下键入 ~ * kb 以列出所有进程中的线程。

标识对函数调用的线程 Kernel32! UnhandledExceptionFilter 。 它类似于以下:

120 id: f0f0f0f0.a1c Suspend: 1 Teb 7ff72000 Unfrozen

ChildEBP RetAddr Args to Child

09a8f334 77eb9b46 0000244c 00000001 00000000 ntdll!ZwWaitForSingleObject+0xb

[ @ 2004]

09a8f644 77ea7e7a 09a8f66c 77e861ae 09a8f674 KERNEL32!UnhandledExceptionFilter+0x2b5

[D:ntprivatewindowsbaseclientthread.c @ 1753]

09a8ffec 00000000 787bf0b8 0216fe94 00000000 KERNEL32!BaseThreadStart+0x65

[D:ntprivatewindowsbaseclientsupport.c @ 453]

切换到该线程 (在本例中,该线程是"~120s")。

在第一个参数的指定位置显示内存内容 Kernel32! UnhandledExceptionFilter 通过 添加 第一个参数 。 此指向 EXCEPTION_POINTERS 结构

0:120> dd 09a8f66c

09a8f66c 09a8f738 09a8f754 09a8f698 77f8f45c

09a8f67c 09a8f738 09a8ffdc 09a8f754 09a8f710

09a8f68c 09a8ffdc 77f8f5b5 09a8ffdc 09a8f720

09a8f69c 77f8f3fa 09a8f738 09a8ffdc 09a8f754

09a8f6ac 09a8f710 77e8615b 09a8fad4 00000000

09a8f6bc 09a8f738 74a25336 09a8f6e0 09a8f910

09a8f6cc 01dc8ad8 0d788918 00000001 018d1f28

09a8f6dc 00000001 61746164 7073612e 09a8f71c

第一个 DWORD 代表异常记录。 若要获取有关异常的类型信息,请请在命令提示符处运行以下:

.exr first DWORD from step 6

0:120> .exr 09a8f738

ExceptionAddress: 78011f32 (MSVCRT!strnicmp+0x00000092)

ExceptionCode: c0000005

ExceptionFlags: 00000000

NumberParameters: 2

Parameter[0]: 00000000

Parameter[1]: 00000000

Attempt to read from address 00000000

第二个 DWORD 是上下文记录。 若要获取上下文的信息,请在命令提示符处运行以下:

.cxr second DWORD from step 6

0:120> .cxr 09a8f754

eax=027470ff ebx=7803cb28 ecx=00000000 edx=00000000 esi=00000000 edi=09a8fad4

eip=78011f32 esp=09a8fa20 ebp=09a8fa2c iopl=0 nv up ei ng nz na po nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010286

MSVCRT!strnicmp+92:

78011f32 8a06 mov al,[esi]

运行 kv 命令获得实际的异常的调用堆栈。 这有助于您识别可能未被处理正确的过程中实际问题

0:120> kv

ChildEBP RetAddr Args to Child

WARNING: Stack unwind information not available. Following frames may be wrong.

09a8fa2c 780119ab 09a8fad4 00000000 09a8faa8 MSVCRT!strnicmp+0x92

09a8fa40 7801197c 09a8fad4 00000000 6d7044fd MSVCRT!stricmp+0x3c

09a8fa80 6e5a6ef6 09a8fad4 2193d68d 00e5e298 MSVCRT!stricmp+0xd

09a8fa94 6d7043bf 09a8fad4 09a8faa8 0000001c IisRTL!CLKRHashTable::FindKey+0x59 (FPO:

[2,0,1])

09a8faac 749fc22d 09a8fad4 01d553b0 0000001c ISATQ!CDirMonitor::FindEntry+0x1e

(FPO: [Non-Fpo]) [D: @ 884]

09a8fac4 749fd1cb 09a8fad4 09a8fb10 525c3a46 asp!RegisterASPDirMonitorEntry+0x6e

(FPO: [EBP 0x09a8fb08] [2,0,4]) [D: @ 534]

09a8fb08 749fcdd6 00000000 09a8fcbc 018d1f28

asp!CTemplateCacheManager::RegisterTemplateForChangeNotification+0x8a

(FPO: [Non-Fpo]) [D: @ 621]

09a8fb3c 74a08bfe 00000000 000000fa 74a30958 asp!CTemplateCacheManager::Load+0x382

(FPO: [Non-Fpo]) [D: @ 364]

09a8fc68 74a0d4c9 04c12518 018d1f28 09a8fcbc asp!LoadTemplate+0x42

(FPO: [Non-Fpo]) [D: @ 1037]

09a8fcc0 74a2c3e5 00000000 0637ee38 09a8fd58 asp!CHitObj::ViperAsyncCallback+0x3e8

(FPO: [Non-Fpo]) [D: @ 2414]

09a8fcd8 787c048a 00000000 77aa1b03 01e91ed8 asp!CViperAsyncRequest::OnCall+0x3f

(FPO: [Non-Fpo]) [D: @ 194]

09a8fce0 77aa1b03 01e91ed8 77a536d8 00000000 COMSVCS!STAActivityWorkHelper+0xa

(FPO: [1,0,0])

09a8fd24 77aa1927 000752f8 000864dc 787c0480 ole32!EnterForCallback+0x6a

(FPO: [Non-Fpo]) [D: @ 1759]

09a8fe50 77aa17ea 000864dc 787c0480 01e91ed8 ole32!SwitchForCallback+0x12b

(FPO: [Non-Fpo]) [D: @ 1644]

09a8fe78 77aa60c1 000864dc 787c0480 01e91ed8 ole32!PerformCallback+0x50

(FPO: [Non-Fpo]) [D: @ 1559]

09a8fed4 77aa5fa6 04f2b4c0 787c0480 01e91ed8

ole32!CObjectContext::InternalContextCallback+0xf5

(FPO: [Non-Fpo]) [D: @ 3866]

09a8fef4 787bd3c3 04f2b4c0 787c0480 01e91ed8 ole32!CObjectContext::DoCallback+0x1a

(FPO: [Non-Fpo]) [D: @ 3746]

09a8ff24 787bf373 0216fb3c 00000007 09a8ffec COMSVCS!STAActivityWork::DoWork+0x73

(FPO: [0,4,2])

09a8ffb4 77e8758a 0216fe94 0216fb3c 00000007

COMSVCS!STAThread::STAThreadWorker+0x2bb

(FPO: [EBP 0x09a8ffec] [1,31,4])

09a8ffec 00000000 787bf0b8 0216fe94 00000000 KERNEL32!BaseThreadStart+0x52

(FPO: [Non-Fpo]) [D:ntprivatewindowsbaseclientsupport.c @ 451]

30.8 调试符号

在第25章中,我们详细地介绍了调试符号的概念、种类、产生过程和存储方式。本节将讨论如何在WinDBG调试器中使用调试符号,包括加载调试符号,设置调试符号选项以及解决有关的问题。

30.8.1 重要意义

调试符号(Debug Symbols)是调试器工作的重要依据,保证调试符号的准确对于调试器的正常工作非常重要。如果缺少调试符号或调试符号不匹配,那么调试器就可能显示出错误的结果。为了让大家对这一点有深刻的认识,我们先来看一个简单的例子。使用WinDBG(尚未设置符号路径)附加到一个记事本进程上,然后使用~0 s切换到0号线程,再输入k命令显示栈回溯信息,其结果如清单30-2所示。

清单30-2 没有调试符号时显示的栈回溯

0:000> k

*** ERROR: Module load completed but symbols could not be loaded for C:…

ChildEBP RetAddr

WARNING: Stack unwind information not available. Following frames may be wrong.

0007fed8 01002a1b ntdll!KiFastSystemCallRet

0007ff1c 01007511 notepad+0x2a1b

*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:... -

0007ffc0 7c816fd7 notepad+0x7511

0007fff0 00000000 kernel32!RegisterWaitForInputIdle+0x49

在上面的结果中,WinDBG报告了两个错误和一条警告,都与符号有关。第一个错误是告诉我们未能为加载符号。接下来的警告告诉我们因为缺少符号文件提供栈展开信息,所以其下各帧的信息可能是错误的。这个警告决不是空穴来风,看到了这样的警告,确实需要提高警惕,开始以怀疑的眼光观察其后的内容。例如最下面一行显示的函数名是中的RegisterWaitForInputIdle函数。栈回溯结果的最下面一个栈帧对应的是当前线程的起始函数,这个函数名怎么会是线程的起始函数呢?键入.symfix c:symbols命令设置符号文件的搜索路径(稍后将详细介绍这条命令),然后输入.reload加载符号,再次输入k命令的结果如清单30-3所示。

清单30-3 有调试符号时显示的栈回溯

0:000> k

ChildEBP RetAddr

0007feb8 7e4191ae ntdll!KiFastSystemCallRet

0007fed8 01002a1b USER32!NtUserGetMessage+0xc

0007ff1c 01007511 notepad!WinMain+0xe5

0007ffc0 7c816fd7 notepad!WinMainCRTStartup+0x174

0007fff0 00000000 kernel32!BaseProcessStart+0x23

这次的结果中没有任何错误和警告,是正确的结果。与这个结果相比,清单30-2中的显示不仅少了一个栈帧,而且显示出的四个栈帧中有三个都是不准确的,可见调试符号的重要性。

30.8.2 符号搜索路径

大多数调试任务都涉及到多个模块,因此需要加载很多个符号文件,而且这些符号文件很可能不在同一个位置上。为了方便调试,WinDBG允许用户指定一个目录列表,当需要加载符号文件时,WinDBG会从这些目录中搜索合适的符号文件。这个目录列表被称为符号搜索路径,简称符号路径(Symbol Path)。在符号路径中可以指定两类位置,一类是普通的磁盘目录或者网络共享目录的完整路径,另一类是符号服务器,多个位置之间使用分号分隔。例如,以下是一个典型的符号路径:

SRV*d:symbols*/download/symbols;c:workdebug;

第一个分号后面定义的是一个本地目录,前面定义的是符号服务器,我们稍后再详细介绍。可以有几种方法来设置符号路径。

n 设置环境变量_NT_SYMBOL_PATH和_NT_ALT_SYMBOL_PATH。

n 启动调试器(WinDBG)时,在命令行参数中通过-y开关来定义。

n 使用.sympath命令来增加、修改或者显示符号路径。如执行sympath + c:folder2便将c:folder2目录加入到符号搜索路径中。

n 使用.symfix命令来自动设置符号服务器(详见下文)。

n 使用WinDBG的GUI,通过File>Symbol File Path菜单打开Symbol Search Path对话框,然后通过图形界面进行设置。

执行不带任何参数的.sympath命令可以显示当前的符号路径。

30.8.3 符号服务器

无论是用户态调试,还是内核态调试,通常都涉及到很多个模块,而且不同的模块可能属于不同的开发部门或者公司,一个模块通常还会有很多个不同的版本,所以在调试时要为每个模块都找到正确的符号文件并不是一件简单的事。

解决以上问题的一个有效方法是使用符号服务器(Symbol Server)。简单来说,符号服务器就是用来存储调试符号文件的一个大文件库,调试器可以从这个文件库中读取指定特征(名称、版本等)的符号文件。图30-10画出了符号服务器的示意图,图中左侧是使用WinDBG调试器的工作机,右侧是符号服务器。

图30-10 符号服务器架构示意图

在工作机一端,我们画出了WinDBG进程中与访问符号有关的各个模块。其中,是Windows操作系统的调试辅助库模块,WinDBG通过它读取和解析调试符号;符号服务器DLL是符号服务器的本地模块,负责从符号服务器查找、下载和管理符号文件。

为了避免重复下载以前下载过的符号文件,符号服务器DLL会将下载好的文件保存在本地的一个文件夹中,这个文件夹使用与符号服务器上相似的方式来组织符号文件,称为下游符号库(Downstream Store),以便于符号服务器上的中央符号仓库(Centralized Store)相区分。当符号服务器DLL接收到DbgHelp的请求需要某个符号文件时,符号服务器DLL会先在下游仓库中寻找,如果寻找不到才到远程的中央符号仓库去寻找。

DbgHelp通过所谓的符号服务器API(Symbol Server API)来调用符号服务器DLL。符号服务器DLL输出这些API供DbgHelp来调用。WinDBG开发工具包中的DbgHelp帮助文件()详细描述了符号服务器API的函数原型和功能。根据符号服务器API的定义,用户也可以编写符号服务器DLL来实现自己的符号服务器,只要这个DLL正确地实现和输出符号服务器API所定义的函数。WinDBG工具包中包含了一个符号服务器DLL,名为。

可以通过以下格式来向符号搜索路径中加入符号服务器:

symsrv*ServerDLL*[DownstreamStore*]ServerPath

其中ServerDLL是符号服务器DLL的文件名称,DownstreamStore 是下游符号库的位置,ServerPath 是符号服务器的URL或共享路径,例如以下是两个有效的定义:

symsrv**mybuildsmysymbols

symsrv**localsrvmycache*/manysymbols

因为大多用户都是使用WinDBG工具包中的作为符号服务器DLL,所以可以使用以下简化形式:

srv*[DownstreamStore*]ServerPath

其中的srv相当于symsrv**。

30.8.4 加载符号文件

下面通过几个例子来说明符号文件的加载过程。清单30-4列出了在WinDBG调试器中使用ld kernel32命令从符号服务器加载符号文件的过程。也就是调试器工作线程通过SymSrv模块向符号服务器请求符号文件的过程。

清单30-4 从符号服务器获取符号文件的执行过程(节选)

0:001> kn 30

# ChildEBP RetAddr

00 00f1b158 01d1182a WININET!HttpSendRequestW //向符号服务器发送请求

01 00f1b180 01d11528 symsrv!StoreWinInet::request+0x2a

05 00f1b4f8 01d06277 symsrv!cascade+0x87

06 00f1ba48 01d06087 symsrv!SymbolServerByIndexW+0x127 //根据索引串找符号文件

07 00f1bc78 0302dfee symsrv!SymbolServerW+0x77 //符号服务器的接口函数

08 00f1c0b8 03018e7d dbghelp!symsrvGetFile+0x12e //调用符号服务器模块

09 00f1cda0 03019ee7 dbghelp!diaLocatePdb+0x33d //寻找PDB文件

0a 00f1d01c 030415fe dbghelp!diaGetPdb+0x207

0e 00f1dbd4 0303815a dbghelp!SymLoadModuleEx+0x7d //模块加载函数

0f 00f1dc00 02185a18 dbghelp!SymLoadModule64+0x2a //调试辅助库的模块加载函数

10 00f1e900 02187ca8 dbgeng!ParseLoadModules+0x188 //解析模块加载命令

11 00f1e9d8 021889a9 dbgeng!ProcessCommands+0xbd8 //分发命令

14 00f1eee4 01028553 dbgeng!DebugClient::ExecuteWide+0x6a //调试引擎的接口函数

15 00f1ef8c 01028a43 WinDBG!ProcessCommand+0x143 //处理命令

17 00f1ffb4 7c80b6a3 WinDBG!EngineLoop+0x366 //调试会话工作循环

18 00f1ffec 00000000 kernel32!BaseThreadStart+0x37 //调试会话工作线程

其中,从栈帧#16到栈帧#10是分发命令的过程,栈帧#0f是调用DbgHelp库的模块加载函数,而后调用diaGetPdb发起读取PDB文件的过程,栈帧#09中的diaLocatePdb函数是搜索PDB文件的一个主要函数,当它在符号搜索路径中指定的普通位置中找不到符号文件时,它会调用symsrvGetFile函数试图从符号服务器下载文件。接下来symsrvGetFile函数加载符号搜索路径中定义的符号服务器DLL,并调用它的SymbolServer接口函数。

SymbolServer函数是符号服务器API中的一个重要函数,它的作用就是向符号服务器请求指定的符号文件,并返回访问这个文件的完整路径。SymbolServer函数的典型实现是,如果所需要的符号文件已经在下游库中,那么便返回它的全路径,不然的话便向远程查询;如果在远程查找到,那么便将其下载到下游库,然后返回符号文件在下游库的全路径,它的函数原型如下:

BOOL CALLBACK SymbolServer(LPCSTR params, LPCSTR filename,

PVOID id, DWORD two, DWORD three, LPSTR path);

其中,第1个参数params是符号服务器的设置信息,例如"d:symbols*msdl. /download/symbols",第2个参数filename是符号文件名,如,最后一个参数path用来返回符号文件的完整路径。第3~5三个参数用来定义符号文件的版本特征,它们的用法因filename参数中的文件类型而定,如表30-6所示。

表30-6 SymbolServer函数用来指定文件版本的参数

模块后缀

.dbg

参数id

PE文件头中定义的映像时间戳(TimeDateStamp)

参数two

PE文件头中定义的映像文件大小(SizeOfImage)

同上

PDB年龄(Age)

参数three

没有使用,为0

PE文件(.exe/.dll)

.pdb

同上

PDB签名

同上

没有使用,为0

SymbolServer首先根据参数中指定的特征调用SymbolServerGetIndexStringW函数生成一个索引串。比如,以下是为某一版本的模块生成的符号索引串:

0:001> du 00f1ba60

00f1ba60 "006D2240474D414087FF801C64935DDD"

00f1baa0 "2"

其中,006D2240474D414087FF801C64935DDD是GUID签名,即{006D2240-474D- 4140-87FF-801C64935DDD},2是PDB文件的年龄(Age)。

接下来,SymbolServer函数调用SymbolServerByIndexW函数取符合指定索引串的符号文件。后者会调用一个名为cascade的函数,cascade函数先使用StoreUNC类在下游库中查找指定的符号文件是否存在,如果存在便返回完整的路径。

如果在下游库中没有找到匹配的符号文件,那么cascade便使用StoreWinInet类来搜索远程的中央符号库,即栈帧#04到栈帧#00所示的情况。

以下是SymbolServer函数返回时path参数中所存放的内容: "c:",其中006D2240474D414087FF801C64935DDD2就是索引串,c:dstore是下游符号库的根目录。

使用.reload命令可以重新加载所有或者指定模块的符号文件,以下是主要的执行步骤:

00f1d488 030380b5 dbghelp!LoadModule+0x501 //DbgHelp库的内部函数

00f1d4f0 02190a29 dbghelp!SymLoadModuleExW+0x65 //DbgHelp库的加载模块函数

00f1e440 022215ed dbgeng!ProcessInfo::AddImage+0xbf9 //增加模块对象

00f1e8d0 02102822 dbgeng!TargetInfo::Reload+0x1cbd //调试目标的基类方法

00f1e8e4 0210577f dbgeng!DotReload+0x22 //分发给Reload命令

00f1e8f8 0218758e dbgeng!DotCommand+0x3f //元命令的入口

00f1e9d8 021889a9 dbgeng!ProcessCommands+0x4be //调试器引擎的命令分发函数

因为是元命令,所以ProcessCommands先分发给DotCommand函数,后者再调用DotReload,DotReload交给TargetInfo类的Reload方法。Reload方法枚举进程中的各个模块,对于每个模块调用ProcessInfo类的AddImage方法将其加入到进程信息中。AddImage方法会调用调试辅助库(dbghelp)的SymLoadModuleExW方法来加载这个模块的信息,包括符号文件,接下来的过程与清单30-4所示的情况非常类似。

除了使用ld和.reload命令直接加载符号文件,某些使用符号的命令也可以触发调试器来加载符号,比如栈回溯命令(k*)和反汇编命令等。

值得说明的是,因为WinDBG默认使用所谓的懒惰式符号加载策略(lazy symbol loading),所以当它接收到模块加载事件时,它并不会立刻为这个模块加载符号文件。因此,当我们观察模块列表时会看到很多模块的符号状态都是deferred,即推迟加载。

30.8.5 观察模块信息

可以使用以下方法之一来观察模块信息,包括加载符号文件的情况:

n 使用lm命令。

n 使用!lmi扩展命令。

n 使用WinDBG图形界面的模块列表对话框(Debug>Modules)。

我们先介绍lm命令。如果不指定任何参数,那么lm命令显示一个简单的列表:

0:001> lm

start end module name

01000000 01093000 WinDBG (pdb symbols) d:...

01400000 015c6000 ext (deferred) …

其中start列和end列分别是该模块在进程空间中的起始地址和终止地址,module name列是模块名称,接下来的一类是加载符号文件的状态,第4列是符号文件的完整名称(如果已经加载符号文件)或者空白。表30-7列出了符号状态列中可能出现的状态信息和它们的含义。

表30-7 符号文件加载状态

缩写

deferred

#

T

C

DIA

Export

M

PERF

Stripped

PDB

COFF

含义

模块已经加载,但是调试器还没有试图为其加载符号,会在需要时尝试

符号文件和执行映像文件存在不匹配,比如时间戳或校验和不一致

时间戳缺失、不可访问或者等于0

校验和缺失、不可访问或者等于0

符号文件是通过DIA(Debug Interface Access )方式加载的

没有发现符号文件,使用映像文件的输出信息(如DLL的Export)作为符号

符号文件和执行映像文件存在不匹配,但是仍然加载了这样的符号文件

执行文件包含性能优化代码,对地址进行简单加减运算可能产生错误结果

调试信息是从映像文件中抽取出来的

符号信息是.PDB格式

符号信息是COFF格式(Common Object File Format)

PDB文件又分为私有PDB文件和公共PDB文件,后者是在前者的基础上剥离私有信息后产生的(详见25.2.3节)。

如果要为每个模块显示更丰富的信息,那么可以使用v选项,例如:

0:001> lm v

start end module name

01000000 01093000 WinDBG (pdb symbols) d:....

Loaded symbol image file: C:

Image path: C:

Image name:

Timestamp: Thu Mar 29 21:09:08 2007 (460C00C4)

CheckSum: 0008852B…

如果想控制要显示的模块,那么可以使用如下方法。

n 使用m开关来指定对模块名的过滤模式,比如lm m k*显示模块名以k开头的模块。

n 使用M开关来指定对模块路径的过滤模式(参见下文关于x命令的说明)。

n 使用o开关只显示加载的模块(排除已经卸载的模块)。

n 使用l开关只显示已经加载符号的模块。

n 使用e开关只显示有符号问题的模块。

也可以使用!lmi扩展命令来观察模块的信息,但是这个命令每次只能观察一个模块,清单30-5给出了针对WinDBG模块的执行结果。

清单30-5 使用!lmi命令观察模块信息

0:001> !lmi WinDBG //参数也可以是模块的基地址

Loaded Module Info: [WinDBG]

Module: WinDBG //模块名称

Base Address: 01000000 //模块在内存中的基地址

Image Name: C: //映像文件的全路径

Machine Type: 332 (I386) //模块所针对的CPU架构

Time Stamp: 460c00c4 Thu Mar 29 21:09:08 2007 //时间戳

Size: 93000 //文件大小,字节

CheckSum: 8852b //校验和

Characteristics: 102

Debug Data Dirs: Type Size VA Pointer //调试数据目录,详见25.4.3节

CODEVIEW 23,c348,b748 RSDS - GUID: {CDA70185-4AB9-4F6F-8B60-FDC14F75FB31}

Age: 1, Pdb:

Image Type: FILE - Image read successfully from debugger.

C:

Symbol Type: PDB - Symbols loaded successfully from symbol server.

d:

Load Report: public symbols , not source indexed

30.8.6 检查符号

可以用标准命令x(或X,不分大小写)来检查调试符号,其命令格式如下:

X [选项] 模块名!符号名

其中的模块名和符号名都可以包含通配符,*代表0或任意多个字符,?代表任一个单一字符,#代表它前面的字符可以出现任意次,比如lo#p表示所有以l开始p结束,中间有任意多个o的所有符号,比如lop,loop,looop,…。如果中间允许多个字符重复,那么可以使用方括号,例如用m[ai]#n可以通配man、min、maan、main、maiain等。

举例来说,使用x ntdll!dbg*可以列出ntdll模块的所有以dbg开头的符号,即:

0:000> x ntdll!dbg*

7c95081a ntdll!DbgUiDebugActiveProcess =

第一列是这个符号的地址,如果符号是函数,那么便是这个函数的入口地址,如果符号是变量,那么便是这个变量的起始地址。等号后面用来显示符号的类型或取值,这需要私有符号文件中的类型信息。因为我们没有NTDLL的私有符号信息,所以WinDBG显示

打开调试版本的dbgee小程序,然后执行x dbgee!arg*命令,得到的结果如下:

0:000> x dbgee!arg*

0041718c dbgee!argret = 0

00417184 dbgee!argv = 0x003a2e90

0041717c dbgee!argc = 3

可见,等号后面出现了每个变量的取值,argc是命令行参数的个数,argv是参数数组的指针。

类似的,模块名中也可以使用通配符,比如x *!_crtheap会检查所有模块,看其是否有_crtheap符号,如果有便显示出来:

0:000> x *!_crtheap

103130d0 MSVCR80D!_crtheap =

77c62418 msvcrt!_crtheap =

下面我们看一下x命令的选项,根据选项的功能可以分为如下几类。

n 控制显示结果的排列顺序,例如/a和/A分别代表按地址的升序和降序,/n和/N分别代表按名称的升序和降序,/z和/Z分别代表按符号大小(size)的升序和降序。

n 显示符号的数据类型,即/t。

n 显示符号的符号类型和大小(/v),其中符号类型分为local(局部)、global(全局)、parameter(参数)、function(函数)或者unknown(未知)。

n 按符号大小设置过滤条件,其格式为/s <符号大小>。对于函数类符号,其大小是这个函数在内存中的大小(字节数),对于其他符号,是这个符号的数据类型的大小。

n 控制显示格式,/p可以省去函数名与括号之间的空格,/q参数可以启用所谓的引号格式来显示符号名。

下面给出几个例子来说明以上选项。首先我们看/v选项,在前面的x dbgee!arg*命令中加入/v:

0:000> x /v dbgee!arg*

prv global 0041718c 4 dbgee!argret = 0

prv global 00417184 4 dbgee!argv = 0x003a2e90

prv global 0041717c 4 dbgee!argc = 3

现在的显示多了三列,最左边的prv代表这个符号属于私有(private)符号信息,如果是公共符号,那么显示为pub(public)。第二列是符号类型,global代表全局变量,接下来是这个符号在调试目标中的地址,第4列是符号的大小,这里列出的几个符号的大小都是4个字节。如果再增加/t选项,那么显示结果变为:

0:000> x /v /t dbgee!arg*

prv global 0041718c 4 int dbgee!argret = 0

prv global 00417184 4 unsigned short ** dbgee!argv = 0x003a2e90

prv global 0041717c 4 int dbgee!argc = 3

可见,在符号大小后面多了数据类型,argret和argc的类型都是int(整数),argv是unsigned short **即wchar_t **,也就是字符串指针数组。以下是使用/v开关来观察函数符号:

0:000> x /v dbgee!wmain

prv func 00411790 51 dbgee!wmain (int, wchar_t **)

注意,在函数名wmain与左括号之间有一个空格,如果不需要这个空格,那么可以指定/p开关,即:

0:000> x /v /p dbgee!wmain

prv func 00411790 51 dbgee!wmain(int, wchar_t **)

可见空格被删除了,这主要是为了复制整个函数声明时会更方便些。以下是使用更多选项的例子:

0:000> x /v /q /t /N dbgee!*main*

prv func 00411520 f @!"dbgee!wmainCRTStartup" ()

prv func 00411790 51 @!"dbgee!wmain" ()

prv global 00417194 4 int @!"dbgee!mainret" = 0

pub global 00418288 0 @!"dbgee!_imp____wgetmainargs" =

pub global 00411c92 0 @!"dbgee!__wgetmainargs" =

prv func 00411540 244 @!"dbgee!__tmainCRTStartup" ()

prv global 00417020 4 unsigned int @!"dbgee!__native_dllmain_reason" = 0xffffffff

因为使用了/q参数,所以以上符号名是以@!”模块名!符号名”的格式显示的。另一点值得注意的是,因为公开的符号信息不包括类型信息,所以其类型部分显示为,符号大小也显示为0。

3.8.7 搜索符号

标准命令ln(List Nearest Symbols)用来搜索距离指定地址最近的符号,比如:

lkd> ln 8053ca11

(8053ca11) nt!KiSystemService | (8053ca85) nt!KiFastCallEntry2

Exact matches:

nt!KiSystemService =

上面的结果显示了地址8053ca11附近的两个符号,其中KiSystemService与指定的地址精确匹配。

30.8.8 设置符号选项

元命令.symopt用来显示和修改符号选项,其命令格式为:

.symopt[+/- 选项标志]

WinDBG使用一个32位的DWORD来记录符号选项,每个二进制位代表一个选项。使用+可以设置指定的标志位,使用-可以移除指定的标志位,不带任何参数便显示当前的设置。表30-8列出了目前定义的所有标志位。

表30-8 符号选项的各个标志位

标志位

0x1

0x2

0x4

0x8

常量

SYMOPT_CASE_INSENSITIVE

SYMOPT_UNDNAME

SYMOPT_DEFERRED_LOADS

SYMOPT_NO_CPP

含义

不分大小写

显示未装饰的符号名

延迟加载符号

关闭C++翻译*

默认值

On

On

On

Off

标志位

0x10

常量

SYMOPT_LOAD_LINES

含义

加载源代码行信息

允许为优化过的代码使用最相默认值

**

0x20 SYMOPT_OMAP_FIND_NEAREST

近的符号

On

0x40

0x80

0x100

0x200

0x400

SYMOPT_LOAD_ANYTHING

SYMOPT_IGNORE_CVREC

SYMOPT_NO_UNQUALIFIED_LOADS

SYMOPT_FAIL_CRITICAL_ERRORS

SYMOPT_EXACT_SYMBOLS

降低匹配符号的挑剔度

忽略映像文件的CV记录

禁止符号处理器自动加载模块

显示关键错误

严格评估所有符号文件

Off

Off

Off

On

Off

续表

标志位

0x800

常量

SYMOPT_ALLOW_ABSOLUTE_SYMBOLS

含义

允许位于内存绝对地址的符号

忽略环境变量中的符号和映像默认值

Off

0x1000 SYMOPT_IGNORE_NT_SYMPATH

路径

对于安腾处理器系统,强制列举Off

0x2000 SYMOPT_INCLUDE_32BIT_MODULES

32位模块

忽略全局、局部和作用域相关的Off

0x4000 SYMOPT_PUBLICS_ONLY

符号

Off

0x8000 SYMOPT_NO_PUBLICS

不搜索公共符号表

其他方法失败时才使用PDB文Off

0x10000 SYMOPT_AUTO_PUBLICS

件中的公共符号

On

0x20000

0x40000

SYMOPT_NO_IMAGE_SEARCH

SYMOPT_SECURE

不搜索映像文件

(内核调试)Secure Mode

(远程调试)不显示代理服务器On

Off

0x80000 SYMOPT_NO_PROMPTS

的认证对话框

***

0x80000000 SYMOPT_DEBUG

显示符号加载过程

Off

* 使用C++翻译时,类成员的__或被替换为::。

** 在KD和CDB中默认为Off,在WinDBG中默认为On。进行源代码级调试时,必须设置这个选项。

*** 在KD和CDB中默认为On,在WinDBG中默认为Off。

因为记忆和使用十六进制的标志位比较困难,所以WinDBG提供了扩展命令!sym来设置常用的选项。比如!sym noisy相当于.symopt+0x80000000,即开启所谓的“吵杂”式符号加载,显示加载符号的过程信息,!sym quiet相当于.symopt-0x80000000,用来关闭吵杂模式。

30.8.9 加载不严格匹配的符号文件

在实际工作中,有时要调试的程序只是做了简单的重新构建(rebuild),代码仅有微小的变化或者根本没有变化。这时,如果调试环境中只有旧的符号文件,那么调试器默认仍会因为符号文件和映像文件不匹配而拒绝加载符号文件。

一种解决方法是使用.reload /i命令来加载不完全匹配的符号文件。为了便于发现问题,最好先使用!sym noisy命令开启加载符号的“吵杂”模式,清单30-6给出了启动吵杂模式后重新加载内核模块的执行结果。

清单30-6 强制加载不严格匹配的符号文件

0:000> .reload /i

SYMSRV: d:75DC… not found

SYMSRV: /…//75DC…15565162/ not found

DBGHELP: C: - mismatched pdb

DBGHELP: c: - mismatched pdb

DBGHELP: Loaded mismatched pdb for C:digdbg…

*** WARNING: Unable to verify checksum for

DBGENG: has mismatched symbols - type ".hh dbgerr003" for details

DBGHELP: dbgee - private symbols & lines

C: - unmatched

以上信息说明,WinDBG在本地符号库(第2行)和符号服务器(第3行)都没有找到精确匹配的符号文件,然后从debug目录加载了不完全匹配的符号文件(第4、5行)。执行lm命令显示模块列表,可以看到dbgee模块的符号状态栏中包含字符M,表示符号文件和执行映像文件存在不匹配:

00400000 0041a000 dbgee M (private pdb symbols) C:...

除了使用带有/i开关的.reload命令,也可以通过设置符号选项SYMOPT_LOAD_ ANYTHING(0x40)来让调试器加载不严格匹配的符号文件。

0:000> .symopt+0x40

Symbol options are 0x30277:

0x00000001 - SYMOPT_CASE_INSENSITIVE

0x00000002 - SYMOPT_UNDNAME

0x00000004 - SYMOPT_DEFERRED_LOADS

0x00000010 - SYMOPT_LOAD_LINES

0x00000020 - SYMOPT_OMAP_FIND_NEAREST

0x00000040 - SYMOPT_LOAD_ANYTHING

0x00000200 - SYMOPT_FAIL_CRITICAL_ERRORS

0x00010000 - SYMOPT_AUTO_PUBLICS

0x00020000 - SYMOPT_NO_IMAGE_SEARCH

本节使用比较大的篇幅详细介绍了调试符号有关的WinDBG命令,熟练使用这些命令对于调试非常重要,希望读者能够认真体会并在实际调试中灵活应用。