vc程序调试 entry point,VC++调试

  vc程序调试 entry point,VC++调试

  1前言

  当程序的运行结果与程序员的预期不同时,如崩溃、计算值不正确、内存访问冲突等。需要调试。

  2.调试前做好准备。

  因为程序调试是一项非常耗时的工作,所以很难估计需要多长时间。所以在调试之前,一定要做好充分的准备,尽量避免做无用功:

  1.结构化的测试步骤,使程序有规律地出错或者出错的概率尽可能高。

  2.被调试的程序和相关库是最兼容的版本。

  3.临时工程文件,如。ncb被删除。

  4.整个项目被重新编译。

  5.应用程序的链接路径与调试路径一致。

  6.单体测试全部通过。

  3错误位置和原因的确定

  3.1几种典型错误的原因

  1记忆莫名失效。

  原因:内存指针在很多地方被引用和释放。

  2多线程条件下的崩溃

  原因:使用SendMessage导致线程死锁,可以人为添加到消息循环中。

  3多线程条件下的内存访问冲突

  原因:内存被多个线程同时使用,线程同步机制(消息队列,信号灯等。)可以添加。

  4内存访问冲突

  原因:内存越界(如字符串复制,内存复制)

  5窗口消息的顺序

  原因:如果窗口没有初始化,就会被使用。

  3.2定位错误的位置

  对代码理解的越深,就能越准确的确定代码的错误位置。必要的话要画出相关代码的类图和时序图。

  2从IDE调用堆栈中确定错误位置和原因。

  3从Win32 API或MFC类库函数返回的错误代码确定错误原因。返回错误代码的含义可以从MSDN或源代码中找到,也可以通过VC工具错误查找。

  在代码中添加带编号的TRACE语句或MessageBox(发布版本),以逐渐缩小调试范围。

  5对于崩溃或偶然现象,可以通过逐渐注释掉代码来确定崩溃的位置和原因。

  6如果崩溃或意外现象是新的,可以通过比较当前版本和以前版本的差异来确定位置和原因。

  4调试模式下的调试

  4.1几种调试技巧

  使用断言

  ASSERT(ASSERT_VALID)宏只捕捉程序“调试”版本中的程序错误。这个宏在“发布”版本中不生成任何代码。

  使用跟踪

  以下示例只能在调试中显示,

  a)跟踪

  CString csTest=" test

  TRACE("CString是%s/n ",csTest);

  b)ATLTRACE

  c)AfxDump

  AfxDump要求转储的对象从CObject类继承,并实现Dump方法。

  CTime time=CTime:GetCurrentTime();

  #ifdef _DEBUG

  afxDump时间"/n ";

  #endif

  4.1.3如何在循环语句中设置断点

  例如,在下面的代码中,当nRet==0时,程序被认为有错误。但是这个时候如何定位I的值呢?

  1)将光标放在要调试的语句前面。

  2)按住CTRL键,选择换行处的行号

  3)选择条件

  输入nRet==0;在[输入表达式…];单击确定。运行程序,弹出错误对话框后点击重试,程序会在断点处停止执行。可以看出,循环变量的值,比如I,此时等于9。表示I==9时的误差。

  在CTRL B中,在[输入次数…]中输入7,当循环变量i=8时程序会停止。可以输入错误的函数进行调试。

  参见:

  VCDebugSample/src/debug main/debug mindlg . CPP-

  void cdebugmandlg:OnButtonSetBkpt()

  数据断点(Data Breakpoint)

  void cdebugmandlg:OnButtonDataBkpt()

  {

  //TODO:在此添加控件通知处理程序代码

  char SZ name 1[10];

  char SZ name 2[4];

  strcpy(szName1,深圳);//A

  CString str1

  str1。格式( %s/n ,SZ name 1);

  TRACE(str 1);

  strcpy(szName2, vck base );//B

  CString str2

  str2。格式( %s/n ,SZ name 2);

  TRACE(str 2);

  str1。格式( %s/n ,SZ name 1);

  TRACE(str 1);#include stdafx.h

  这个程序的输出是

  深圳1:深圳

  sz21: vckbase

  sz1:日月光

  szName1是什么时候修改的?因为szName1代码没有明显的修改。我们可以先在A行设置一个公共断点,F5运行程序,程序停在A行,然后我们设置一个数据断点。如下图:

  F5继续运行,程序停在B行,表示B处的代码修改了szName1。为什么没有在B位修改szName1?但是调试器指示这一行,一般是正确的,还是静下心来看看程序比较好。哦,你发现szName2只有4个字节,而strcpy有7个字节,所以覆盖了szName1。数据断点不仅对变量变化有效,还可以设置变量是否等于某个值。比如你可以把图2中的红圈改成条件“szName2 [0]== y ”,那么断点就会在szName2的第一个字符是y的时候开始,可以看出数据断点和断点的一个很大的区别就是你不用指定在哪一行代码上设置断点。

  其他

  1在调用堆栈窗口设置断点,选择一个函数,按F9设置断点。这样就可以从深层的函数调用中快速返回到需要的函数。2设置下一条语句命令(调试时右击debug中的命令)该命令用于将程序的指令指针(EIP)指向不同的代码行。比如你正在调试上面的代码,它运行在A行,但是你不想运行B行和c行,这时候你可以右击D行,然后“设置下一条语句”。调试器不会执行B行和c行,只要是在同一个函数中,这个指令可以在跳转之前或之后任意执行。灵活使用该功能可以节省大量的调试时间。3 Window watch window支持丰富的数据格式化功能。如果输入0x65,u,右栏将显示101。显示实时windows API调用的错误:在左栏输入@err,hr。调用"监视"窗口中的函数。作为提醒,在调用该函数后,请立即清除“监视”窗口中的该函数,否则,调试器将在单步调试期间的每一步都调用该函数。4消息断点不是很实用。基本上可以用前面提到的断点来代替。

  4.2 dll的调试

  4.2.1DLL的测试和调试方法

  DLL的测试和调试通常使用客户端。在客户端调用DLL的API之前添加断点,可以直接进入DLL的内部调试。

  4 . 2 . 2加载库失败。

  通常的原因是加载的dll同时加载了其他dll或组件。当这些dll或组件不存在时,loadLibrary将不会成功。可以使用visual studio工具-> depends将要加载的dll拖动到depends中,从中可以发现那些dll或者组件是不存在的。

  4.2.3无法在dll中注册COM组件(Regsvr32)。

  大多数失败也是由于dll加载不成功造成的。这是因为在dll内部注册组件时,必须首先加载dll。如果加载失败,也会导致regsvr32失败。也可以采用上面的方法。

  4.3 com的调试

  4.3.1跟踪参考计数

  若要跟踪ATL中的引用数,请在包含atlbase.h之前添加以下代码行:

  #定义_ ATL _调试_接口

  每次调用AddRef或Release时,该语句都会使输出窗口显示接口的当前引用号以及相应的类名和接口名。

  将断点设置为:void cdebugmainlg:onbuttontracerefcnt()

  4 . 3 . 2查询接口调用调试

  若要调试ATL中的QueryInterface调用,请在包含atlcom.h之前添加以下定义:

  #define _ATL_DEBUG_QI

  然后,在调试时,在输出窗口中查找在对象上查询的每个接口的名称。

  将断点设置为:void cdebugmainlg:onbuttontraceqi()

  4.4多线程调试

  说到多线程通信,要注意在合适的地方设置断点。

  参见:

  void cdebugmandlg:OnButtonProcThd()

  void cdebugmandlg:OnButtonWinThd()

  void cdebugmandlg:OnButtonThdMsg()

  在示例中,首先启动一个运行thread函数的线程(Thread1),然后启动一个CWinThread类型的线程(Thread2)。然后用OnButtonThdMsg函数中的event通知Thread1,Thread1收到事件后用ThreadMessage通知Thread2。其实还有窗口的主线程,也就是OnbuttonThdMsg运行的线程,所以如果要在整个进程的根中点击Button后运行,就要在每个线程中适当的位置设置断点。

  在程序的调试状态下,选择debug- Threads,可以查看当前正在运行的线程,并且可以暂停一个线程,继续执行等。

  5调试版本和发布版本之间的差异

  Debug通常称为调试版本,包含调试信息,没有任何优化,方便程序员调试程序。Release称为release version,它往往会进行优化,使程序在代码大小和运行速度上达到最优,让用户用得好。

  调试和发布的真正秘密在于一组编译选项。下面列出了两者的选项(当然除此之外还有其他的,比如/Fd /Fo,但区别并不重要,通常不会造成发布错误,这里就不讨论了)

  调试版本

  参数

  意义

  /MDd /MLd或/MTd

  使用调试运行时库(调试版本的运行时库)

  /Od

  关闭优化开关

  /D

   DEBUG 相当于#define _DEBUG,打开编译和调试代码开关(主要针对assert函数)。

  /子

  创建一个“编辑并继续”数据库,这样,如果在调试过程中修改了源代码,就不需要重新编译它。

  /GZ

  可以帮助捕捉内存错误。

  发布候选人

  参数

  意义

  /MD /ML或/MT

  使用运行时库的发布版本。

  /O1或/O2

  优化开关,使程序最小或最快。

  /D

  “NDEBUG”关闭条件编译调试代码开关(即不编译assert函数)。

  /GF

  合并重复的字符串,并将字符串常量放在只读内存中以防止它们被修改。

  实际上,Debug和Release之间并没有本质的界限,它们只是一组编译选项,编译器只是按照预定的选项进行操作。事实上,我们甚至可以修改这些选项,以获得优化的调试版本或带有跟踪语句的发布版本。

  什么情况下发布版本会出错?

  有了上面的介绍,我们再来逐一比较这些选项,看看发布版本错误是如何产生的。

  1.运行时库:链接哪个运行时库通常只会影响程序的性能。运行时库的调试版本包含调试信息,并采用一些保护机制来帮助发现错误,因此其性能不如发布版本。编译器提供的运行时库通常是稳定的,不会导致发布版本错误;但是由于Debug的运行时库加强了对错误的检测,比如堆内存分配,有时候会出现Debug有错误但是正常释放的情况。需要指出的是,如果调试错了,即使发布正常,程序肯定有Bug,但可能是发布版本的某次运行没有显示出来。

  2.优化:这是产生错误的主要原因,因为关闭优化时,基本上是直接翻译源程序,而打开优化时,编译器会做一系列假设。主要有以下几种错误:

  1.框架指针省略(简称FPO):在函数调用过程中,所有的调用信息(返回地址、参数)和自动变量都放在堆栈上。如果函数的声明与实现(参数、返回值、调用方法)不同,就会产生错误。然而,在调试模式下,堆栈访问是通过存储在EBP寄存器中的地址实现的。如果没有数组越界(或者“不多”越界)之类的错误,函数通常可以正常执行。在释放模式下,优化会省略EBP堆栈基址指针,这样通过全局指针访问堆栈会导致返回地址错误和程序崩溃。

  c的强类型特性可以检查出这些错误中的大部分,但是如果使用强制类型转换就做不到了。可以在发布版本中强制使用/Oy- compilation选项关闭帧指针省略,从而判断是否存在这种错误。这类错误通常包括:MFC消息响应函数编写错误。正确的说法应该是:

  afx_msg LRESULT OnMessageOwn

  (WPARAM wparam,LPARAM LPARAM);

  ON_MESSAGE宏包含强制类型转换。防止此错误的方法之一是重新定义ON_MESSAGE宏,并将以下代码添加到stdafx.h中(在#include afxwin.h 之后)。当函数原型错误时,编译会报错。

  #undef打开消息

  #define ON_MESSAGE(message,memberFxn) /

  {

  消息,0,0,0,AfxSig_lwl,/

  (AFX_PMSG)

  (static _ cast LRESULT(AFX _ MSG _ CALL/

  CWnd:*)(WPARAM,LPARAM) ( memberFxn)

  },

  2.volatile变量:volatile告诉编译器这个变量可能会在程序外被以未知的方式修改(比如系统、其他进程和线程)。为了提高程序性能,优化程序往往会将一些变量放在寄存器中(类似于register关键字),而其他进程只能修改变量所在的内存,而寄存器中的值保持不变。

  如果你的程序是多线程的,或者你发现一个变量的值与你期望的不同,并且你确定它已经被正确设置,你很可能会遇到这样的问题。这种错误有时表明程序在最快优化时出错,而最小优化是正常的。尝试在你认为可疑的变量中加入volatile。

  3.变量优化:优化器将根据变量的用途优化变量。比如函数中有一个未使用的变量,在调试版本中可能会掩盖一个数组越界,而在发布版本中,这个变量很可能会被优化,越界的数组会破坏堆栈中有用的数据。当然,实际情况会比这复杂得多。与此相关的错误包括非法访问,包括数组越界、指针错误等。例如:

  无效fn(无效)

  {

  int I;

  I=1;

  int a[4];

  {

  int j;

  j=1;

  }

  a[-1]=1;

  //当然误差不会那么明显。例如,下标是一个变量

  a[4]=1;

  }

  虽然J在数组越界的时候已经越界了,但是它的空间还没有被回收,所以I和J会把越界的部分掩盖掉。但是发布版本可能会因为I和J没有发挥太大作用而优化,会破坏堆栈。

  3.DEBUG和NDEBUG:当定义了_DEBUG时,assert()函数将被编译,但当定义了NDEBUG时则不会。另外,TRACE()宏的编译也是由_DEBUG控制的。

  所有这些断言只在调试版本中编译,但在发布版本中被忽略。唯一的例外是VERIFY()。其实这些宏都调用了assert()函数,只是附上了一些与库相关的调试代码。如果你给这些宏添加任何程序代码,而不仅仅是布尔表达式(比如赋值,可以改变变量值的函数调用等等。),那么发布版本将不会执行这些操作,从而导致错误。新手容易犯这种错误,搜索方法简单,因为上面列出了这些宏。只需使用VC的在文件中查找功能,在项目的所有文件中找到这些宏被使用的地方,并逐一检查。另外,有些专家可能会添加#ifdef _DEBUG之类的条件编译,要注意。

  顺便说一下,值得一提的是VERIFY()宏,它允许您将程序代码放入布尔表达式中。这个宏通常用于检查Windows API的返回值。有些人可能会因为这个原因滥用VERIFY()。其实很危险,因为VERIFY()违背了断言的思想,不能把程序代码和调试代码完全分开,最终可能会带来很多麻烦。所以专家建议尽量少用这个宏。

  4./GZ选项:该选项将执行以下操作:

  1.初始化内存和变量。包括0xCC初始化所有自动变量,0xCD (Cleared Data)初始化堆中分配的内存(即动态分配的内存,如new),0xDD (Dead Data)填充释放的堆内存(如delete),0xFD( deFencde Data)初始化受保护的内存(调试版在动态分配的内存前后增加受保护的内存,防止越界访问),括号内的文字是微软建议的助记符。这样做的好处是,这些值非常大,不可能作为指针使用(而且,在32位系统中,指针很少是奇数,在某些系统中,奇数指针会导致运行时错误),作为值很少遇到,这些值很容易识别,所以在调试版本中找到只在发布版本中遇到的错误非常有帮助。需要注意的是,很多人认为编译器会用0初始化变量,这是错误的(而且这也不利于发现错误)。

  2.当通过函数指针调用函数时,将通过检查堆栈指针来验证函数调用的匹配性。(防止原型不匹配)

  3.在函数返回之前检查堆栈指针,以确认它没有被修改。(为了防止越界访问与原型不匹配,它可以在与第二项结合时粗略模拟省略FPO的帧指针。)通常,/GZ选项会导致调试版本出错而发布版本正常,因为发布版本中未初始化的变量是随机的,可能会使指针指向有效地址,掩盖非法访问。况且/Gm/GF等选项造成的错误更少,其效果也很明显,容易发现。

  如何“调试”程序的发布版本

  2.在编程过程中要时刻注意测试发布版本,避免最后代码过多,时间紧张。

  3.在调试版本中使用/W4警告级别,这样可以从编译器中获得最大限度的错误信息。例如,如果(i=0)将导致/W4警告。不要忽略这些警告,通常这是由程序中的一个错误引起的。但是有时候/W4会带来很多冗余信息,比如对未使用函数参数的警告,很多消息处理函数会忽略一些参数。我们可以使用:

  #progma警告(禁用:4702)

  //禁止

  //.

  #progma警告(默认值:4702)

  //重新允许暂时禁止警告,或者使用

  #程序警告(按压,3)

  //将警告级别设置为/W3

  //.

  #程序警告(弹出)

  //重置为/W4

  要临时更改警告级别,有时只能在代码中被认为可疑的部分使用/W4。

  6发布模式下的调试

  当程序在release下出错,而在debug下没有出错时,就需要Release模式下的调试技术了。在发布模式下使用调试技术之前,请按如下方式检查代码:

  1在发布模式下没有代码被注释掉,比如ASSERT(a=f());TRACE(f());它将在release下被注释掉。

  2检查是否所有变量都已初始化。

  3检查边界错误,如以下代码:

  void函数()

  {

  char缓冲器[10];

  int计数器;

  lstrcpy(buffer, abcdefghik );//11字节的副本,包括NULL

  .

  发布模式下的调试技术:

  1在VC IDE中选择项目设置(Alt-F2),在“C /C选项卡”中将类别设置为“常规”,并将调试信息设置更改为“程序数据库”。

  2在“链接标签”中选择“生成调试信息”标签。

  3执行“全部重建”

  注意:1有时需要在发布模式下禁用优化选项。

  2如果某些代码段不能设置断点,可以添加下面的代码:

  _ _ ASM { int 3 };

  类似于有效调试模式下的ASSERT(FALSE)

  7多进程同时调试

  当一个应用程序被另一个应用程序启动,或者多个应用程序相互交互时,就需要多进程调试技术。方法是:

  1启动任务管理器,选择要调试的进程,单击鼠标右键,选择debug。

  启动VC IDE,选择下图所示的菜单,然后选择相应的进程。

  注意:这里有一个问题。可能我们要设置断点的代码在进程可以调试之前就已经被执行了,无法设置断点。有两种解决方案:

  1向带有断点的代码中添加代码。

  断言(假);

  当进程运行时,将出现以下对话框,您可以通过选择“重试”来调试它。

  2将代码添加到带有断点的代码中。

  AfxMessageBox(" debug ");//调试是任意的

  流程运行时会出现一个对话框,此时可以调试附加的改进成果。

  当多个进程同时调试时,我们可以从它们的调试窗口中看到跟踪信息。

  8杂项

  8.1响应WM_MOUSEMOVE消息的调试:

  很多情况下,如果直接在MouseMove响应函数中无条件设置断点,不方便调试。比如鼠标移动到某个区域,就无法追踪断点。此时,您可以在程序中添加更多的跟踪语句来定位它。对于发布版本,您还可以使用日志工具输出到日志文件。如果没有现成的日志工具,也可以自己写一个非常简单的日志工具。

  PreTranslateMessage,HookMessage,与上述情况类似。

  8.2在Spy中使用简单函数

  8 . 2 . 1查找工具

  Visual studio工具- Spy - ctrl F

  将圆圈图标拖到目标窗口上,您可以在下面看到关于该窗口的一些信息。

  标题:窗口的标题。

  阶级:阶级。

  样式:窗口样式:

  Rect:窗口的位置和大小

  8.2.2检查窗口的各种属性。

  在上面的窗口中选择Properties单选按钮- OK,您可以在下面的窗口中查看各种信息。

  8.2.3检查窗口中的消息。

  选择消息单选按钮- OK- ctrl o on the way,

  所有能看到的消息都列在上面的路上了。如果你只关心一种消息,比如鼠标消息。

  单击全部清除,然后选中右侧的仅鼠标复选框。

  单击确定。

  然后,所有关于鼠标的消息都可以显示在输出窗口上。如果您只想跟踪WM_LBUTTONDOWN的消息,在左侧的“要查看的消息”框中,只需选择WM_LBUTTONDOWN。

  如果你想监视某个空间,比如一个按钮,你可以在那个按钮上定位查找工具。然后按照上面的步骤进行设置。

  9摘要

  调试最重要的是你要思考,猜测你的程序可能出了什么问题,然后用你的调试器来确认你的猜测。熟练运用这些技巧无疑会加快这个过程。另外,在调试过程中,不要局限于一种方法。几种方法结合在一起可以加快错误定位,从而快速解决问题。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

相关文章阅读

  • vs2015打包安装程序,vs2015程序打包,VS2022实现VC++打包生成安装文件图文详细历程
  • vc++6.0的快捷键,vc 快捷键
  • vc++6.0的快捷键,vc 快捷键,VC6.0常用快捷键大全
  • 绘制圆角矩形的方法,c++ 画矩形,C#画圆角矩形的方法
  • 懒汉式和饿汉式代码,单列模式懒汉和饿汉,C++单例模式的懒汉模式和饿汉模式详解
  • 好用的C++编译器,c++编译软件哪个好
  • semaphore c#,c++ semaphore
  • semaphore c#,c++ semaphore,C++中Semaphore内核对象用法实例
  • dev-c++使用教程,dev c++安装教程
  • dev-c++使用教程,dev c++安装教程,Dev C++ 安装及使用方法(图文教程)
  • C里面指针常量和常量指针的区别,c++指针常量和常量指针
  • C里面指针常量和常量指针的区别,c++指针常量和常量指针,简单总结C++中指针常量与常量指针的区别
  • com组件初始化失败,c#开发com组件,C++中COM组件初始化方法实例分析
  • c++静态成员变量使用,c++静态成员函数和静态成员变量
  • c++静态成员变量使用,c++静态成员函数和静态成员变量,详解c++ 静态成员变量
  • 留言与评论(共有 条评论)
       
    验证码: