visualstudio调试方法

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

visualstudio调试⽅法
最近碰巧读了Ivan Shcherbakov写的⼀篇⽂章,《》。

这篇⽂章只介绍了⼀些有关Visual Studio的基本调试技巧,但是还有其他⼀些同样有⽤的技巧。

我整理了⼀些Visual Studio(⾄少在VS 2008下)原⽣开发的调试技巧。

(如果你是⼯作在托管代码下,调试器会有更多的特性,在CodeProject中有介绍它们的⽂章),下⾯是我的整理的⼀些技巧:
1. 异常中断 | Break on Exception
2. Watch窗⼝中的伪变量 | Pseudo-variables in Watch Windows
3. 符号越界后查看堆对象 |
4. 查看数组的值
5. 避免进⼊不必要的函数
6. 从代码启动调试器 | Launch the debugger from code
7. 在Output窗⼝打印
8. 隔离内存泄漏
9. 调试发⾏版 | Debug the Release Build
10. 远程调试
11. 数据断点
12. 线程重命名
13. 给指定线程设置断点
14.(粗略)估算执⾏时间
15. 数字格式化
16. (内存)数据格式化
17.系统DLL中断
18.加载符号表
19. 监测MFC中的内存泄漏
20. 调试ATL
技巧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的构造函数。

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的构造函数。

更多阅读:
技巧6:从代码启动调试器 Launch the debugger from code
你可能很少需要将调试器附加到程序中,但你不能在Attach窗⼝这样做(可能因为中断发⽣太快⽽没有捕获到),你也不能⼀开始就在调试
器中启动程序。

你可以在程序中产⽣中断给调试器⼀个机会通过调⽤内部的_degbugbreak()来附加。

void break_for_debugging()
{
__debugbreak();
}
实际上还有其他的⽅法来完成,例如触发中断3,但这仅仅适⽤于x86平台(C++64位不再⽀持ASM)。

另外还有DebugBreak()函数,但它的使⽤不怎么简便,所以这⾥推荐使⽤内部⽅法。

__asm int 3;
程序运⾏内部⽅法时会停⽌运⾏,这时你就有机会将调试器附加到该进程。

更多阅读:
技巧7:在output窗⼝打印
通过调⽤DebugOutputString可以在调试器的output窗⼝显⽰⼀段特定的⽂本。

如果没有附加的调试器,该函数什么也不做。

更多阅读:
技巧8:隔离内存泄漏
内存泄漏是在原⽣开发中的⼀个很重要的问题,要检测内存泄漏是⼀个很严峻的挑战,尤其是在⼤型项⽬中。

Vistual Studio可以提供检测内存泄漏的报告,还有其他的⼀些应⽤程序(免费的或商业的)也可以帮助你检测内存泄漏.有些情况下,在⼀些内存分配最终会导致泄漏时,可以使⽤调试器去中断。

但是你必须找到可再现的分配编号(尽管没那么容易)。

如果能做到这⼀点,执⾏程序时调试器才会中断。

我们来看下⾯的代码,分配了8个字节,却⼀直没释放分配的内存。

Visual Studio提供了造成内存泄漏的对象的报告,多运⾏⼏次,会发现⼀直是同⼀个分配编号(341)。

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. 确定你有⾜够的关于内存泄漏的报告模式(参考)
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
1. 远程和本地机器上的防⽕墙必须允许Visual Studio和远程调试监控之间能够通信
2. 想要调试,PDB⽂件是关键;为了能够让VisualStudio⾃动加载它们,必须满⾜以下条件:
1)本地的PDB⽂件必须可⽤(在远程机器的相同路径下放置⼀个对应的模块)。

2) 远程机器上的托管PDB⽂化必须可⽤。

远程调试监控下载:
更多阅读:
结束语1
Ivan Shcherbakov那篇⽂章和我这篇⽂章提到的调试技巧,在⼤多数的调试问题中都是必不可少的。

想要知道更多的关于调试技巧的知识,建议阅读⽂章中提供的额外阅读。

技巧11:数据断点
当数据所在的内存位置发⽣变化时,可以通知调试器进⾏中断,但是每次只能创建4个字节这样的硬件数据断点。

数据断点只能在调试期间添加,可以通过菜单(Debug>New Breakpoint>New Data Breakpoint) 或者断点窗⼝来添加。

你可以使⽤内存地址或者地址表达式。

尽管栈上和堆上的值你都可以看到,但是我认为当堆上的数值发⽣变化时,这个功能才会更有⽤处。

它对于识别内存损坏有很⼤的帮助。

下⾯的例⼦中,指针的值发⽣了变化,不再是它所指向对象的值。

为了找出在什么地⽅发⽣改变的,我在存储指针值的位置设置了⼀个断点,即&ptr(注意必须在指针初始化之后)。

数据发⽣变化就意味着有⼈修改了指针的值,调试器发⽣中断,我就能找出是哪段代码引起的改变。

额外阅读:
技巧12:线程重命名
在调试多线程应⽤程序时,线程窗⼝会显⽰创建了哪些线程以及当前正在运⾏的线程。

