感谢博主 http://book.51cto.com/art/200711/59731.htm
《Windows用户态程序高效排错》第二章主要介绍用户态调试相关的知识和工具。本文主要讲了排错的工具:调试器Windbg。
第二章 汇编、异常、内存、同步和调试器
——重要的知识点和神兵利器
这一部分主要介绍用户态调试相关的知识和工具。包括:汇编、异常(exception)、内存布局、堆(heap)、栈(stack)、CRT(C Runtime)、handle/Criticalsection/thread context/windbg/ dump/live debug和Dr Watson等。
书中不会对知识点作全面的介绍,而是针对知识点在调试中过程中应该如何使用进行说明。知识点本身在下面两本书中有非常详细的介绍:
Programming Applications for Microsoft Windows |
2.1 排错的工具:调试器Windbg
本节介绍调试器Windbg的相关知识。Windbg的使用贯穿本书很多章节,它是分析问题的高效工具。
Windbg的下载地址是:
Install Debugging Tools for Windows 32-bit Version
http://www.microsoft.com/whdc/devtools/debugging/installx86.mspx
建议安装到C:\Debuggers目录,后面的例子默认用这个目录。
开发人员写完代码,通常会在Visual Studio中按F5直接进行调试。使用Visual Studio自带调试器能够非常方便地在源代码上设定断点,检查程序的中间变量,单步骤执行。在完成代码阶段,Visual Studio自带的调试器能够非常方便地做源代码级别的排错。
Visual Studio调试器的典型用例是源代码级别的排错。与其相比,Windbg并不是一款针对特殊用例的调试器。Windbg提供了一个GUI界面,也可以在源代码上直接用F5设定断点,但更多的情况下,调试人员会直接用文本的方式输入调试命令,Windbg执行对应的操作,用文本的方式返回对应的结果。Windbg的调试命令覆盖了Windows平台提供的所有调试功能。
本节首先对调试器和符号文件作大致的介绍,然后针对常用的Windbg调试命令作演示。接下来介绍Windbg中强大而灵活的条件断点,最后介绍调试器目录下的相关工具。
对调试器深入的了解后,相信读者就能体会到Windbg和Visual Studio调试器设计上的区别,选用最合适的调试器来解决问题。
书中不会从Windbg的基本使用方法说起,而是着重介绍调试器原理,常用的命令,Windbg的高级用法和相关的工具。如果读者从来没有使用过Windbg,下面的文章可以提供帮助:
DebugInfo:
http://www.debuginfo.com/
Windows Debuggers: Part 1: A WinDbg Tutorial
http://www.codeproject.com/debug/windbg_part1.asp
2.1.1 调试器的功能:检查代码和资料,保存dump文件,控制程序的执行
调试器,无论是Visual Studio调试器还是Windbg,都是用来观察和控制目标进程的工具。对于用户态的进程,调试器可以查看用户态内存空间和寄存器上的资料。对于不同类型的数据和代码,调试器能方便地把这些信息用特定的格式区分和显示出来。调试器还可以把一个目标进程某一时刻的所有信息写入一个文件(dump),直接打开这个文件分析。调试器还可以通过设置断点的机制来控制目标程序什么时候停下来接受检查,什么时候继续运行。
关于调试器的工作原理,请参考Debugging Applications for Windows这本书。
Windbg及其相关工具的下载地址:
http://www.microsoft.com/whdc/devtools/debugging/installx86.mspx
在安装好Windbg后,可以在windbg.exe的主窗口按F1弹出帮助。这是了解和使用Windbg的最好文档。每个命令的详细说明,都可以在里面找到。
调试器可以直观地看到下面一些信息:
进程运行的状态和系统状态,比如进程运行了多少时间,环境变量是什么。
当前进程加载的所有EXE/DLL的详细信息。
某一个地址上的汇编指令。
查看内存地址的内容和属性,比如是否可写。
每个的call stack(需要symbol)。
Call stack上每个函数的局部变量。
格式化地显示程序中的数据结构(需要symbol)。
查看和修改内存地址上的资料或者寄存器上的资料。
部分操作系统管理的数据结构,比如Heap、Handle、CriticalSection等。
在Visual Studio调试器中,要查看上面的信息,需要在很多调试窗口中切换。而在Windbg中,只需要简单的命令就可以完成。
调试器的另外一个作用是设定条件断点。可以设定在某一个指令地址上停下来,也可以设定当某一个内存地址等于多少的时候停下来,或者当某一个exception/notification发生的时候停下来。还可以进入一个函数调用的时候停下来,或跳出当前函数调用的时候停下来。停下来后可以让调试器自动运行某些命令,记录某些信息,然后让调试器自动判断某些条件来决定是否要继续运行。通过简单的条件断点功能,可以很方便地实现下面一些任务:
当某一个函数被调用的时候,在调试器输出窗口中打印出函数参数。
计算某一个变量被修改了多少次。
监视一个函数调用了哪些子函数,分别被调用了多少次。
每次抛C++异常的时候自动产生dump文件。
在Visual Studio调试器中也能够设定条件断点,但灵活性和功能远不能跟Windbg相比。
2.1.2 符号文件(Symbol file),把二进制和源代码对应起来
当用VC/VB编译生成EXE/DLL后,往往会同时生成PDB文件。PDB里面包含的是EXE/DLL的符号信息。
符号是指代码中使用到的类型和名字。比如下面这些都是符号包含的内容:
代码所定义的Class的名字,Class的所有成员的名字和所有成员的类型。
变量的名字和变量的类型。
函数的名字,函数所有参数的名字和类型,以及函数的返回值。
PDB文件除了包含符号外,还负责把符号和该符号所处的二进制地址联系起来。比如有一个全局变量叫做gBuffer,PDB文件不仅仅记录了gBuffer的类型,还能让调试器找到保存gBuffer的内存地址。
有了符号文件,当在调试器中试图读取某一个内存地址的时候,调试器会尝试在对应的PDB文件中配对,看这个内存地址是否有符号对应。如果能够找到,调试器就可以把对应的符号显示出来。这样,极大程度上方便了开发人员的观察。 对于操作系统EXE/DLL,微软也提供了对应的符号文件下载地址。
默认情况下,符号文件中包含了所有的结构、函数,以及对应的源代码信息。微软提供的Windows符号文件去掉了源代码信息、函数参数定义和一些内部数据结构的定义。
2.1.3 一个简单的上手程序
接下来用一个简单的例子演示一下Windbg的基本使用。下面这段代码的目的是把字符串"6969,3p3p"中的所有3都修改为4。
#include "stdafx.h" |
这段牖岬贾卤览!1览:罂吹降慕涌谌缤?.1所示。
图2.1 |
接下来,一起用Windbg来看看上述对话框的具体含义是什么。
在启动Windbg调试以前,首先把程序对应的PDB文件放到一个指定的文件夹。上面程序的EXE叫做crashscreen-shot.exe,把编译时候生成的crashscreen-shot.pdb文件拷贝到C:\PDB文件夹。同时把程序的主CPP文件拷贝到C:\SRC文件夹。
接下来启动Windbg。像用Visual Studio调试程序一样,我们需要在调试器中运行对应的EXE。所以在Windbg的主窗口中,使用File→Open Executable菜单找到crashscreen-shot.exe,然后打开。
Windbg不会让目标进程立刻开始运行。相反,Windbg这时会停下来,让用户有机会对进程启动过程进行排错,或者进行一些准备工作,比如设定断点,如图2.2所示。
图2.2 |
2.1.4 用Internet Explorer来操练调试器的基本命令
下面用Internet Explorer作为目标进程演示Windbg中更多的调试命令。
用Windbg来调试目标进程,有两种方法,分别是通过调试器启动,和用调试器直接监视(attach)正在运行的进程。
通过File→Open Executable菜单,可以选择对应的EXE在调试器中启动。通过File→Attach to a process可以选择一个正在运行的进程进行调试。
打开IE,访问www.msdn.com, 然后启动Windbg,按F6,选择刚刚启动的(最下面)iexplorer.exe进程。
IE的PDB文件也需要从微软的网站上下载。具体做法请参考上一节的链接。在我本地,我的symbol路径设定如下:
SRV*D:\websymbols*http://msdl.microsoft.com/download/symbols;D:\MyAppSymbol |
这里的D:\websymbols目录是用来保存从msdl.microsoft.com上自动下载的操作系统符号文件。而我自己编译生成的符号文件,我都手动拷贝到D:\MyAppSymbol路径下。
接下来,在Windbg的命令窗口中(如果看不到可以用Alt+1打开),运行下面命令。
vertarget检查进程概况 |
上面的0:026>是命令提示符,026表示当前的线程ID。后面会介绍切换线程的命令,到时候就可以看到提示符的变化。
跟大多数的命令输出一样,vertarget的输出非常明白直观,显示当前系统的版本和运行时间。
!peb 显示Process Environment Block |
接着可以用!peb命令来显示Process Environment Block。由于输出太长,这里就省略了。
lmvm 检查模块的加载信息
用lmvm命令可以看任意一个DLL/EXE的详细信息,以及symbol的情况:
0:026> lmvm msvcrt |
命令的第二行显示deferred,表示目前并没有加载msvcrt的symbol,可以用.reload命令来加载。在加载前,可以用!sym命令来打开symbol加载过程的详细输出:
.reload / !sym 加载符号文件 |
默认情况下,调试器不会加载所有的symbol文件。只有某个调试器命令需要使用symbol的时候,调试器才在设定的符号文件路径中检查和加载。!sym命令可以让调试器在自动寻找symbol的时候给出详细的信息,比如搜索和下载的路径。.reload命令可以让调试器加载指定模块的symbol。
0:026> !sym noisy |
可以看到,symbol从msdl.microsoft.com自动下载后加载。
lmf 列出当前进程中加载的所有模块
lmf命令可以列出当前进程中加载的所有DLL文件和对应的路径:
0:018> lmf |
r,d,e 寄存器,内存的检查和修改
r命令显示和修改寄存器上的值。
d命令显示内存地址上的值。
e命令修改内存地址上的值。
显示寄存器:
0:018> r |
如果需要修改寄存器,比如把eax的值修改为0x0,可以用 r eax=0。
用d命令显示esp 寄存器指向的内存,默认为byte格式。
0:018> d esp |
用dd命令直接指定054efc14 地址,第二个d表示用DWORD格式。除了DWORD外,还有db(byte),du(Unicode),dc(char)等等。详细信息请参考帮助文档中d命令的说明。
0:018> dd 054efc14 |
e命令可以用来修改内存地址。跟d命令一样,e命令后面也可以跟类型后缀。比如ed命令表示用DWORD的方式修改。下面的命令把054efc14 地址上的值修改为11112222。
0:018> ed 054efc14 11112222 |
修改完成后,用dd命令检查054efc14 地址上的值。后面的 L4参数指定内存区间的长度长度为4个DWORD。这样输出就只有1行,而不是默认的8行。
0:018> dd 054efc14 L4 |
有了上面几个命令,就可以访问和修改当前进程中的所有内存。这些命令的参数和格式非常灵活,详细内容参考帮助文档。
!address显示内存页信息 |
该命令在前面的例子中就用到过,可以显示某一个地址上的页信息:
0:001> !address 7ffde000 |
如果不带参数,可以显示更详细的统计信息。
S 搜索内存
S命令可以搜索内存。比如用在下面的地方:
1. 寻找内存泄漏的线索。比如知道当前内存泄漏的内容是一些固定的字符串,就可以在 DLL区域搜索这些字符串出现的地址,然后再搜索这些地址用到什么代码中,找出这些内存是从什么地方开始分配的。
2. 寻找错误代码的根源。比如知道当前程序返回了0x80074015这样的一个代码,但是不知道这个代码是由哪一个内层函数返回的,就可以在代码区搜索0x80074015,找到可能返回这个代码的函数。
下面就是访问sina.com的时候,用Windbg搜索ie里面www.sina.com.cn的结果:
0:022> s -u 0012ff40 L?80000000 "www.sina.com.cn" |
结合S命令和前面介绍的修改内存命令,根本不需要用什么金山游侠就可以查找/修改游戏中主角的生命了 :-)
接下来,看看跟线程相关的命令。
!runaway 检查线程的CPU消耗
!runaway可以显示每一个线程所耗费usermode CPU时间的统计信息:
0:001> !runaway |
上面输出的第一列是线程编号和线程id。后一列对应的是该线程在用户态模式中的总繁忙时间。在该命令加上f参数,还可以看到内核态的繁忙时间。当进程内存占用率高的时候,通过该命令可以方便地找到对应的繁忙线程。
~ 切换目标线程
用~命令,可以显示线程信息和在不同线程之间切换:
0:001> ~ |
上面的~0s命令,把当前线程切换到0号线程,也就是主线程。切换后提示符会变为0:000。
k,kb,kp,kv,kn 检查call stack
k命令显示当前线程的call stack。跟d命令一样,k后面可以跟很多后缀,比如kb、kp、kn、kv、kL等。这些后缀控制了显示的格式和信息量。具体信息请参考帮助文档和动手实践。
0:000> k |
可以结合~和k命令,来显示所有线程的call stack. 输入~*k试一下。
u 反汇编
u命令把指定地址上的代码翻译成汇编输出。
0:000> u 7739d023 |
如果符号文件加载正确,可以用uf命令直接反汇编整个函数,比如uf USER32! NtUserWaitMessage。
x 查找符号的二进制地址
有了符号文件,调试器就能查找源代码符号和该符号所处的二进制地址之间的映射。如果要找一个符号保存在什么二进制地址上,可以用x命令:
0:000> x msvcrt!printf |
上面的命令找到了printf函数的入口地址在77bd27c2。
0:001> x ntdll!GlobalCounter |
上面的命令表示ntdll!GlobalCounter这个变量保存的地址是7c99f72c。
(注意: 符号对应的是变量和变量所在的地址,不是变量的值。上面的命令不是说ntdll!GlobalCounter这个变量的值是7c99f72c。要找到变量的值,需要用d命令读取内存地址来获取。)
x命令还支持通配符,比如 x ntdll!*命令列出ntdll模块中所有的符号,以及对应的二进制地址。由于输出太长,这里就省略了。
dds 对应二进制地址的符号
dds打印内存地址上的二进制值,同时自动搜索二进制值对应的符号。比如要看看当前stack 中保存了哪些函数地址,就可以检查ebp指向的内存:
0:000> dds ebp |
这里dds命令从ebp指向的内存地址0013ed98 开始打印。第1列是内存地址的值,第2列是地址上对应的二进制数据,第3列是二进制数据对应的符号。上面的命令自动找到了75ecb30f对应的符号是BROWSEUI!BrowserProtectedThreadProc+0x44。
COM Interface和C++ Vtable里面的成员函数都是顺序排列的,所以dds命令可以方便地找到虚函数表中具体的函数地址。比如用下面的命令可以找到OpaqueDataInfo类型中虚函数对应的实际函数地址。
首先用x命令找到OpaqueDataInfo虚函数表地址:
0:000> x ole32!OpaqueDataInfo::`vftable' |
接下来dds命令可以打印出虚函数表中的函数名字:
0:000> dds 7768265c 7768265c 77778245 ole32!ServerLocationInfo::QueryInterface 77682660 77778254 ole32!ScmRequestInfo::AddRef 77682664 77778263 ole32!ScmRequestInfo::Release 77682668 77779d26 ole32!OpaqueDataInfo::Serialize 7768266c 77779d3d ole32!OpaqueDataInfo::UnSerialize 77682670 77779d7a ole32!OpaqueDataInfo::GetSize 77682674 77779dcb ole32!OpaqueDataInfo::GetCLSID 77682678 77779deb ole32!OpaqueDataInfo::SetParent 7768267c 77779e18 ole32!OpaqueDataInfo::SerializableQueryInterface 77682680 777799b5 ole32!InstantiationInfo::QueryInterface 77682684 77689529 ole32!ServerLocationInfo::AddRef 77682688 776899cc ole32!ScmReplyInfo::Release 7768268c 77779bcd ole32!OpaqueDataInfo::AddOpaqueData 77682690 77779c43 ole32!OpaqueDataInfo::GetOpaqueData 77682694 77779c99 ole32!OpaqueDataInfo::DeleteOpaqueData 77682698 776a8cf6 ole32!ServerLocationInfo::GetRemoteServerName 7768269c 776aad96 ole32!OpaqueDataInfo::GetAllOpaqueData 776826a0 77777a3b ole32!CDdeObject::COleObjectImpl::GetClipboardData 776826a4 00000021 776826a8 77703159 ole32!CClassMoniker::QueryInterface 776826ac 77709b01 ole32!CErrorObject::AddRef 776826b0 776edaff ole32!CClassMoniker::Release 776826b4 776ec529 ole32!CClassMoniker::GetUnmarshalClass 776826b8 776ec546 ole32!CClassMoniker::GetMarshalSizeMax 776826bc 776ec589 ole32!CClassMoniker::MarshalInterface 776826c0 77702ca9 ole32!CClassMoniker::UnmarshalInterface 776826c4 776edbe1 ole32!CClassMoniker::ReleaseMarshalData 776826c8 776e5690 ole32!CDdeObject::COleItemContainerImpl::LockContainer 776826cc 7770313b ole32!CClassMoniker::QueryInterface 776826d0 7770314a ole32!CClassMoniker::AddRef 776826d4 776ec5a8 ole32!CClassMoniker::Release 776826d8 776ec4c6 ole32!CClassMoniker::GetComparisonData |
2.1.5 检查程序资料的小例子
下面用一个小例子来演示如何具体地观察程序中的数据结构。
首先在debug模式下编译并且按Ctrl+F5运行下面的代码:
struct innner { char arr[10]; }; class MyCls { private: char* str; innner inobj; public: void set(char* input) { str=input; strcpy(inobj.arr,str); } int output() { printf(str); return 1; } void hold() { getchar(); } }; void foo1() { MyCls *pcls=new MyCls(); void *rawptr=pcls; pcls->set("abcd"); pcls->output(); pcls->hold(); }; void foo2() { printf("in foo2\n"); foo1(); }; void foo3() { printf("in foo3\n"); foo2(); }; int _tmain(int argc, _TCHAR* argv[]) { foo3(); return 0; } |
当console等待输入的时候,启动Windbg,然后用F6加载目标进程。
用~0s命令切换到主线程,查看callstack:
0:000> knL |
.frame 在栈中切换以便检查局部变量
上面callstack中每一行前面的序号叫做frame number。通过.frame命令,可以切换到对应的函数中检查局部变量。比如exceptioninject!foo1 这个函数前面的 frame number是d,于是执行.frame d命令:
0:000> .frame d |
x命令显示当前frame的局部变量。在foo1函数中,两个局部变量分别是pcls和rawptr:
0:000> x |
dt 格式化显示资料
在符号文件加载的情况下,dt命令格式化显示变量的资料和结构:
0:000> dt pcls |
上面的命令打印出pcls的类型是MyCls指针,指向的地址是0x0039ba80,其中的两个class成员的偏移分别在+0和+4,对应的值在第2列显示。加上-b -r参数可以显示inner class和数组的信息:
0:000> dt pcls -b -r Local var @ 0x12fce4 Type MyCls* 0x0039ba80 +0x000 str : 0x00416648 "abcd" +0x004 inobj : innner +0x000 arr : "abcd" [00] 97 'a' [01] 98 'b' [02] 99 'c' [03] 100 'd' [04] 0 '' [05] 0 '' [06] 0 '' [07] 0 '' [08] 0 '' [09] 0 '' |
对于任意的地址,也可以手动指定符号类型来格式化显示。比如把0x0039ba80地址上的数据用MyCls类型来显示:
0:000> dt 0x0039ba80 MyCls |
2.1.6 用Windbg控制程序进行实时调试(Live Debug)
除了检查静态资料外,调试器还能够控制和观察代码的执行。
1. wt命令
wt命令的作用是watch and trace data。 它可以跟踪一个函数的所有执行过程,并且给出统计信息。
2. 设定断点
Windbg里面可以设定灵活而强大的条件断点。比如可以通过条件断点实现这样的功能:当某个全局变量被修改100次以后,同时stack上的第2个参数是100,那么就停下来进入调试模式;如果第2个参数是200,那么就生成1个dump文件,否则就只打印出当前的callstack,然后继续运行。
Wt Watch and Trace, 跟踪执行的强大命令
还是对于上面那个程序。
首先用bp (break point) 命令在foo3上面设断点:
0:001> bp exceptioninject!foo3 |
然后用g命令让程序执行:
0:001> g |
执行到foo3上的时候,调试器停下来了:
Breakpoint 0 hit |
用bd(breakpoint disable)命令取消设定好的断点,以免打扰wt的执行:
0:000> bd 0 |
用wt命令监视foo3的执行,深度设定成2(-l2参数):
0:000> wt -l2 1107 instructions were executed in 1106 events (0 from other threads) Function Name Invocations MinInst MaxInst AvgInst 0 system calls were executed eax=00000073 ebx=7ffd7000 ecx=00437c7e edx=10310bd0 esi=0012fe9c edi=0012ff68 |
上面wt命令一直监视到foo3函数执行完为止。随着函数的执行,Windbg打印出foo3调用过的子函数。如果需要更详细的信息,可以调整深度参数的值。
wt命令最后给出统计信息。无论是观察函数执行过程和分支,或者是评估性能,wt命令都是很有帮助的。
断点和条件断点 (condition breakpoint),高效地控制观测目标
Windbg中的断点分为3种,命令格式和功能如下:
1. bp+地址/函数名字可以在某个地址上设定断点。当程序运行到这个地址的时候断点触发。
2. ba (break on access)用来设定访问断点,在某个地址被读/写的时候断点触发。
3. Exception断点。当发生某个Exception/Notification的时候断点触发。详情请参考Windbg帮助中的sx(Set Exception)小结。
第1种格式前面已经实践过。第2种格式的断点也很简单。比如程序有一个全局变量,符号是testapp!g_Buffer。要想在程序修改这个变量的时候停下来,可以使用下面的命令设定断点:
ba w4 testapp!g_Buffer |
上面的w4表示需要检查的类型和长度。W4中的W表示类型为写(Write),4表示长度为4字节。testapp!g_Buffer是符号的名字,调试器会自动转换成该符号所在的内存地址。所以该断点的作用是:监视一块内存地址区域,起点是testapp!g_Buffer所在的地址,长度为4字节。当有代码对该块地址任意位置有写操作发生的时候,调试器就把程序断下来。
其实设置ba断点的原理很简单。在设置断点后,调试器通过API把所监视地址的页面属性改为不可访问。这样当有代码访问这块地址的时候,就会引起访问异常。这样调试器就可以监视内存的读写操作,作出相应判断。
第3种命令用来监视异常。调试器能捕获程序中所有的异常,但是并不是说任何异常发生的时候调试器就一定要把程序断下来。调试人员可以通过sx命令来指定异常发生时候的对应操作。下面3条命令达到的效果是,当Access Violation异常发生的时候,调试器就停下来。当DLL Load事件发生的时候,调试器就只是在屏幕上输出。当C++ exception发生的时候,调试器什么都不做。
Sxe av |
关于异常的详细说明,在后面的小结有详细介绍。
条件断点(condition breakpoint)的是指在上面3种基本断点停下来后,执行一些自定义的判断。详细说明参考Windbg帮助中的Setting a Conditional Breakpoint小结。
在基本断点命令后加上自定义调试命令,可以让调试器在断点触发停下来后,执行调试器命令。每个命令之间用分号分割。
下面这个命令,在exceptioninject!foo3上设断点,每次断下来后,先用k显示callstack,然后用.echo命令输出简单的字符串‘breaks’,最后g命令继续执行:
0:001> bp exceptioninject!foo3 "k;.echo 'breaks';g" |
更复杂一点的例子是:
int i=0; while(1) |
条件断点的命令是:
ba w4 exceptioninject!i "j (poi(exceptioninject!i)<0n40) ' |
首先ba w4 exceptioninject!i表示在修改exceptioninject!i这个全局变量的时候停下来。j(judge)命令的作用是对后面的表达式作条件判断,如果为true,执行第1个单引号里面的命令,否则执行第2个单引号里面的命令。
条件表达式是(poi“exceptioninject!i”<0n40)。在Windbg中,exceptioninject!i符号表示符号所在的内存地址,而不是符号的数值,相当于C语言中的 &操作符的作用。Windbg命令poi的作用是取这个地址上的值,相当于C语言中的*操作符。所以这个条件的意思就是判断exceptioninject!i的值,是否小于十进制(Windbg中十进制用0n当前缀)的40。
如果为真,那么就执行第1个单引号:
printf \"exceptioninject!i value is:%d\",poi(exceptioninject!i);.echo;g |
这一个单引号里面有3个命令:.printf、.echo 和g,这里的printf语法跟C中printf函数语法一样。不过由于这个printf命令本身是在ba命令的双引号里面,所以需要用\来转义printf中的引号。转义的结果是:
printf “exceptioninject!i valus is %d”, poi(exceptioninject!i) |
所以第1个单引号命令的作用是:
1)打印出当前exceptioninject!i的值;2).echo命令换行;3)g命令继续执行。
如果为假,那么就执行第2个单引号:.echo stop! 这个命令就是显示stop,由于后面没有g命令,所以windbg会停下。运行输出如下:
0:001> ba w4 exceptioninject!i "j (poi(exceptioninject!i)<0n40) ' |
伪寄存器,帮助保存调试的中间信息
考虑这样的情况,如果要记录某一个函数被执行了多少次,应该怎么做?简单的做法就是修改代码,在对应的函数入口做记录。可是,如果要记录的函数是系统API呢?
下面的命令可以统计VirtualAllocEx被执行了多少次:
bp /1 /c @$csp @$ra;g |
bp kernel32!VirtualAllocEx "r $t0=@$t0+1;.printf \"function executes: %d times \",@$t0;.echo;g"
这里用到的$t0就是Windbg提供的伪寄存器。可以用来存储中间信息。这里用它来存储函数执行的次数。r命令可以用来查看,修改寄存器(CPU寄存器和Windbg的伪寄存器都有效)的值。随便挑一个繁忙的进程,用这个命令设定断点后观察:
0:009> bp kernel32!VirtualAllocEx "r $t0=@$t0+1;.printf |
关于伪寄存器信息,可以参考帮助文档中的Pseudo-Register Syntax小结。
Step Out的实现
Step Out的定义是“Target executes until the current function is complete.”。Windbg中是如何实现这个功能的呢?根据这个定义,可以简单地在当前函数的返回地址上设定bp 断点就可以了。当前函数的返回地址保存在函数入口时候的EBP+4上。但如果简单地在EBP+4上面设定断点有两个问题:
1. 无法区分递归调用和函数返回,甚至其他线程对该地址的调用。
2. 第一次触发后不会自动清除端点,可能会多次触发。
如果观察windbg中step out的实现,可以看到:
bp /1 /c @$csp @$ra;g |
这里的/1参数使得断点在触发后自动清除,避免了第2个问题,/c @$csp参数通过指定callstack 的最小深度避免了第1个问题。而$ra伪寄存器直接表示当前函数的返回地址。多方便 :-)
2.1.7 远程调试(Remote debug)
远程调试是让调试人员远程地操作调试器的一种手段。
在调试WinForm程序的时候,如果要保持目标程序一直全屏运行,就没办法在同一台机器上切换到调试器输入命令。使用remote debug可以解决这样的情况,避免调试器对目标程序的干扰。
如果开发团队中的开发人员在两个城市,通过远程调试可以节省创建多个调试环境的时间。如果某些问题只能在固定的机器上重现,远程调试让排错过程简单便利。
在Windbg中,一种方法使用.server命令在本地创建一个TCP端口或者通过named pipe,使得远程的Windbg可以连接到本地调试。双方都可以输入命令,执行结果在双方的Windbg上都显示出来。具体介绍参考Windbg中关于.server命令的帮助。
另外一种更为强大的方法是使用DbgSrv。DbgSrv是一个调试服务。跟.server命令不同的地方在于,.server只是简单地通过重定向方便远程调试人员检查,而实际的调试工作都发生在目标机器上。DbgSrv则是让非常必要的调试动作发生在目标机器上,而次要的调试功能,比如加载PDB显示符号等,发生在调试人员的机器上。在DbgSrv出现以前,调试系统的核心服务,比如lsass.exe进程,需要同时结合用户态调试器和内核调试器,而且符号文件必须位于目标机器上。DbgSrv的出现让这个过程大为简化,请参考:
Debugging LSASS ... oh what fun, it is to ride.. |
2.1.8 如何通过Windbg命令行让中文魔兽争霸运行在英文系统上
买了中文版的魔兽争霸,但家里的Windows却是英文版。中文的魔兽争霸必须要运行到中文的操作系统上,否则就报告操作系统语言不匹配。
为了解决这个问题,首先能想到的就是到Windows的地区设置里面去把国家改为中国。尝试后发现问题依旧。看来魔兽争霸判断的并非本地Local设置,而是操作系统的语言版本。怎么办呢?重装系统?去网上找破解?其实Windbg就可以解决问题。
获取系统语言版本的API是GetSystemDefaultUILanguage。所以可以在这个 API上设定条件断点,然后观察魔兽争霸判断语言版本的逻辑是怎样的。
用Windbg启动war3.exe,然后在GetSystemDefaultUILanguage上设定断点。API触发后,发现调用完这个API后,war3.exe的下一条语句是一个cmp eax,ChineselanID,判断当前是否是中文系统。如果不是中文,就退出程序。
那好,在cmp这条语句上设定断点,然后用下面的命令把eax修改成中文语言符的 ID,就可以欺骗程序,让程序认为当前系统是中文:
r eax = 0n2052 |
eax被修改成了中文的语言符后,接下来的cmp执行结果就跟中文系统上的一样了,war3就可以正确运行了。每次都要做这样的修改麻烦得很,为了简化这个过程,创建内容为如下的script文件,在GetSystemDefaultUILanguage API返回前把ax设定为0n2052:
bp kernel32!GetSystemDefaultUILanguage+0x2c "r ax=0n2052;g" |
每次启动魔兽争霸都要手动设定断点是很麻烦的事情。如果要简化整个过程,可以采用下面文章中介绍的方法,让war3.exe启动的时候自动启动Windbg,通过-cf参数自动执行我们的条件断点来达到欺骗war3的目的。
How to debug Windows services |
2.1.9 Dump文件
前面提到过,dump文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到dump文件中。当在调试器中打开dump文件时,使用前面介绍的命令检查,看到的结果跟用调试器检查进程是一样的。
在Windbg中可以通过.dump命令保存进程的dump文件。比如下面的命令把当前进程的镜像保存为c:\testdump.dmp文件:
.dump /ma C:\testdump.dmp |
其中的/ma参数表示dump文件应该包含进程的完整信息,包括整个用户态的内存,这样dump文件尺寸会比较大,信息非常全面。如果不使用/ma参数,保存下来的dump文件只包含了部分重要资料,比如寄存器和线程栈空间,文件尺寸会比较小,无法分析所有的数据。
在Windbg中,通过File→Open Crash Dump菜单可以打开dump文件进行分析。打开dump文件后,运行调试命令看到的信息和状态,就是dump文件保存时进程的状态。通过dump文件能够方便地保存发生问题时进程的状态,方便事后分析。
2.1.10 CDB、NTSD和重定向到Kernel Debugging
除了Windbg,另外有两个调试器分别叫做CDB和NTSD。Windbg、CDB和NTSD三者使用的命令都完全一样。只是Windbg提供了窗口接口,剩下两个是基于命令行的工具。NTSD位于system32目录下,不需要特别安装。
这3个工具其实都使用了同样的调试引擎dbgeng.dll。关于调试引擎的详细信息,请参考:
Symbols and Crash Dumps |
由于CDB和NTSD采用命令行标准输入输出,所以可以很方便地通过重定向来控制这两个工具。一个典型的用例就是可以把用户态的调试重定向到Kernel Debugger。这样只需要一个Debugging Session就可以同时控制核心态和用户态的调试例程。详细信息请参考Windbg 帮助中的CDB and NTSD小结。
2.1.11 Debugger Extension,扩展Windbg的功能
Debugger Extension相当于是用户自定义,可编程的Windbg插件。一个最有用的extension就是.NET Framework 提供的sos.dll。它可以用来检查.NET程序中的内存、线程、callstack、appdomain、assembly等等信息。关于sos.dll后面会作详细讲解。关于如何开发自己的Debugger Extension,可以参考:
Debug Tutorial Part 4: Writing WINDBG Extensions |