Windows程序调试系列文章——Windbg轻松上路

 转自 http://www.cnblogs.com/itrust/archive/2006/08/18/480692.html       
    摘译自  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 ''

你可能感兴趣的:(Windows程序调试系列文章——Windbg轻松上路)