调试是软件开发周期中很重要的一部分。它具有挑战性,同时也很让人疑惑和烦恼。总的来说,对于稍大一点的程序,调试是不可避免的。最近几年,调试工具的发展让很多调试任务变的越来越简单和省时。
这篇文章总结了可能节省你大量时间的11个Visual studio的调试技巧和方法。
1 悬停鼠标查看表达式值
调试是很有挑战性的。比如在函数内逐步运行可以看出哪里出错,查看堆栈信息可以知道函数被谁调用等等……但是无论哪种情况下,查看表达式和局部变量的值都是很麻烦的(把表达式和局部变量放到watch窗口里)。一种更简单的方法,把鼠标停在所需查看的数据上。如果是类或结构,那么点击展开可以很方便快速地查看其字段。
2 在运行过程中改变变量值
调试器不仅仅是分析程序崩溃和诡异行为的工具,还可以通过逐步调试检查数据和行为是否符合程序预期的方法解决许多bug。有时,你会想是否设置某些条件为真,程序就能正确运行了。其实你只要把鼠标移动到变量上,双击值,然后输入你需要的值。这样就不需要修改代码,重启程序了。
3 设置下一个运行位置
一个典型的调试案例是我们经常会用逐步调试的方法去分析为什么函数出错了。这时你遇到这个函数调用其他函数返回错误,而这个错误不是你想要的,你该怎么办?重启调试器?这里有个更好的方法,直接把黄色的运行位置箭头拖到你想要的运行位置。其实就是跳过中间运行代码,直接到想要的位置。很简单吧。
4 编辑然后继续运行
在运行一个很复杂的程序和插件时,发现一个错误,但是不想浪费时间去重编译重启动程序。很简单,只要在这个位置修改这个bug,然后继续调试。Visual studio会修改这个程序,使得你可以继续调试而不需要重启程序。
值得注意的是“编辑然后继续运行”这个功能有几个限制。一,它不能在64位代码上使用。如果想使用这个功能,到项目设置里的编译选项,选择”x86”作为目标平台。不要担心,这目标平台在reslease配置是和”debug是分离的,也就是说依然是”Any CPU”的设置。二,“编辑然后继续运行”这个功能仅适用于一个函数内部改变。如果你想要改变这个函数的声明或者增加新的方法,你只能选择重启程序,或者不做任何改变继续。如果修改的方法中包含lambda表达式,则意味着修改了编译器自动生成的委托类型,这样会导致编译器停止运行。
5 一个方便的查看窗口
大部分现代的调试器都有查看窗口。但是,visual studio的查看窗口使用特别简单,你能很方便的增加和删除变量。只要在窗口里点击空白行,输入表达式然后按enter键。或者点击表达式,按delete键删除不需要的表达式。
在调试窗口不仅仅可以查看普通的变量值,甚至可以输入$handles去追踪打开的句柄数量,$err去查看函数的错误代码(然后使用 Tools->Error 查看错误代码的描述)或者输入 @eax(在64位下是@rax )查看包含函数返回值的寄存器值。
6 注释反汇编
使用内部的反汇编功能使得优化局部代码更加简单。Visual studio可以在你的每一行代码下显示汇编指令,并且可以逐步调试汇编代码,也可以在任意位置设置断点。查看和修改汇编代码类似于c++。
7 堆栈信息的线程窗口
调试多线程代码是很痛苦的。或许也是有趣的。这取决于你的调试器。Visual studio 一个很赞的功能就是在线程窗口查看线程的堆栈信息。你能很方便的直接看到所有线程以及他们的堆栈信息。
8 条件断点
如果你想重现一个小概率事件,但是断点在大量不需要的条件下也会触发。你可以很简单的设置条件断点。在断点窗口设置该断点条件,Visual studio 会自动忽略不符合条件的断点。
9 内存窗口
一些bug是由错误的结构定义、缺少对齐属性等原因引起的。查看每行内存的内容很容易定位和解决这些bug. Visual studio 的内存窗口可以把数据翻译成8/16/32/64-bit数字或者浮点数。你可以在编辑窗口直接改变数值。
10 跳转到定义
如果你在解决别人写的代码一个bug,会遇到“这个类型是什么”“这个函数做什么的”之类的问题,你可以使用visual studio的跳转到定义的命令来查看类型或函数的定义。
11 命令窗口
这个小技巧是由chaau建议的,它能节省你大量的时间。Visual studio支持一个命令窗口,你可以通过菜单View->Other Windows->Command Window 打开。你可以在窗口里输入不同命令使调试自动化。比如,可以通过很简单的命令去的测试MFC的COleDateTime变量。
下面有从浅入深的6个问题,您可以尝试回答一下
问题1,2
这两个问题最简单,我在一个例子里说明
例如如下循环
for(int i=0;i<1000;i++){doSomeThing......}
在循环的大括号上单击右键,插入断点,用这个方法,可以对付那些喜欢把语句写在一行上的家伙,其实,随着.Net3.5中Linq的出现,我们肯定也会经常在在一行上写复杂的表达式,这个时候用这种插入方法会比较管用
ok,现在我们来编辑这个断点的条件,在断点上右键单击,选择如图菜单项
在弹出的窗口中可以设置断点命中的条件i==900
注意我是在调试C#代码,默认的条件语句语法是C#,如果你想切换,那就需要用Ctrl-B,来插入断点,并在弹出窗口中选择语言
通过这样设置条件断点,我们就可以解决我们的问题1,2了
问题3
同样通过设置条件断点我们还可以解决我们的问题3,对表达式变化的跟踪
string user="yizhu2000"
for(int i=0;i<10000;i++){
DoSomething1()
.......
DoSomethingN()
}
当循环执行完毕时我们发现user变成了"smart_boy",你不知道这个值是在第几次循环的时候变化的,那么你是不是会选择打上断点,一次一次中断,来查看呢?当然不用
在循环体结束的位置我们设置一个断点,打开条件编辑窗口(打开方法同上),设置表达式为user,勾选下面的HasChanged,也就是说,你告诉断点,当user的值发生变化时才触发
(注意:第一次执行到断点的时候,程序一定会中断,并计算这时表达式的值,所以,所谓发生变化,指的是以后执行到断点是表达式的值和第一次执行到断点时表达式的值的比较)
问题4
如何让断点在指定的命中次数或者大于某个次数时触发呢?方法是设定几个断点的HitCount,右键单击断点,在弹出菜单中选择Hit Count,会弹出如下窗口
在"when the break point is hit"下拉列表里,我们可以看到四个选项
break always:总是中断
break when the hit count is equal to:等于某次数时中断
beak when the hit count is a multpile of:当次数是某数的倍数时中断
break when the hit count is greater than or equal to:当大于等于某数时中断
问题5
前面4个问题都已经解决了,第5个问题的解决方法是利用断点的Filter功能,比如我希望断点只有被机器名为yizhu的机器访问才能触发,我可以这样设置
当其他机器访问程序的时候,断点将不会触发,这样做的优点是通过设置机器名,我们可以让其他机器访问的时候感觉不到断点的存在,除此之外我们可以设 置机器名,进程号,进程名,线程号,线程名作为filter,而且还可以把他们组合起来,比如我希望通过当机器yizhu的dllhost进程调用时才触 发,那么问题就可以设置为MachineName="yizhu"&ProcessName="dllhost"
问题6
现在我们来解决第6个问题:
在程序性能调试的时候,我们经常需要知道某段代码的执行效率,一般来说,我们可以在程序中加入时间点,通过时间点相减来取得时间间隔,这种方法有个 显而易见的缺点就是需要修改程序,想要不修改程序,就需要借助一些工具,那么有没有什么方法可以声明式的插入时间点,并计算值呢?其实断点完全可以做到
在给出方法前,我们来看看断点的另外一个设置项,When Hit,这个选项可以让我们在命中断点后做一些事情,包括输出一些内容,或者调用宏,比如输出一个程序中变量的值
我们输出了变量user的值,下面Continue Execution表示程序不会中断,输出后继续执行,注意表达式需要用{}括起来,,其他的部分会被作为字符串输出。设定WhenHit后断点变成了方形(看厌了圆断点,我还挺喜欢这个方家伙的)
在output中查看输出结果,如下:
既然可以计算表达式,我们的第一个最简方案就出来了,也就是在程序执行到断点的时候,输出DateTime.Now,这样当然是可行的,但是我们需 要的是时间间隔,所以我们还需要自己来算个减法,还是挺麻烦的,怎么样才能让程序自己输出时间间隔呢?有一个想法是这样的,我们在上一个断点声明一个时间 变量,然后在下面的断点里用DataTime.Now减去这个变量,即
断点一的条件:{DateTime _t=DateTime.Now;}
断点二的条件:{DateTime.Now-t;}
看起来不错,但是实际运行时就有问题了,让我们看看输出吧
上面高亮的部分说,变量申明只能在immediate window中进行,所以断点一的变量没有申明成功,关于immediatewindow,我们以后会涉猎到,反正就是说想在表达式里申明变量,没门,死路一条.那么我们怎么才能不申明变量又时间点呢?
这时我想起了Thread.SetData 方法,这个方法可以往当前线程专门提供的空间中插入一些数据,并且可以通过GetData得到数据,具体细节参考
http://msdn2.microsoft.com/zh-cn/library/system.threading.thread.setdata(VS.80).aspx
于是方案就有了,在第一个断点处把时间放入Thread的DataSlot,然后第二个断点取出来相减
断点一的条件:{Thread.SetData(Thread.GetNamedDataSlot("ExecutionTime"),DateTime.Now);}
断点二的条件:{DateTime.Now-(DateTime)System.Threading.Thread.GetData(System.Threading.Thread.GetNamedDataSlot("ExecutionTime"));}
看看输出效果
我们的目的已经达到了,output中成功的输出了时间间隔,当然,还不是很完善,首先,这个方法限于两个断点,你想多打几个断点,测试两两间的间隔还是比较麻烦.测量精度也可以提高,大家有兴趣可以自己研究这个方法的扩展
调试可以深入程序内部,观察运行时各个变量的值。但是,并不是一出现bug就要调试。调试最适合用来探究一些自己不太熟悉的语言特性或者是技术。比如你对C某些语句的作用不太熟悉,对某个库函数的作用不太熟悉,调试一下,就可以看得清清楚楚了。如果程序只是逻辑出错误,最好的方法是测试,通过逐个单元的测试,找出问题的所在。为什么测试的效率更高?因为测试可以是自动化的,你可以编写测试代码,一次性地完成很多测试,但调试只能一步一步地来。调试的好处是可以直接看变量的值,而测试的话,必须写额外的代码把变量的值输出到控制台或者日志文件里。下面说一些调试的技巧。
断点
最简单的一种,设置一个断点,程序执行到那一句就自动中断进入调试状态。
单步执行
有三种,一种是每次执行一行;一种是每次执行一行,但遇到函数调用就会跳到被调用的函数里;一种是直接执行当前函数里剩下的指令,返回上一级函数。在Visual Studio中,上面三种方法对应的快捷键分别为F10、F11、Shift F11。
监视
调试器可能会自动列出一些相关变量的值,但是你可能还关心其它变量的值,可以添加对这些变量的监视。还可以监视一个表达式的值,比如ab。但是,这个表达式最好不要修改变量的值,比如监视a 都会导致监视时修改了a的值,影响了程序的运行结果。
条件中断
假如你有这样的循环:
for(int i=0;i<100;i ){
for(int j=0;j<100;j ){
……
}
}
你怀疑当i=10且j=10的时候执行有问题,那如何调试?用断点的话,从i=0的初始状态,需要中断10次才能到i=10,然后从j=0也需要再中断10次,才能到j=10的状态。所以想进入i=10且j=10的状态,需要中断20次,这太麻烦了。可以使用条件中断:
for(int i=0;i<100;i ){
for(int j=0;j<100;j ){
if(i==10 && j==10){
;//空语句
}
……
}
}
在空语句的那一行设置断点就可以了。
上面的if结构太占地方,还可以用assert:
assert(i!=10 || j!=10);
断言i不为10或j不为10,那么当i=10且j=10的时候,断言就不成立,程序就会中断,进入调试状态。
有时候用throw也可以中断:
if(i==10 && j==10)throw;
但是最好不要这样做,调试器不一定会在throw的地方中断。
控制变量法
其实这已经不算是调试的内容了,但是也是一种找出bug原因的手段,所以还是在这里说。
控制变量法常用于科学研究中,比如说,研究牛顿第二定律a=F/m,a与F和m都有关,那么可以先固定m,研究a与F的关系;然后固定F,研究m与a的关系。
对于一个程序来说,一个bug可能跟多处代码有关。假如你怀疑这个bug与某些语句有关,可以把这些语句注释掉,或者是改一改,看看bug是否还存在,如果不存在,说明确实跟这些语句有关。(当然,要保证程序少了这些语句之后还可以顺利运行。)如果bug还存在,就说明它跟这些语句无关。
有些时候我们缺乏调试工具,比如在网页上运行的程序,在特殊设备上运行的程序,那么控制变量法是一种很有用的代替手段。
二分法
二分法是控制变量法的进一步扩展。
在数学上,二分法用于求一个连续函数的根。比如一个函数f(x),如果f(x1)>0且f(x2)<0,那么在区间x1和x2之间,必定存在一个x,使f(x)=0。然后我们再考察区间的中点x3=(x1x2)/2,如果f(x3)>0,则函数的根就在区间x3和x2之间,如果f(x3)<0,那么函数的根就在区间x1和x3之间。如此不断地把区间一分为二,最后锁定函数的根。
对于一个程序来说,如果当前情况是有bug的,那就好比是f(x1)>0;如果你把main函数里所有的操作都注释掉,那么程序什么都不做,就不可能有bug,那就好比是f(x2)<0;于是在这两种状态之中,肯定存在一些临界的语句,当这些语句改动的时候,就会使程序在有bug和无bug状态间切换,这些语句就是bug的原因所在。运用二分法的思想可以锁定这些临界语句。一开始先对程序做一些大刀阔斧的改动,比如说,程序的主循环会循环10次,就改成1次;程序有10个功能,就关掉5个功能。看看哪些改动,可以让程序由有bug状态切换到无bug状态。找到这样改动后,就把这个改动再细分成几个小改动,比如关掉5个功能,就细分为关掉一两个功能,再看看哪些小改动可以让程序由有bug状态切换到无bug状态。如此一步一步缩小包围圈,就后锁定一个无法再分的小改动,这个改动就是bug的原因所在。
同步法
有些bug是由于多线程而产生的。因为在不同线程里的操作我们无法预测其发生的顺序,可能当它们按某种次序进行时,bug不会出现,当它们按另一种次序进行时,bug就出现了。比如多线程那一节说到的那个银行帐户,如果没有加同步锁,就会出现这种bug。对这种bug的调试是很困难的,有时你运行程序发现了bug,而在进行调试的时候,由于执行顺序不同了,bug又不出现了。
为了解决这个问题,我想了一个办法,就是利用同步事件,强行把多线程的程序按照预定好的顺序去执行。比如说有两个线程,一开始就让线程1运行,线程2睡觉,线程1运行到某个特定的点后,就换线程2运行,线程1睡觉。任何时候,都只有一个线程可以运行。我们可以在多次运行的过程中使用不同的执行顺序,如果按某种执行顺序运行之后bug浮现了,那么就把这种顺序记录下来。然后按照这种顺序进入调试,找出bug。
用这种方法一定要谨慎,如果你的程序里本来就有线程同步的代码,再加上这些强制的同步,可以会导致死锁。
=============================================
Visual Studio调试(Debug)小技巧
http://hi.baidu.com/liudong/blog/item/d0434c08c6d315970b7b827e.html
在Visual Studio 运行调试过程中,有两个非常有用的小工具:
1,Command Window。
2,Immediate Window。
Command Window用于执行一些有用的命令。例如创建一个新文件可以使用命令:
File.NewFile "abc.cpp"
有趣的是提供一个和Dos中cls类似命令。
ImmediateWindow提供在调试过程中运行一些有用的表达式,或者查看程序中的变量的值。例如程序中有两个变量a和b,现在要测试ab的值,可以使用下面的命令 (注意前面的>不可省略):
>Debug.Print a b
MSDN参考:
ms-help://MS.MSDNQTR.v90.en/dv_vscmds/html/48711628-1909-4713-a73e-d7b714c77f8a.htm
ms-help://MS.MSDNQTR.v90.en/dv_vscmds/html/d33e7937-73f3-4c69-9df0-777a8713c6f2.htm
====================================
Visual Studio2008环境与VC6.0的环境存在着比较大的区别,下面就一些小小的区别在这里做一些探讨,欢迎指教!
1、如果是调试控制台程序,很多时候点击“启动调试”后是一闪而过,此时可有两种方法让cmd下dos调试屏幕暂停:
A:不要直接点击vs2008的“启动调试”按钮,而是按Control F5组合键。
B:在主函数main()里“return 0;”前加上两句:cin.get();样式如下
eg: int main()
{
cout<<"这是一个调试屏幕暂停的例子!"<
cin.get();
return 0;
}
2、类、函数和变量是C 编译器的标准组件,它们都放置在名称空间std中(此时头文件没有后缀名h)。在vs2008 C下,如果不包含using namespace std;指令,那么必须使用std::前缀,如
#include "stdafx.h"
#include
void simon(int); //函数原型
//using namespacestd; 没有使用std名称空间
int _tmain(int argc, _TCHAR* argv[])
{
simon(3);
std::cout<<"请输入一个整数: ";//必须使用std::前缀,以下都是
int count;
std::cin>>count;
simon(count);
std::cout<<"完成!"<
std::cin.get();//这两行是使调试屏幕暂停,不会一闪而过
std::cin.get();//让程序等待键击
return 0;//退出主函数
}
void simon(int n) //自定义函数
{
std::cout<<"现在整数是"<
下面是一个使用using namespace std;名称空间的对比:
#include "stdafx.h"
#include
void simon(int);
using namespace std; //这是一个使用std;名称空间的例子
int _tmain(int argc, _TCHAR* argv[])
{
simon(3);
cout<<"请输入一个整数: ";
int count;
cin>>count;
simon(count);
cout<<"完成!"<
cin.get();
cin.get();
return 0;
}
void simon(int n)
{
cout<<"现在整数是:"<
还有一种方法,就是既不使用std;名称空间,也不使用std::前缀,而是使用using编译指令。如:
#include "stdafx.h"
#include
using std::cout; //直接使用using指令
using std::cin;
using std::endl;
void simon(int);
//using namespace std; //这是一个既没有使用std;名称空间也没有使用std::前缀的例子
int _tmain(int argc, _TCHAR* argv[])
{
simon(3);
cout<<"请输入一个整数: ";
int count;
cin>>count;
simon(count);
cout<<"完成!"<
cin.get();
cin.get();
return 0;
}
void simon(int n)
{
cout<<"现在整数是:"<
数据断点 当数据所在内存位置变化时,调试器将会中断。然而,这是唯一可能在一个时间创建4这样的硬件的数据断点。数据断点只能在编译的过程中添加,可以通过菜单(编译>新断点>新数据断点)或者通过断点窗口来添加。
|
凡程子
|
您可以使用一个内存地址或地址表达式。即使你能看到堆栈上的两个值,我认为通常当堆上的值被改变时,这项功能才会有用。这对 识别内存损坏是一个很大的帮助。
|
沙滩哥
|
在下面的例子中,指针的值已经更改为所指向对象的值。为了能找出什么地方做的更改,我在指针值存储的位置设置了一个断点,如 &ptr (注意这是在指针初始化后发生的)。当数据更改后,以为着某人更改了指针的值,调试器终止,然后能发现哪些代码引起了这个改变。
额外阅读:
|
小编辑
|
线程重命名 当你调试多线程应用是,Threads窗口会显示创建了哪些线程,以及当前的线程。线程越多,你就越难找到你要找的线程(特别是当同一段程序,被多个线程同时执行的时候,你就不知道当前执行的是哪个线程实例)
调试器允许你给线程重新命名。用右键单击一个线程,并重命名。
|
guoliang
|
也可以以程式设计方式命名线程,虽然这有点棘手而且线程启动后必须去做的,否则调试器将以它的默认命名规定重新将其初始化,下面的函数显示了如何定义和使用一个线程。
|
凡程子
|
指定线程设置断点 对于多线程应用程序,另一个有用的技巧是在指定线程、进程甚至计算机设置断点。可以使用断点的Filer命令来实现这种功能。
调试器允许你使用线程名、线程ID、进程名、进程ID和机器名的不同组合(使用AND、OR、NOT连接)。掌握如何设置线程名称也使得这种过滤技术操作更为简单。 辅助阅读:
|
|
(不准确的)定时执行 在我之前的文章中我曾提及在Watch窗口中使用伪变量。其中一个没有提及的是@clk,可显示一个计数器的值,用于获得两个断点之间代码执行所需要的大体时间,该值的单位为毫秒(ms)。但是,这种方法不能用于配置程序执行。你应该使用Visual Studio Profiler或者性能计时器来完成这些配置。 |
|
通过在Watch窗口或者Immediate窗口中添加@clk=0来重置计时器。因此,若需要计算末段代码执行所需要的时间,做下列处理:
|
注意网上有技巧说需要在Watch窗口中添加两条表达式:@clk和@clk=0,据说可以每次在断点执行的位置重置计时器。这种技巧只能在较低版本的Visual Studio中使用,但是不能在高版本VS中使用,例如VS2005(作者做过测试,vs2005不支持这种技巧)以及更高版本。 辅助阅读:
|
|
提示15:格式化数字 当你使用Watch或者Quick Watch窗口查看变量时,显示这些数值是用默认的预定义可视化格式。当变量是数字时,显示形式按照他们的类型(int、float、double)来的,并且使用十进制显示。然而,你可以设置调试器在显示数字的使用使用不同的类型,或者使用不同的进制。 |
|
改变变量显示类型可以在变量前添加以下前缀:
改变变量显示的进制可以在变量前添加以下前缀:
辅助阅读:
|
提示16:格式化内存数据 除了数字,debugger还可以在Watch窗口中显示格式化的内存数据,最长为64字节。你可以在表达式(变量或者内存地址)后面添加下面的后缀来格式化数据:
附加阅读:
|
|
提示17:在系统DLL调用处暂停 有时在DLL的某个函数被调用时暂停是很有用,特别是系统DLL(比如kernel32.dll、user32.dll)。实现这种暂停需要使用原生debugger提供的上下文运算符。你可以设定断点位置、变量名或者表达式:
大括号内可以是函数名、源代码及模块的任意组合,但是逗号不能省略。 |
|
举个例子如果我们需要在CreateThread函数调用时暂停。这个函数是从kernel32.dll导出的,因此上下文运算符应该是这样子的:{,,kernel32.dll}CreateThread。然而,这样并不行,因为该运算符需要CreateThread修饰之后的名字。可以使用 DBH.exe 来获得一个特定函数的修饰名(编译器编译生成)。
|
|
转至:http://blog.csdn.net/discxuwei/article/details/6752620