转载的,还没仔细看。先放着吧。
转自: http://www.cnblogs.com/itrust/archive/2006/08/17/479603.html
使用VC++生成调试信息
ZhangTao,zhangtao.it@gmail.com, 译自 “Generating debug information with Visual C++”,Oleg Starodumov
引子
当我们使用调试器来调试程序时,我们希望能够单步调试到源代码中,在代码中设置断点,观察变量的值(包括用户自定义的复杂类型的值)。但是可执行文件只含有原始的字节数据——机器指令和操作系统执行程序时所使用的头信息和表信息。操作系统加载并运行可执行文件后,它根据不同的需求使用不同片段的内存(栈、堆)存放数据,其中的存放的依然是原始的字节数据。那么,调试器如何知道当前CPU指令对应哪一行代码?如何知道堆栈中的地址对应哪一个函数的局部变量?答案是“调试信息”,调试信息是高级编程语言和运行程序的原始字节数据之间的桥梁。名词解释
位置(location): 在不同的情况有不同的含义。对于函数而言,是函数首字节的地址;对于全局和静态变量而言,是内存中变量的首字节;对局部变量和函数参数而言,通常是该变量的首字节相对于函数堆栈的预先定义的基址的偏移。另外,其他类型的位置也可能出现,如:寄存器、TLS slot(参见:http://www.blogcn.com/u2/38/94/silannyukun/blog/37069531.html)、元数据标记(metadata token, 参见http://naoku.net/blogs/framesniper/archive/2005/04/12/1910.aspx)。
FPO (frame pointer omission) : 帧指针省略,FPO用来链接CodeView或PDB符号。它在编译器没有用EBP寄存器生成标准堆栈桢(a standard stack frame) 的地方帮助调试器查找函数的参数和本地变量。调试信息的类型
我们只讨论在Intel X86平台上的现有的由微软提供的调试器。
信息的类型 | 描述 |
公共函数和变量 | 用于描述在多个的编译单元(源代码文件)中可见的函数和变量,调试信息保存每个函数和变量的位置(location)和名称。 |
私有函数和变量 | 用于描述除公共函数和变量以外的所有函数和变量,包括静态函数、静态和局部变量、函数参数),调试信息保存每个函数和变量的位置、大小和名称。 |
源文件和代码行信息 | 用于将每一行代码映射到可执行文件的某个位置上。当然,某些代码行不能做映射,如注释行,这样的代码行在调试信息中不做体现。 |
类型信息 | 用于存储每一个函数和变量的类型信息。对于变量或函数参数,类型信息能够告诉调试器它是整型还是字符串类型,或是用户自定义的类型。对于函数,类型信息记载了参数的个数、调用转换和返回值的类型。 |
FPO信息 | 对于做了FPO优化的函数,调试信息保存了一些数据来帮助调试器确定函数堆栈帧的大小,甚至在帧指针无效时也能工作。 如果没有FPO信息,调试器无法正确显示被优化的程序的调用堆栈。 |
编辑和继续执行信息 | 用于帮助Visual Studio IDE在调试时实现编辑和继续执行的功能 |
调试信息格式
现在来探索调试信息是如何存储的。在过去的十年中,微软开发工具使用了几种不同的格式来包装调试信息。这里我们讨论COFF、CodeView和应用的最广泛的PDB(Program Database)格式。在讨论每种格式时,我们从下列几个特性着手:
- 哪些类型的调试信息可以通过该格式保存?
- 调试信息究竟保存在哪里(在可执行文件中,还是单独的一个文件)?
- 该格式是否有文档说明?
COFF
COFF是这里要涉及的所有格式中最古老的一种,它只能保存三种调试信息: 公共函数和变量,源文件和代码行信息,FPO信息。COFF总是保存在可执行文件中,不能够单独保存在其他文件中。该格式的文档说明参见:微软可移植可执行和通用对象文件格式规范.
CodeView
CodeView是较COFF更新的而且更复杂的一种格式,它可以存储除编辑和继续执行信息外的所有类型的调试信息。CodeView通常保存在可执行文件中,它也可从可执行文件中导出到一个单独的文件(.DGB文件)。CodeView文档不全,其文档可以在MSDN中的VC++5.0符号调试信息规范(Symbolic Debug Information Specification)中找到。
Program Database 程序数据库
这是三种中最新的一种调试信息格式,可以存储所有类型的调试信息(包括编辑和继续执行信息),也支持增量编译(其余两种格式不支持)。程序数据库信息保存在一个单独的.PDB文件中。遗憾的是,微软没有提供程序数据库格式的文档,只提供特殊的编程接口DbgHelp 和DIA来访问它。目前,程序数据库格式有两个版本,第一版(PDB2.0)为VC6.0所用,第二版(PDB 7.0)被Visual Studio.NET采用。PDB 7.0不能向上兼容,也就是说:VC6.0不能读取PDB 7.0格式。
三种格式对比如下:
格式 | 是否有文档 | 存储 | 公共函数和变量 | 私有函数和变量 | 源文件和代码行信息 | 类型信息 | FPO 信息 | 编辑和继续执行信息 |
COFF | 有 | 可执行文件中 | + | - | + | - | + | - |
CodeView | 部分 | 可执行文件中 或.DBG文件中 | + | + | + | + | + | - |
Program Database | 无 | .PDB文件中 | + | + | + | + | + | + |
生成调试信息
构造(build)过程
一个典型的可执行文件的构造过程包含两步:编译和链接。首先,编译器分析源文件,生成机器指令(保存在.obj对象文件中);然后链接器将所有可用的对象文件合并到最终的可执行文件。在对象文件之外,链接器也会用到库文件(库文件也是其他一些对象文件的汇集)。整个构造过程如下图:
如果我们想要为可执行文件生成调试信息,也得经历两步:首先,编译器为每一个源文件创建调试信息;然后,链接器合并由编译器创建得调试信息,如下图:
缺省状态下,编译器和链接器不会产生调试信息。因此我们必须通过编译和链接选项来要求编译器和链接器生成调试信息,我们也可以指定生成哪些类型得调试信息,使用什么调试信息格式,将调试信息保存在什么地方。
接下来,我讨论具体得编译器和链接器选项。
Visual C++ 6.0
编译器 Compiler
有下列选项:
/Zd 生成COFF格式的调试信息,保存在对象文件中
/Z7 生成CodeView格式的调试信息,保存在对象文件中
/Zi 生成程序数据库格式的调试信息,保存在.PDB文件中
/ZI 与 /Zi 基本一致, 唯一不同的是调试信息中包含编辑和继续执行信息
缺省时,/Zi 和 /ZI 选项生成的PDB文件名为VC60.PDB,也可以使用/Fd指定文件名。
选项 | 格式 | 存储文件 | 内容 |
/Zd | COFF | .OBJ |
|
/Z7 | CodeView | .OBJ |
|
/Zi | Program Database | .PDB |
|
/ZI | Program Database | .PDB |
|
链接器Linker
下列选项可用:
/debug 告诉链接器生成调试信息,如果该选项不使用,则其他所有选项都无效
/debugtype 指定调试信息格式,可能的用法包括:
/debugtype:coff COFF格式。注意:该选项下,调试信息中不包含源文件和代码行信息
/debugtype:cv CodeView或程序数据库格式。究竟是哪一种格式,由/pdb决定
/debugtype:both 同时使用COFF格式和CodeView/程序数据库格式
/pdb 决定是CodeView还是程序数据库格式。/pdb:none 表示CodeView格式,/pdb:filename(如/pdb:myexe.pdb)表示使用程序数据库格式,文件名为myexe.pdb。在/debugtype:coff 选项下,/pdb 选项无效。
/pdbtype 该选项只在一个或多个对象文件或库文件的调试信息也保存在一个单独的PDB文件中。/pdbtype:sept 选项可以使得调试信息各自保存在各自的PDB文件中,这样可以加快链接速度,不利的是调试信息分散,调试时需要多个PDB文件。相对的,/pdbtype:con 选项使得所有调试信息都保存在与可执行文件对应的最终的PDB文件中。
为便于理解各个选项的配对使用,请见下表:
/debugtype | /pdb | 格式 | 存储 |
coff | /pdb:none (无效) | COFF | 在可执行文件中 |
coff | /pdb:filename (无效) | COFF | 在可执行文件中 |
cv | /pdb:none | CodeView | 在可执行文件中 |
cv | /pdb:filename | Program Database | .PDB 文件 |
both | /pdb:none | COFF and CodeView | 在可执行文件中 |
both | /pdb:filename | COFF and Program Database | COFF 信息在可执行文件中, 程序数据库信息在 .PDB 文件中 |
Visual C++.NET (2002 and 2003)
编译器 Compiler
下列选项可用:
/Z7 生成CodeView格式的调试信息,保存在对象文件中
/Zd, /Zi 和 /ZI都表示生成程序数据库格式的调试信息,保存在.PDB文件中. 不同之处是调试信息的内容(见下表)。
缺省时,/Zd,/Zi 和 /ZI 选项生成的PDB文件名为VC70.PDB或VC71.PDB,也可以使用/Fd指定文件名。
注意: VC++.NET 编译器不支COFF。
选项 | 格式 | 存储 | 内容 |
/Z7 | CodeView | .OBJ |
|
/Zd | Program Database | .PDB |
|
/Zi | Program Database | .PDB |
|
/ZI | Program Database | .PDB |
|
链接器Linker
下列选项可用:
/debug告诉链接器生成调试信息,如果该选项不使用,则其他所有选项都无效。调试信息的格式总是程序数据库格式,保存在PDB文件中。缺省的,链接器使用可执行文件名生成PDB文件名。PDB文件名可包含所有调试信息的变量内容。
/pdb 指定PDB文件名.
/pdbstripped 允许链接器生成附加的PDB文件,该文件的内容限定于:
- 公共函数和变量
- FPO信息
注意: COFF 和 CodeView 格式不被 VC++.NET链接器支持。
静态库的调试信息
由于没有连接过程,静态库的调试信息的生成比可执行文件要简单的多。不考虑编译器版本(VC6 或 VS.NET),我们可以使用(/Zd, /Z7, /Zi, /ZI)中一个选项通知编译器为静态库生成调试信息。
关键问题是将调试信息保存在什么地方。当使用/Z7或/Zd选项时,调试信息保存在.LIB文件中;当使用/Zi或/ZI选项时,调试信息保存在.PDB文件中(当然可以使用/Fd指定文件名)。
调试信息对可执行文件的大小的影响
调试信息对可执行文件的大小的影响,决定于存储调试信息的地方,也间接的决定于所使用的格式。
COFF和CodeView格式下,调试信息保存在可执行文件中,因此可执行文件的大小将显著增长(通常要增长一倍以上,甚至更大)。
程序数据库格式下,调试信息单独保存,对可执行文件的大小几乎没有影响。在这种情况下,可执行文件需要保存一个头信息方便调试器对调试信息进行定位,因此需要增长大约几百个字节。
要避免可执行文件的膨胀,我们需要在使用/debug 同时,将/opt:ref 选项改为opt:noref。这样做,有一个另外的结果就是关闭了链接器的大小优化。如果要恢复大小优化,需要改回/opt:ref。
.DBG 文件
使用一个小工具——Rebase——可以将CodeView格式的内容从可执行文件中导出,存入到DBG文件中。Rebase包含在Visual Studio中。除了用于导出DBG文件外,它还有其他的一些用途。如果用于导出DBG文件,其命令行格式为:
rebase –b BaseAddr –x SymbolDir [-p] ExeName
选项 | 描述 |
-b BaseAddr | 指定可执行文件的基地址,如果你不想更改基地址,就指定当前可执行文件所使用的地址 |
-x SymbolDir | 制定存放.DBG文件的目录, 使用“.”表示当前目录 |
-p | 如果该选项被使用,DBG文件只包含公共函数和变量和FPO信息 |
例如:下面的命令行从DLL中导出调试信息到当前目录下的DBG文件中: rebase –b 0x60000000 –x . MyDll.dll
调试器和调试信息的格式
通用的调试器支持的格式如下:
调试器 | COFF | CodeView | Program Database (2.0) | Program Database (7.0) |
Visual Studio.NET | - | + | + | + |
Visual C++ 6.0 | + | + | + | - |
WinDbg 6.3 | + | + | + |
WinDbg 6.3 部分支持CodeView格式,它只能读取下列信息:
- 公共函数和变量
- FPO信息
- 源文件和代码行信息
它可以单步进入源代码,看到调用堆栈,但无法观察变量的值(因此类型信息不被支持).
操作系统符号文件(symbols)
Windows操作系统所公开的调试系统格式如下:
操作系统 | 格式 |
Windows NT 4.0 | CodeView (.DBG files) |
Windows 2000 | CodeView (.DBG files) and Program Database (2.0) |
Windows XP (including SP1 and SP1a) | Program Database (2.0) |
Windows XP SP2 | Program Database (7.0) |
Windows 2003 Server | Program Database (2.0) |
Windows程序调试系列文章——Windbg轻松上路
摘译自 WinDbg the easy way ,Oleg Starodumov
源码下载:http://files.cnblogs.com/itrust/WindbgEasyWayDemo.rar
如果要说最好的调试器是什么?那一定是:Visual Studio + Windbg。Visual Studio直观简捷,Windbg强大复杂。在你调试程序的时候,如果使用Visual Studio感觉束手无策时,就该考虑Windbg了,但Windbg是如此的专业,入门是如此的难。有没有更简单轻松一点的办法呢?可以考虑先使用CDB(Windbg的姐妹——轻量级控制台程序)。CDB和Windbg的命令是一致的,一旦熟悉了CDB,Windbg可上手了。
1 简介
1.1 环境准备
首先需要通过环境变量_NT_SYMBOL_PATH来配置符号文件的定位,可以从微软的网站上去下载,也可直接指定网站地址,让CDB和Windbg需要时自己去找,各自如此设置:
set _NT_SYMBOL_PATH = D:/debug/symbols;D:/debug/WindowsXP-KB835935-SP2-symbols
set _NT_SYMBOL_PATH = srv*c:/symbols*http://msdl.microsoft.com/download/symbols
注意:除了操作系统符号文件的定位,你也需要设置自己的程序的调试信息(*.pdb文件)的定位,如上例中的D:/debug/symbols。
1.2 CDB命令行基本用法
选项 | 描述 | 举例 |
-p Pid | 告知CDB通过进程号挂接到某个进程 | cdb -p 1034 |
-pn ExeName | 告知CDB通过进程的可执行文件名挂接到某个进程。如果当前有多个同名的进程运行,则不能使用该选项(CDB会报错) | cdb -pn myapp.exe |
-psn ServiceName | 告知CDB通过服务名挂接到某个Windows服务的进程 | cdb -psn MyService |
1.3 命令行用法汇总
这里列出本文将使用到的命令行,也是CDB的主要用法。
通过进程号以非侵入模式挂接到进程上,执行一些命令(command1; command2;...;commandN;),然后推出(q),并输出日志文件:
cdb -pv -p <processid> -logo out.txt -lines -c "command1;command2;...;commandN;q"
打开转储文件,执行一些命令,并打印到日志文件:
cdb -z <dumpfile> -logo out.txt -lines -c "command1;command2;...;commandN;q"
需要说明一下非侵入模式(noninvasive),可以理解为不打断不影响进程运行的情况下和进程挂接。当然,这种模式下,调试器无法控制程序的运行。
2 实例应用
2.1 调试死锁
下面演示如何通过CDB来找出死锁,请做如下准备工作:
1. 编译DeadLockDemo.cpp
2. 运行编译出的exe,程序会立刻死锁
我们先通过"~*kb命令(显示所有的堆栈信息)看看都有那些线程在运行:cdb -pv -pn myapp.exe -logo out.txt -lines -c "~*kb;q"
. 0 Id: 6fc.4fc Suspend: 1 Teb: 7ffdf000 Unfrozen
ChildEBP RetAddr Args to Child
0012fdf8 7c90d85c 7c8023ed 00000000 0012fe2c ntdll!KiFastSystemCallRet
0012fdfc 7c8023ed 00000000 0012fe2c 0012ff54 ntdll!NtDelayExecution+0xc
0012fe54 7c802451 0036ee80 00000000 0012ff54 kernel32!SleepEx+0x61
0012fe64 004308a9 0036ee80 a0f63080 01c63442 kernel32!Sleep+0xf
0012ff54 00432342 00000001 003336e8 003337c8 DeadLockDemo!wmain+0xd9 [c:/tests/deadlockdemo/deadlockdemo.cpp @ 154]
0012ffb8 004320fd 0012fff0 7c816d4f a0f63080 DeadLockDemo!__tmainCRTStartup+0x232 [f:/rtm/vctools/crt_bld/self_x86/crt/src/crt0.c @ 318]
0012ffc0 7c816d4f a0f63080 01c63442 7ffdd000 DeadLockDemo!wmainCRTStartup+0xd [f:/rtm/vctools/crt_bld/self_x86/crt/src/crt0.c @ 187]
0012fff0 00000000 0042e5aa 00000000 78746341 kernel32!BaseProcessStart+0x23
1 Id: 6fc.3d8 Suspend: 1 Teb: 7ffde000 Unfrozen
ChildEBP RetAddr Args to Child
005afc14 7c90e9c0 7c91901b 000007d4 00000000 ntdll!KiFastSystemCallRet
005afc18 7c91901b 000007d4 00000000 00000000 ntdll!ZwWaitForSingleObject+0xc
005afca0 7c90104b 004a0638 00430b7f 004a0638 ntdll!RtlpWaitForCriticalSection+0x132
005afca8 00430b7f 004a0638 005afe6c 005afe78 ntdll!RtlEnterCriticalSection+0x46
005afd8c 00430b15 005aff60 005afe78 003330a0 DeadLockDemo!CCriticalSection::Lock+0x2f [c:/tests/deadlockdemo/deadlockdemo.cpp @ 62]
005afe6c 004309f1 004a0638 f3d065d5 00334fc8 DeadLockDemo!CCritSecLock::CCritSecLock+0x35 [c:/tests/deadlockdemo/deadlockdemo.cpp @ 90]
005aff6c 004311b1 00000000 f3d06511 00334fc8 DeadLockDemo!ThreadOne+0xa1 [c:/tests/deadlockdemo/deadlockdemo.cpp @ 182]
005affa8 00431122 00000000 005affec 7c80b50b DeadLockDemo!_callthreadstartex+0x51 [f:/rtm/vctools/crt_bld/self_x86/crt/src/threadex.c @ 348]
005affb4 7c80b50b 003330a0 00334fc8 00330001 DeadLockDemo!_threadstartex+0xa2 [f:/rtm/vctools/crt_bld/self_x86/crt/src/threadex.c @ 331]
005affec 00000000 00431080 003330a0 00000000 kernel32!BaseThreadStart+0x37
2 Id: 6fc.284 Suspend: 1 Teb: 7ffdc000 Unfrozen
ChildEBP RetAddr Args to Child
006afc14 7c90e9c0 7c91901b 000007d8 00000000 ntdll!KiFastSystemCallRet
006afc18 7c91901b 000007d8 00000000 00000000 ntdll!ZwWaitForSingleObject+0xc
006afca0 7c90104b 004a0620 00430b7f 004a0620 ntdll!RtlpWaitForCriticalSection+0x132
006afca8 00430b7f 004a0620 006afe6c 006afe78 ntdll!RtlEnterCriticalSection+0x46
006afd8c 00430b15 006aff60 006afe78 003332e0 DeadLockDemo!CCriticalSection::Lock+0x2f [c:/tests/deadlockdemo/deadlockdemo.cpp @ 62]
006afe6c 00430d11 004a0620 f3e065d5 00334fc8 DeadLockDemo!CCritSecLock::CCritSecLock+0x35 [c:/tests/deadlockdemo/deadlockdemo.cpp @ 90]
006aff6c 004311b1 00000000 f3e06511 00334fc8 DeadLockDemo!ThreadTwo+0xa1 [c:/tests/deadlockdemo/deadlockdemo.cpp @ 202]
006affa8 00431122 00000000 006affec 7c80b50b DeadLockDemo!_callthreadstartex+0x51 [f:/rtm/vctools/crt_bld/self_x86/crt/src/threadex.c @ 348]
006affb4 7c80b50b 003332e0 00334fc8 00330001 DeadLockDemo!_threadstartex+0xa2 [f:/rtm/vctools/crt_bld/self_x86/crt/src/threadex.c @ 331]
006affec 00000000 00431080 003332e0 00000000 kernel32!BaseThreadStart+0x37
可以看到有三个线程:主线程4fc,子线程3d8和284都在调用WaitForCriticalSection等待一个线程同步对象可用。
然后,再看看锁的列表:cdb -pv -pn myapp.exe -logo out.txt -lines -c "!locks;q"
CritSec DeadLockDemo!CritSecOne+0 at 004A0620
LockCount 1
RecursionCount 1
OwningThread 3d8
EntryCount 1
ContentionCount 1
*** Locked
CritSec DeadLockDemo!CritSecTwo+0 at 004A0638
LockCount 1
RecursionCount 1
OwningThread 284
EntryCount 1
ContentionCount 1
*** Locked
问题很清楚了,3d8和284在等待调用WaitForSingleObject等待一个线程同步对象可用时,都自己锁住了一个同步对象。两者互相等待,发生死锁。
这是一个简单的例子,在实际的应用中情况会比这复杂,但基本方法不变,具体的思路是:首先找到被锁住的线程,通过kb找到这个线程等待的同步对象,再通过!lock找到持有该同步对象的线程,顺着这个思路重复,看看最终是否线程是否能够回到最初的线程上。
如果应用程序使用了一些更复杂的同步对象(如:Mutex),调试会更复杂,在后续的文章中再讨论。
2.2 调试CPU的高消耗
要找出消耗CPU最厉害的线程:cdb -pv -pn myapp.exe -logo out.txt -c "!runaway;q"
0:000> !runaway
User Mode Time
Thread Time
1:358 0 days 0:00:47.408
2:150 0 days 0:00:03.495
0:d8 0 days 0:00:00.000
其时间为该线程自创建后所消耗的总时间,因此不能说线程358当前消耗CPU最厉害,应再来一次,观察时间增量:
0:000> !runaway
User Mode Time
Thread Time
1:358 0 days 0:00:47.408
2:150 0 days 0:00:06.859
0:d8 0 days 0:00:00.000
如此多次,可以发现消耗CPU最厉害的是线程150。
2.3 调试堆栈溢出
一般而言,堆栈溢出是由于函数的嵌套调用控制不好造成的。IDE能够很好的调试堆栈溢出。但有时,我们已经注意通过控制函数的嵌套调用来避免堆栈溢出,但堆栈溢出还是在偶尔出现,为什么呢?有某些函数在一些特定的情况下占用了过多的空间,造成了堆栈溢出。因此,我们需要知道在堆栈中函数对堆栈空间的占用情况,对此IDE没有提供简洁的方法。
操作方法:
1. 使用Debug模式编译StackOvrDemo.cpp(对这个例子,Release版本无法看到具体的函数栈)
2. 在VC中使用调试状态运行
3. 一旦异常被VC捕捉到,运行命令行:cdb -pv -pn stackovfdemo.exe -logo out.txt -c "~*kf;q"
. 0 Id: 210.3a8 Suspend: 1 Teb: 7ffde000 Unfrozen
Memory ChildEBP RetAddr
00033440 0041aca5 StackOvfDemo!_woutput+0x22
44 00033484 00415eed StackOvfDemo!wprintf+0x85
d8 0003355c 00415cc5 StackOvfDemo!ProcessStringW+0x2d
fc878 0012fdd4 00415a44 StackOvfDemo!ProcessStrings+0xe5
108 0012fedc 0041c043 StackOvfDemo!main+0x64
e4 0012ffc0 7c4e87f5 StackOvfDemo!mainCRTStartup+0x183
30 0012fff0 00000000 KERNEL32!BaseProcessStart+0x3d
可见,ProcessStrings方法占用了大量内存,最有可能是导致堆栈溢出元凶。
对于这个例子,你可能会疑惑ProcessStrings怎么能够占用如此多的堆栈内存,需要从ATL宏A2W上找原因,A2W调用了_alloca函数从栈上申请内存,这些内存只能在函数ProcessStrings退出堆栈清除时才能释放。因此,应避免在循环中调用A2W。
2.4 生成转储文件(Dump)
如果程序带着未知的Bug发布出去,怎么调试? 因此,在某些情况下,我们需要生成转储文件,通过转储文件来分析。我们使用可以CDB/Dr.Waton/dbghelp接口/Windgb/XP任务管理器等等方法生成转储文件。
使用CDB的方法:cdb -pv -pn myapp.exe -c ".dump /m c:/myapp.dmp;q"
选项 | 描述 | 举例 |
/m | 缺省选项,生成标准的minidump, 转储文件通常较小,便于在网络上通过邮件或其他方式传输,当然这种文件的信息量较少,之包含:系统信息、加载的模块(DLL)信息、 进程信息和线程信息。 | .dump /m c:/myapp.dmp |
/ma | 带有尽量多选项的minidump(包括完整的内存内容、句柄、未加载的模块,等等),文件很大,可用于本地调试。 | .dump /ma c:/myapp.dmp |
/mFhutwd | 带有数据段、非共享的读/写内存页和其他有用的信息的minidump。包含了通过minidump能够得到的最多的信息。 | .dump /mFhutwd c:/myapp.dm |
如果你要为一个正在被IDE调试的进程创建转储文件,记得先使所有断点暂时失效。如果不这样做,转储文件中将带有所有断点指令(int 3)。
2.5 分析转储文件
一般而言,我们分析转储文件希望得到下列信息::
- 异常发生的地方 (地址、源码文件和代码行)
- 异常发生时的调用堆栈
- 调用堆栈上函数参数和本地变量的值
windgb和CDB都提供一个强大的命令!analyze –v来分析转储文件:cdb -z c:/myapp.dmp -logo out.txt -lines -c "!analyze -v;q"
CrashDemo.cpp演示了如何通过dbghelp接口实现自定义过滤器为异常创建转储文件。功能更完备的dbghelp接口封装在我(译者)其他的文章中将会讨论。
0:001> !analyze -v
*******************************************************************************
* *
* Exception Analysis *
* *
*******************************************************************************
FAULTING_IP:
CrashDemo!TestFunc+2e [c:/tests/crashdemo/crashdemo.cpp @ 124]
004309de c70000000000 mov dword ptr [eax],0x0
EXCEPTION_RECORD: ffffffff -- (.exr ffffffffffffffff)
.exr ffffffffffffffff
ExceptionAddress: 004309de (CrashDemo!TestFunc+0x0000002e)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 00000001
Parameter[1]: 00000000
Attempt to write to address 00000000
DEFAULT_BUCKET_ID: APPLICATION_FAULT
PROCESS_NAME: CrashDemo.exe
ERROR_CODE: (NTSTATUS) 0xc0000005 - The instruction at "0x%08lx" referenced memory at "0x%08lx". The memory could not be "%s".
WRITE_ADDRESS: 00000000
BUGCHECK_STR: ACCESS_VIOLATION
LAST_CONTROL_TRANSFER: from 0043096e to 004309de
STACK_TEXT:
006afe88 0043096e 00000000 00354130 00350001 CrashDemo!TestFunc+0x2e [c:/tests/crashdemo/crashdemo.cpp @ 124]
006aff6c 00430f31 00000000 52319518 00354130 CrashDemo!WorkerThread+0x5e [c:/tests/crashdemo/crashdemo.cpp @ 115]
006affa8 00430ea2 00000000 006affec 7c80b50b CrashDemo!_callthreadstartex+0x51 [f:/rtm/vctools/crt_bld/self_x86/crt/src/threadex.c @ 348]
006affb4 7c80b50b 00355188 00354130 00350001 CrashDemo!_threadstartex+0xa2 [f:/rtm/vctools/crt_bld/self_x86/crt/src/threadex.c @ 331]
006affec 00000000 00430e00 00355188 00000000 kernel32!BaseThreadStart+0x37
FOLLOWUP_IP:
CrashDemo!TestFunc+2e [c:/tests/crashdemo/crashdemo.cpp @ 124]
004309de c70000000000 mov dword ptr [eax],0x0
SYMBOL_STACK_INDEX: 0
FOLLOWUP_NAME: MachineOwner
SYMBOL_NAME: CrashDemo!TestFunc+2e
MODULE_NAME: CrashDemo
IMAGE_NAME: CrashDemo.exe
DEBUG_FLR_IMAGE_TIMESTAMP: 43dc6ee7
STACK_COMMAND: .ecxr ; kb
FAILURE_BUCKET_ID: ACCESS_VIOLATION_CrashDemo!TestFunc+2e
BUCKET_ID: ACCESS_VIOLATION_CrashDemo!TestFunc+2e
Followup: MachineOwner
注意看粗体字部分(异常发生的地址、调用堆栈信息、进一步分析异常的命令.ecxr和kb)。
通过.ecxr,我们可以切换到记录了异常信息的向下文中,这样,我们就能够访问到异常发生时调用堆栈和本地变量的值。我们可使用dv命令显示函数参数和本地变量的值。
cdb -z c:/myapp.dmp -logo out.txt -lines -c "!analyze -v;.ecxr;!for_each_frame dv /t;q"
/t 选项告诉dv命令显示变量的类型信息,输入如例:
00 006afe88 0043096e CrashDemo!TestFunc+0x2e [c:/tests/crashdemo/crashdemo.cpp @ 124]
int * pParam = 0x00000000
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
01 006aff6c 00430f31 CrashDemo!WorkerThread+0x5e [c:/tests/crashdemo/crashdemo.cpp @ 115]
void * lpParam = 0x00000000
int * TempPtr = 0x00000000
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
02 006affa8 00430ea2 CrashDemo!_callthreadstartex+0x51 [f:/rtm/vctools/crt_bld/self_x86/crt/src/threadex.c @ 348]
struct _tiddata * ptd = 0x00355188
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
03 006affb4 7c80b50b CrashDemo!_threadstartex+0xa2 [f:/rtm/vctools/crt_bld/self_x86/crt/src/threadex.c @ 331]
void * ptd = 0x00355188
struct _tiddata * _ptd = 0x00000000
.
2.6 虚拟内存分析
下列命令可以显示出进程的整个虚拟内存图:cdb -pv -pn myapp.exe -logo out.txt -c "!vadump -v;q"
BaseAddress: 00040000
AllocationBase: 00040000
AllocationProtect: 00000004 PAGE_READWRITE
RegionSize: 0002e000
State: 00002000 MEM_RESERVE
Type: 00020000 MEM_PRIVATE
BaseAddress: 0006e000
AllocationBase: 00040000
AllocationProtect: 00000004 PAGE_READWRITE
RegionSize: 00001000
State: 00001000 MEM_COMMIT
Protect: 00000104 PAGE_READWRITE + PAGE_GUARD
Type: 00020000 MEM_PRIVATE
BaseAddress: 0006f000
AllocationBase: 00040000
AllocationProtect: 00000004 PAGE_READWRITE
RegionSize: 00011000
State: 00001000 MEM_COMMIT
Protect: 00000004 PAGE_READWRITE
Type: 00020000 MEM_PRIVATE
XP和2003系统上,有一个更帅的命令!address,可执行下列任务:
- 显示出进程的整个虚拟内存图,可能会比vadump更可靠
- 显示虚拟内存的耗用情况统计
- 判断某个地址属于哪一个虚拟内存区 (如,判断该地址是否属于栈、堆还是可执行镜像)
通过!address显示虚拟内存图:
cdb -pv -pn myapp.exe -logo out.txt -c "!address;q"
00040000 : 00040000 - 0002e000
Type 00020000 MEM_PRIVATE
Protect 00000000
State 00002000 MEM_RESERVE
Usage RegionUsageStack
Pid.Tid 658.644
0006e000 - 00001000
Type 00020000 MEM_PRIVATE
Protect 00000104 PAGE_READWRITE | PAGE_GUARD
State 00001000 MEM_COMMIT
Usage RegionUsageStack
Pid.Tid 658.644
0006f000 - 00011000
Type 00020000 MEM_PRIVATE
Protect 00000004 PAGE_READWRITE
State 00001000 MEM_COMMIT
Usage RegionUsageStack
Pid.Tid 658.644
同时显示虚拟内存耗用情况统计:
-------------------- Usage SUMMARY --------------------------
TotSize Pct(Tots) Pct(Busy) Usage
00838000 : 0.40% 27.96% : RegionUsageIsVAD
7e28c000 : 98.56% 0.00% : RegionUsageFree
01348000 : 0.94% 65.60% : RegionUsageImage
00040000 : 0.01% 0.85% : RegionUsageStack
00001000 : 0.00% 0.01% : RegionUsageTeb
001a0000 : 0.08% 5.53% : RegionUsageHeap
00000000 : 0.00% 0.00% : RegionUsagePageHeap
00001000 : 0.00% 0.01% : RegionUsagePeb
00001000 : 0.00% 0.01% : RegionUsageProcessParametrs
00001000 : 0.00% 0.01% : RegionUsageEnvironmentBlock
Tot: 7fff0000 Busy: 01d64000
-------------------- Type SUMMARY --------------------------
TotSize Pct(Tots) Usage
7e28c000 : 98.56% : <free>
01348000 : 0.94% : MEM_IMAGE
007b6000 : 0.38% : MEM_MAPPED
00266000 : 0.12% : MEM_PRIVATE
-------------------- State SUMMARY --------------------------
TotSize Pct(Tots) Usage
01647000 : 1.09% : MEM_COMMIT
7e28c000 : 98.56% : MEM_FREE
0071d000 : 0.35% : MEM_RESERVE
Largest free region: Base 01014000 - Size 59d5c000
在有内存泄漏时,内存耗用情况统计很有用,可用来判断究竟是栈、堆还是虚拟内存在泄漏。同时最大空闲区(Largest free region)有助于我们开发要消耗大量内存的应用程序。
判断某个地址属于哪一个虚拟内存区
0:000> !address 0x000a2480;q
000a0000 : 000a0000 - 000d7000
Type 00020000 MEM_PRIVATE
Protect 00000004 PAGE_READWRITE
State 00001000 MEM_COMMIT
Usage RegionUsageHeap
Handle 000a0000
2.7 查找符号
有时,我们需要通过名字来查找某个函数或变量的地址,CDB可帮忙。
如,定位kernel32模块中的UnhandledExceptionFilter函数:
0:000> x kernel32!UnhandledExceptionFilter;q
7c862b8a kernel32!UnhandledExceptionFilter = <no type information>
你也可以通过通配符来查找,如:
0:000> x myapp!*CMainFrame*
004542f8 MyApp!CMainFrame::classCMainFrame = struct CRuntimeClass
00401100 MyApp!CMainFrame::`scalar deleting destructor' (void)
00401090 MyApp!CMainFrame::CMainFrame (void)
也可以通过地址得到名称(使用ln命令),如:
0:000> ln 0x77d491c8;q
(77d491c6) USER32!GetMessageW+0x2 | (77d49216) USER32!CharUpperBuffW
注意:输入的地址不需要是首地址
2.8 显示数据结构
Visual Studio的观察(watch)窗口可以看到数据结构,而CDB和Windbg可以看的更多(包括偏移和布局)。通过dt命令实现,如:
cdb -pv -pn myapp.exe -logo out.txt -c "dt -b CSymbolInfoPackage;q"
0:000> dt /b CSymbolInfoPackage;q
+0x000 si : _SYMBOL_INFO
+0x000 SizeOfStruct : Uint4B
+0x004 TypeIndex : Uint4B
+0x04c NameLen : Uint4B
+0x050 MaxNameLen : Uint4B
+0x054 Name : Char
+0x058 name : Char
也可以显示数据结构的实例变量的布局信息,需要传入该变量的地址,如:
cdb -pv -pn myapp.exe -logo out.txt -c "dt -b CSymbolInfoPackage 0x0012f6d0;q"
0:000> dt /b CSymbolInfoPackage 0x0012f6d0;q
+0x000 si : _SYMBOL_INFO
+0x000 SizeOfStruct : 0x58
+0x004 TypeIndex : 2
+0x008 Reserved :
[00] 0
[01] 0
+0x038 Address : 0x411d30
[00] 83 'S'
+0x058 name : "SymbolInfo"
[00] 83 'S'
[01] 121 'y'
[02] 109 'm'
[03] 98 'b'
[04] 111 'o'
[05] 108 'l'
[06] 73 'I'
[07] 110 'n'
[17] 0 ''
...
[1998] -52 ''
[1999] -52 ''
[2000] -52 ''