技巧1: 异常中断
在处理被调用之前,异常发生时可以启动调试器进行中断,可以让你在异常发生后立即调试程序。操作调用栈便于你去查找异常发生的根本原因。
Vistual Studio允许你去指定想要中断的异常类型或者特殊异常。选择菜单Debug>Exceptions弹出对话框,你可以指定原生的(或者托管的)异常,除了调试器自带的一些默认异常,你还可以添加自己的自定义异常。
下面是一个std::exception 异常抛出时调试器中断的例子。
更多阅读:
· 1. 异常抛出时如何中断
· 2. 如何添加新的异常
技巧2:Watch窗口中的伪变量
Watch窗口或QuickWatch对话框提供一些特定的(调试器可识别的)变量,被称为伪变量。文档包含以下:
· $tid—–当前线程的线程ID
· $pid——进程ID
· $cmdline———-启动程序的命令行字符串
· $user———-正在运行程序的账户信息
· $registername—–显示寄存器registername 的内容
不管怎么样,关于最后一个错误的伪变量是非常有用的:
· $err——–显示最后一个错误的错误码
· $err,hr—显示最后一个错误的错误信息
更多阅读:伪变量
技巧3:符合越界后查看堆对象
有时候,在调试符号越界后,你还想查看对象的值,这个时候,watch窗口中的变量是被禁用的,不能再查看(也不能更新),尽管对象仍然存在。你如果知道对象的地址,可以继续充分地观察它。你可以将地址转换为该对象类型的指针,放在watch窗中。
下面的例子中,当单步跳出do_foo()之后,_foo不能再被访问。但是,将它的地址转换为foo*后,就可以继续观察这个对象。
技巧4:查看数组的值
如果你在操作一个很大的数组(我们假设至少有几百个元素吧,但是可能更少),在Watch窗口中展开数组,查找一些特定范围内的元素很麻烦,因为你要不停地滚动.如果数组是分配在堆上的话,你甚至不能在watch窗口中展开数组元素.对此,有一个解决办法。你可以使用(array+ <offset>),<count> 去查看从<offset>位置开始的特定范围的<count>元素(当然,这儿的数组是你的实际对象)。如果想查看整个数组,可以简单使用array,<count>.
如果你的数组是在堆上,你可以在watch窗口中将它展开,但是要查看某个特定范围的值,用法稍有不同:((T*) array + <offset>),<count>(注意这种用法对于堆上的多维数组也有效)。但是这种情况下,T是指数组元素的类型。
如果你在用MFC,并使用其中的’array’容器,像 CArray, CDWordArray,CStringArray等等。你当然可以使用同样的过滤方法。除此之外,你必须查看array的m_pData成员,它是保存数据的真实缓存。
技巧5:避免进入不必要的函数
很多时候,你在调试代码时可能会进入到你想跳过的函数,像构造函数,赋值操作或者其他的。其中最困扰我的是CString构造函数。下面是一个例子,当你准备单步执行take_a_string()函数时,首先进入到CString的构造函数。
1 2 3 4 5 6 7 8 | void take_a_string(CString const &text) { }
void test_string() { take_a_string(_T("sample")); } |
幸运的是可以告诉调试器去跳过哪些方法,类或者整个命名空间。实现它的方法也已经改变了,回到使用VS6的日子,通常是通过autoexp.dat文件来指定的。Vistual Studio 2002改成了使用注册表设置。想要跳过一些函数,你需要在注册表里添加一些值(详情如下):
1. 实际位置取决于你使用的Vistual Studio版本和操作系统平台(x86或x64,因为注册表只能在64位的Windows下浏览)
2. 值的名字是数字,代表规则的优先级;数字越大,优先级越高。
3. 值数据是一个正则表达式的REG_SZ值,用于指定怎样过滤和执行。
为了避免进入任何CString方法,我添加了下面的规则:
有了这个,即使你强制进入上例中的take_a_string(),调试器也会跳过CString的构造函数。
更多阅读:
· 使用Visual C++调试器怎样避免进入函数
· 使用AutoExp.dat调整调试器
技巧6:从代码启动调试器 Launch the debugger from code
你可能很少需要将调试器附加到程序中,但你不能在Attach窗口这样做(可能因为中断发生太快而没有捕获到),你也不能一开始就在调试器中启动程序。你可以在程序中产生中断给调试器一个机会通过调用内部的_degbugbreak()来附加。
1 2 3 4 | void break_for_debugging() { __debugbreak(); } |
实际上还有其他的方法来完成,例如触发中断3,但这仅仅适用于x86平台(C++64位不再支持ASM)。另外还有DebugBreak()函数,但它的使用不怎么简便,所以这里推荐使用内部方法。
1 | __asm int 3; |
程序运行内部方法时会停止运行,这时你就有机会将调试器附加到该进程。
更多阅读:
· 内部方法_debugbreak
· Visual Studio 20005/2008的调试,第四部分:为调试器设置代码
技巧7:在output窗口打印
通过调用DebugOutputString可以在调试器的output窗口显示一段特定的文本。如果没有附加的调试器,该函数什么也不做。
更多阅读:
· 函数OutputDebugString
· 函数OutputDebugString的调用机制
技巧8:隔离内存泄漏
内存泄漏是在原生开发中的一个很重要的问题,要检测内存泄漏是一个很严峻的挑战,尤其是在大型项目中。Vistual Studio可以提供检测内存泄漏的报告,还有其他的一些应用程序(免费的或商业的)也可以帮助你检测内存泄漏.有些情况下,在一些内存分配最终会导致泄漏时,可以使用调试器去中断。但是你必须找到可再现的分配编号(尽管没那么容易)。如果能做到这一点,执行程序时调试器才会中断。
我们来看下面的代码,分配了8个字节,却一直没释放分配的内存。Visual Studio提供了造成内存泄漏的对象的报告,多运行几次,会发现一直是同一个分配编号(341)。
1 2 3 4 5 6 7 8 9 | void leak_some_memory() { char* buffer = new char[8]; }
Dumping objects -> d:\marius\vc++\debuggingdemos\debuggingdemos.cpp(103) : {341} normal block at 0x00F71F38, 8 bytes long. Data: < > CD CD CD CD CD CD CD CD Object dump complete. |
在一个特定的(可复现的)位置中断的步骤如下:
1. 确定你有足够的关于内存泄漏的报告模式(参考 使用CRT库检测内存泄漏)
2. 多次运行程序直到你能在程序运行结束后的内存泄漏报告里找到一个可复现的分配编号,例如上个例子中的(341)
3. 在程序一开始的地方设置一个断点以便你能够尽早地进行中断。
4. 当最初的中断发生时,watch窗口的Name栏里会显示:{,,msvcr90d.dll}_crtBreakAlloc,在Value栏里写入你想要查找的位置编号
5. 继续调试(F5)
6. 程序执行到指定位置会停止,你可以使用调用栈被指引找到被该位置触发的那段代码。
遵循这些步骤, 在上个例子中,使用分配的编号(341)就可以识别内存泄漏的起因。
技巧9:调试发行版
调试和发布是两个不同的目的。调试配置是用于开发的,而发布配置,顾名思义,是用来作为程序的最终版本,因为它必须严格遵循发布的质量要求,该配置包含优化部分和调试版本的中断调试的设置。而且,有时候,要像调试调试版本一样去调试发行版。要做到这一点,你需要在配置里做一些改变。但是这种情况下,你就不再是在调试发行版,而是调试和发行的混合版。
你还应该做一些事儿,以下是必须要做的:
1. 配置C/C++ >General>Debug Information Format 应该为 “Program Database(/Zi)”
2. 配置C/C++ >Optimization>Optimization 应该为”Disabld(/Od)”
3. 配置Linker>Debugging>Generate Debug Info 应该为”Yes/(DEBUG)”
如图所示:
更多阅读:怎样调试发行版
技巧10:远程调试
另一个重要的调试就是远程调试,这是一个更大的话题,多次被提到,这里我只做一下简单的概括:
1. 你需要在远程机器上安装远程调试监控
2. 远程调试监控必须以管理员身份运行,并且用户必须属于管理员组
3. 在你运行监控时,会开启一个新的服务,该服务的名字必须用Visual Studio的Attach to Progress窗口的Qualifier组合框的值。
1. 远程和本地机器上的防火墙必须允许Visual Studio和远程调试监控之间能够通信
2. 想要调试,PDB文件是关键;为了能够让VisualStudio自动加载它们,必须满足以下条件:
1)本地的PDB文件必须可用(在远程机器的相同路径下放置一个对应的模块)。
2) 远程机器上的托管PDB文化必须可用。
远程调试监控下载:
· Visual Studio 2008 Service Pack 1 Remote Debugger
· Microsoft Visual Studio 2010 Remote Debugger
更多阅读:
· 设置远程调试
· Visual Studio远程调试和PDB文件
技巧11: 数据断点
当数据所在的内存位置发生变化时,可以通知调试器进行中断,但是每次只能创建4个字节这样的硬件数据断点。数据断点只能在调试期间添加,可以通过菜单(Debug>New Breakpoint>New Data Breakpoint) 或者断点窗口来添加。
你可以使用内存地址或者地址表达式。尽管栈上和堆上的值你都可以看到,但是我认为当堆上的数值发生变化时,这个功能才会更有用处。它对于识别内存损坏有很大的帮助。
下面的例子中,指针的值发生了变化,不再是它所指向对象的值。为了找出在什么地方发生改变的,我在存储指针值的位置设置了一个断点,即&ptr(注意必须在指针初始化之后)。数据发生变化就意味着有人修改了指针的值,调试器发生中断,我就能找出是哪段代码引起的改变。
更多阅读:
1.怎样查明指针是否损坏内存
2.怎样查明指针在什么地方发生改变
技巧12: 线程重命名
在调试多线程应用程序时,线程窗口会显示创建了哪些线程以及当前正在运行的线程。线程越多,想找到你想要的线程就越困难(尤其是当一段程序被多个线程同时执行的时候,你不能确切地知道哪个才是当前正在执行的线程实例)。
调试器允许修改线程的名字,可以在线程窗口使用线程的快捷菜单,给线程重命名。
也可以在程序里给线程命名,尽管有点棘手,而且必须在线程启动之后给它命名,否则调试器会以默认命名规范将它重新初始化。定义一个线程,并用下面的函数重命名该线程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | typedef struct tagTHREADNAME_INFO { DWORD dwType; // must be 0x1000 LPCSTR szName; // pointer to name (in same addr space) DWORD dwThreadID; // thread ID (-1 caller thread) DWORD dwFlags; // reserved for future use, most be zero } THREADNAME_INFO;
void SetThreadName(DWORD dwThreadID, LPCSTR szThreadName) { THREADNAME_INFO info; info.dwType = 0x1000; info.szName = szThreadName; info.dwThreadID = dwThreadID; info.dwFlags = 0;
__try { RaiseException(0x406D1388, 0, sizeof(info)/sizeof(DWORD), (DWORD*)&info); } __except (EXCEPTION_CONTINUE_EXECUTION) { } } |
更多阅读:
设置线程名字(非托管)
技巧13: 给指定线程设置断点
对于多线程应用程序来说,另一个有用的技巧就是给指定的线程,进程,甚至是计算机中的断点设置过滤.可以通过断点的Filter命令来实现此功能.
调试器允许你指定线程名,线程ID,进程名,进程ID和机器名的任意组合(使用AND,OR,NOT)来设置过滤。了解怎样设置线程名字也使得这项过滤操作变得更加简单。
更多阅读:
1. 怎样指定断点过滤器
2. 设置断点过滤
技巧14: (粗略)估算执行时间
在上一篇文章中,我有写关于Watch窗口中的伪变量,有一个没提到的是@clk,它用于显示计数器的值,可以粗略地计算出两个断点之间的代码的执行时间,单位是微秒(μS)。但是,千万不要用这个方法来分析程序的执行效率,应该使用Visual Studio 分析工具或者性能计时器来分析。
可以在Watch 窗口或者即时窗口添加@clk=0来完成对计时器的重置。因此要想估算执行一段代码需要多长时间,可以按照下面的步骤来操作:
1. 在代码块的开始位置设置断点
2. 在代码块的结束位置设置断点
1. 在Watch窗口添加 @clk
2. 程序进入到第一个断点时,在即时窗口输入@clk=0
3. 运行程序直到执行进入代码块末尾的断点,查看Watch窗口 @clk的值。
注意网上有一些技巧说在Watch窗口添加两个表达式:@clk和@clk=0,需要在每次执行断点的时候都要重置计时器。这种用法只适用于Visual Studio的老版本,在VS2005及以上版本不再适用。
更多阅读:
调试技巧-@CLK
技巧15:数字格式化
当你在Watch或者Quick Watch窗口查看变量时, 这些值是以默认的预定义可视化形式显示的。而对于数字,则是根据数据类型(integer, float, double),用十进制形式显示的。但是你可以使用调试器把数字用不同的类型或者进制数显示出来。
想要改变显示类型可在变量前加以下前缀:
1. by –unsigned char (又称为unsigned byte)
2. wo – unsigned shot(又称为 unsigned word)
3. dw – unsigned long(又称为 unsigned double word)
要改变显示的进制数在变量前加下列前缀:
1. d 或者 i– 有符号十进制数
2. u – 无符号十进制数
3. o - 无符号八进制数
4. x – 小写十六进制数
5. X – 大写十六进制数
更多阅读:
C++ 调试技巧
技巧16:(内存数据)格式化
除了数字,调试器还可以在Watch窗口显示格式化的内存数据,最多64 bytes。你可以使用在表达式(变量或内存地址)后添加下列说明符作为后缀来格式化数据:
1. mb 或者 m – 十六进制显示的16字节数据,后跟16个ASCII 字符
2. mw – 8 words
3. md – 4 double words
4. mq - 2 quad-words
5. ma – 64个ASCII字符
6. mu – 2字节的UNICODE字符
更多阅读:
1. C++中的格式说明符
2. Developer Studio的调试技巧
技巧17: 系统DLL的中断
有时候在DLL中的函数被调用时进行中断是很有用的,像系统DLL(比如 Kernel32.dll 或者user32.dll).实现此中断,需要使用本机调试器提供的上下文运算符.你可以设定断点位置,变量名或者表达式:
1.{[函数],[源码],[模块]}位置
2. [函数],[源码],[模块]}变量名
3. [函数],[源码],[模块]}表达式
花括号里可以是函数名,源代码和模块的任意组合,但是逗号不能省略.
我们假设想要在CreateThread函数被调用时发生中断,这个函数是从kernel32.dll中导出的,所以上下文运算符应该为: {,,kernel32.dll}CreateThread. 然而,这样并不可行,因为上下文运算符需要CreatThread的修饰符,可以使用DBH.exe来获取一个特定函数的修饰符。
下面就是如何得到CreateThread函数的修饰符的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | C:\Program Files (x86)\Debugging Tools for Windows (x86)>dbh.exe -s:srv*C:\Symbo ls*http://msdl.microsoft.com/Download/Symbols -d C:\Windows\SysWOW64\kernel32.dl l enum *CreateThread* Symbol Search Path: srv*C:\Symbols*http://msdl.microsoft.com/Download/Symbols
index address name 1 10b4f65 : _BaseCreateThreadPoolThread@12 2 102e6b7 : _CreateThreadpoolWork@12 3 103234c : _CreateThreadpoolStub@4 4 1011ea8 : _CreateThreadStub@24 5 1019d40 : _NtWow64CsrBasepCreateThread@12 6 1019464 : ??_C@_0BC@PKLIFPAJ@SHCreateThreadRef?$AA@ 7 107309c : ??_C@_0BD@CIEDBPNA@TF_CreateThreadMgr?$AA@ 8 102ce87 : _CreateThreadpoolCleanupGroupStub@0 9 1038fe3 : _CreateThreadpoolIoStub@16 a 102e6f0 : _CreateThreadpoolTimer@12 b 102e759 : _CreateThreadpoolWaitStub@12 c 102ce8e : _CreateThreadpoolCleanupGroup@0 d 102e6e3 : _CreateThreadpoolTimerStub@12 e 1038ff0 : _CreateThreadpoolIo@16 f 102e766 : _CreateThreadpoolWait@12 10 102e6aa : _CreateThreadpoolWorkStub@12 11 1032359 : _CreateThreadpool@4 |
看上去实际名字应该是_CreateThreadStub@24,这样我们就可以创建断点,{,,kernel32.dll}_CreateThreadStub@24。
运行程序,发生中断时会有消息提示断点处无相关源代码,直接忽略它。
使用调用栈窗口查看调用该函数的代码。
更多阅读:
1. 在Visual Studio 2010中,没有源代码如何设置断点
2. 上下文运算符(C/C++语言表达式)
3. 怎样给函数设置断点
技巧18:加载符号表
在调试程序的时候,调用栈窗口不会显示完整的调用栈,跳过了系统DLL(比如kernel32.dll 和 user32.dll)的信息。
可以通过加载这些DLL的符号表来获得完整的调用栈信息,直接在调用栈窗口使用快捷菜单就能完成。你可以从预先指定的符号路径或者微软的符号服务器(如果是系统DLL)来下载符号。符号下载完成后,直接导入到调试器,调用栈就会得到更新。</span>
这些符合也可以从组件Modules窗口导入。
一旦下载完成,符号会保存在缓存中,可以在Tools>Options>Debugging>Symbols中配置。
技巧19:监测MFC中的内存泄漏
如果你想要在MFC应用程序中检测内存泄漏,需要使用宏DEBUG_NEW来重新定new运算符,这是new运算符的修改版本,记录了每个对象内存分配的文件名和行号.在发行版中DEBUG_NEW会解析成new运算符.
向导生成的MFC源文件在#includes后包含了下面的预处理指令:
1 2 3 | #ifdef _DEBUG #define new DEBUG_NEW #endif |
这就是怎样重新定义new运算符的。
然而,很多STL头文件和重新定义的new运算符和版本不兼容.如果你重新定义了new运算符后,又包含了<map>,<vector>,<list>,<string>等头文件的话,就会出现下面的错误(以<vector>为例):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 1>c:\program files (x86)\microsoft visual studio 9.0\vc\include\xmemory(43) : error C2665: 'operator new' : none of the 5 overloads could convert all the argument types 1> c:\program files\microsoft visual studio 9.0\vc\include\new.h(85): could be 'void *operator new(size_t,const std::nothrow_t &) throw()' 1> c:\program files\microsoft visual studio 9.0\vc\include\new.h(93): or 'void *operator new(size_t,void *)' 1> while trying to match the argument list '(const char [70], int)' 1> c:\program files (x86)\microsoft visual studio 9.0\vc\include\xmemory(145) : see reference to function template instantiation '_Ty *std::_Allocate<char>(size_t,_Ty *)' being compiled 1> with 1> [ 1> _Ty=char 1> ] 1> c:\program files (x86)\microsoft visual studio 9.0\vc\include\xmemory(144) : while compiling class template member function 'char *std::allocator<_Ty>::allocate(std::allocator<_Ty>::size_type)' 1> with 1> [ 1> _Ty=char 1> ] 1> c:\program files (x86)\microsoft visual studio 9.0\vc\include\xstring(2216) : see reference to class template instantiation 'std::allocator<_Ty>' being compiled 1> with 1> [ 1> _Ty=char 1> ] |
解决办法就是总是把包含这些STL头文件放在重新定义new运算符之前.
更多阅读:
DEBUG_NEW
技巧20: 调试ATL
在开发ATL COM组件时,你可以在调试器观察COM对象的QueryInterface,AddRef和Release的调用情况.默认情况下并不支持这些,但是你只要在预处理定义或者预编译头文件时定义两个宏,宏定义好之后,关于这些函数的调用信息就会显示在output窗口.
这两个宏如下:
1. _ATL_DEBUG_QI: 显示你定义的对象里每一个被查询的接口的名字,必须在atlcom.h被包含之前定义.
2. _ATL_DEBUG_INTERFACES: 在每次AddRef 或者Release被调用时,显示接口的当前引用计数以及对应的类名和接口名,必须在atlbase.h被包含之前定义.
更多阅读:
1. 调试技巧
2. ATL调试技巧
3. _ATL_DEBUG_INTERFACES是如何工作的?
结束语
尽管这两篇文章并不是包含了所有的调试技巧,但是足以帮你解决原生开发中调试时遇到的大多数问题