线程越多,想找到你想要的线程就越困难(尤其是当⼀段程序被多个线程同时执⾏的时候,你不能确切地知道哪个才是当前正在执⾏的线程实例)。

调试器允许修改线程的名字,可以在线程窗⼝使⽤线程的快捷菜单,给线程重命名。

也可以在程序⾥给线程命名,尽管有点棘⼿,⽽且必须在线程启动之后给它命名,否则调试器会以默认命名规范将它重新初始化。

定义⼀个线程,并⽤下⾯的函数重命名该线程。

typedef struct tagTHREADNAME_INFO
{
DWORD dwType; // 必须是两个字节的长度
LPCSTR szName; // 指针指向命名 (同⼀个地址空间)
DWORD dwThreadID; // 线程ID(-1调⽤线程)
DWORD dwFlags; // 保留待⽤,多数情况下为0
} 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)来设置过滤。

了解怎样设置线程名字也使得这项过滤操作变得更加简单。

辅助阅读:
技巧14: (粗略)估算执⾏时间
在上⼀篇⽂章中,我有写关于Watch窗⼝中的伪变量,有⼀个没提到的是@clk,它⽤于显⽰计数器的值,可以粗略地计算出两个断点之间的代码的执⾏时间,单位是微秒(µS)。

但是,千万不要⽤这个⽅法来分析程序的执⾏效率,应该使⽤Visual Studio 分析⼯具或者性能计时器来分析。

通过在Watch窗⼝或者Immediate窗⼝中添加@clk=0来重置计时器。

因此,若需要计算末段代码执⾏所需要的时间,做下列处理:
在代码块起始位置设置断点
在代码块结束位置设置断点
在Watch窗⼝中添加@clk
当第⼀个断点触发时,在Intermediate窗中中输⼊@clk=0
运⾏程序,直到遇到代码块结束位置的断点,并在Watch窗⼝中查看@clk的值
注意⽹上有⼀些技巧说在Watch窗⼝添加两个表达式:@clk和@clk=0,需要在每次执⾏断点的时候都要重置计时器。

这种⽤法只适⽤
于Visual Studio的⽼版本,在VS2005及以上版本不再适⽤。

辅助阅读:
技巧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 – ⼤写⼗六进制数
辅助阅读:
技巧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字符
附加阅读:
技巧17:系统DLL的中断
有时候在DLL中的函数被调⽤时进⾏中断是很有⽤的,像系统DLL(⽐如 Kernel32.dll 或者user32.dll).实现此中断,需要使⽤本机调试器提供的上下⽂运算符.你可以设定断点位置,变量名或者表达式:
1.{[函数],[源码],[模块]}位置
2. [函数],[源码],[模块]}变量名
3. [函数],[源码],[模块]}表达式
花括号⾥可以是函数名,源代码和模块的任意组合,但是逗号不能省略.
我们假设想要在CreateThread函数被调⽤时发⽣中断,这个函数是从kernel32.dll中导出的,所以上下⽂运算符应该为:
{,,kernel32.dll}CreateThread. 然⽽,这样并不可⾏,因为上下⽂运算符需要CreatThread的修饰符,可以使⽤DBH.exe来获取⼀个特定函数的修饰符。

下⾯就是如何得到CreateThread函数的修饰符的:
C:\Program Files (x86)\Debugging Tools for Windows (x86)>dbh.exe -s:srv*C:\Symbo
l enum *CreateThread*
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。

运⾏程序,当遇到暂停时,直接忽略关于在断点位置⽆相关源代码的消息提⽰。

使⽤调⽤堆栈窗⼝来查看调⽤这个函数的代码。

附加阅读:
技巧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后包含了下⾯的预处理指令:
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
这就是怎样重新定义new运算符的。

然⽽,很多STL头⽂件和重新定义的new运算符和版本不兼容.如果你重新定义了new运算符后,⼜包含了<map>,<vector>,<list>,<string>等头⽂件的话,就会出现下⾯的错误(以<vector>为例):
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运算符之前.
附加阅读:
技巧20: 调试ATL
在开发ATL COM组件时,你可以在调试器观察COM对象的QueryInterface,AddRef和Release的调⽤情况.默认情况下并不⽀持这些,但是你只要在预处理定义或者预编译头⽂件时定义两个宏,宏定义好之后,关于这些函数的调⽤信息就会显⽰在output窗⼝.
这两个宏如下:
1. _ATL_DEBUG_QI: 显⽰你定义的对象⾥每⼀个被查询的接⼝的名字,必须在atlcom.h被包含之前定义.
2. _ATL_DEBUG_INTERFACES: 在每次AddRef 或者Release被调⽤时,显⽰接⼝的当前引⽤计数以及对应的类名和接⼝名,必须
在atlbase.h被包含之前定义.
辅助阅读:
结束语2
尽管这两篇⽂章并不是包含了所有的调试技巧,但是⾜以帮你解决原⽣开发中调试时遇到的⼤多数问题。

相关文档
最新文档