摘译自 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 ''