首页未分类 › VC++ 问题

VC++ 问题

1,关于Debug和Release之本质区别的讨论

 

一、Debug 和 Release 编译方式的本质区别

Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。

Debug 和 Release 的真正秘密,在于一组编译选项。下面列出了分别针对二者的选项(当然除此之外还有其他一些,如/Fd /Fo,但区别并不重要,通常他们也不会引起 Release 版错误,在此不讨论)

Debug 版本:

/MDd /MLd 或 /MTd 使用 Debug runtime library(调试版本的运行时刻函数库)

/Od 关闭优化开关

/D “_DEBUG” 相当于 #define _DEBUG,打开编译调试代码开关(主要针对

assert函数)

/ZI 创建 Edit and continue(编辑继续)数据库,这样在调试过

程中如果修改了源代码不需重新编译

/GZ 可以帮助捕获内存错误

/Gm 打开最小化重链接开关,减少链接时间

Release 版本:

/MD /ML 或 /MT 使用发布版本的运行时刻函数库

/O1 或 /O2 优化开关,使程序最小或最快

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

/GF 合并重复的字符串,并将字符串常量放到只读内存,防止

被修改

实际上,Debug 和 Release 并没有本质的界限,他们只是一组编译选项的集合,编译器只是按照预定的选项行动。事实上,我们甚至可以修改这些选项,从而得到优化过的调试版本或是带跟踪语句的发布版本。

 二、哪些情况下 Release 版会出错

有了上面的介绍,我们再来逐个对照这些选项看看 Release 版错误是怎样产生的

1. Runtime Library:链接哪种运行时刻函数库通常只对程序的性能产生影响。调试版本的 Runtime Library 包含了调试信息,并采用了一些保护机制以帮助发现错误,因此性能不如发布版本。编译器提供的 Runtime Library 通常很稳定,不会造成 Release 版错误;倒是由于 Debug 的 Runtime Library 加强了对错误的检测,如堆内存分配,有时会出现 Debug 有错但 Release 正常的现象。应当指出的是,如果 Debug 有错,即使 Release 正常,程序肯定是有 Bug 的,只不过可能是 Release 版的某次运行没有表现出来而已。

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

(1) 帧指针(Frame Pointer)省略(简称 FPO ):在函数调用过程中,所有调用信息(返回地址、参数)以及自动变量都是放在栈中的。若函数的声明与实现不同(参数、返回值、调用方式),就会产生错误 ————但 Debug 方式下,栈的访问通过 EBP 寄存器保存的地址实现,如果没有发生数组越界之类的错误(或是越界“不多”),函数通常能正常执行;Release 方式下,优化会省略 EBP 栈基址指针,这样通过一个全局指针访问栈就会造成返回地址错误是程序崩溃。C++ 的强类型特性能检查出大多数这样的错误,但如果用了强制类型转换,就不行了。你可以在 Release 版本中强制加入 /Oy- 编译选项来关掉帧指针省略,以确定是否此类错误。此类错误通常有:

● MFC 消息响应函数书写错误。正确的应为

afx_msg LRESULT OnMessageOwn(WPARAM wparam, LPARAM lparam);

ON_MESSAGE 宏包含强制类型转换。防止这种错误的方法之一是重定义 ON_MESSAGE 宏,把下列代码加到 stdafx.h 中(在#include “afxwin.h”之后),函数原形错误时编译会报错

#undef ON_MESSAGE

#define ON_MESSAGE(message, memberFxn) { message, 0, 0, 0, AfxSig_lwl, (AFX_PMSG)(AFX_PMSGW)(static_cast< LRESULT (AFX_MSG_CALL CWnd::*)(WPARAM, LPARAM) > (&memberFxn) },

(2) volatile 型变量:volatile 告诉编译器该变量可能被程序之外的未知方式修改(如系统、其他进程和线程)。优化程序为了使程序性能提高,常把一些变量放在寄存器中(类似于 register 关键字),而其他进程只能对该变量所在的内存进行修改,而寄存器中的值没变。如果你的程序是多线程的,或者你发现某个变量的值与预期的不符而你确信已正确 的设置了,则很可能遇到这样的问题。这种错误有时会表现为程序在最快优化出错而最小优化正常。把你认为可疑的变量加上 volatile 试试。

(3) 变量优化:优化程序会根据变量的使用情况优化变量。例如,函数中有一个未被使用的变量,在 Debug 版中它有可能掩盖一个数组越界,而在 Release 版中,这个变量很可能被优化调,此时数组越界会破坏栈中有用的数据。当然,实际的情况会比这复杂得多。与此有关的错误有:

● 非法访问,包括数组越界、指针错误等。例如

 

j 虽然在数组越界时已出了作用域,但其空间并未收回,因而 i 和 j 就会掩盖越界。而 Release 版由于 i、j 并未其很大作用可能会被优化掉,从而使栈被破坏。

3. _DEBUG 与 NDEBUG :当定义了 _DEBUG 时,assert() 函数会被编译,而 NDEBUG 时不被编译。除此之外,VC++中还有一系列断言宏。这包括:

 

此外,TRACE() 宏的编译也受 _DEBUG 控制。

所有这些断言都只在 Debug版中才被编译,而在 Release 版中被忽略。唯一的例外是 VERIFY() .事实上,这些宏都是调用了 assert() 函数,只不过附加了一些与库有关的调试代码。如果你在这些宏中加入了任何程序代码,而不只是布尔表达式(例如赋值、能改变变量值的函数调用 等),那么 Release 版都不会执行这些操作,从而造成错误。初学者很容易犯这类错误,查找的方法也很简单,因为这些宏都已在上面列出,只要利用 VC++ 的 Find in Files 功能在工程所有文件中找到用这些宏的地方再一一检查即可。另外,有些高手可能还会加入 #ifdef _DEBUG 之类的条件编译,也要注意一下。

顺便值得一提的是 VERIFY() 宏,这个宏允许你将程序代码放在布尔表达式里。这个宏通常用来检查 Windows API 的返回值。有些人可能为这个原因而滥用 VERIFY() ,事实上这是危险的,因为 VERIFY() 违反了断言的思想,不能使程序代码和调试代码完全分离,最终可能会带来很多麻烦。因此,专家们建议尽量少用这个宏。

4. /GZ 选项:这个选项会做以下这些事

(1) 初始化内存和变量。包括用 0xCC 初始化所有自动变量,0xCD ( Cleared Data ) 初始化堆中分配的内存(即动态分配的内存,例如 new ),0xDD ( Dead Data ) 填充已被释放的堆内存(例如 delete ),0xFD( deFencde Data ) 初始化受保护的内存(debug 版在动态分配内存的前后加入保护内存以防止越界访问),其中括号中的词是微软建议的助记词。这样做的好处是这些值都很大,作为指针是不可能的(而且 32 位系统中指针很少是奇数值,在有些系统中奇数的指针会产生运行时错误),作为数值也很少遇到,而且这些值也很容易辨认,因此这很有利于在 Debug 版中发现 Release 版才会遇到的错误。要特别注意的是,很多人认为编译器会用 0 来初始化变量,这是错误的(而且这样很不利于查找错误)。

(2) 通过函数指针调用函数时,会通过检查栈指针验证函数调用的匹配性。(防止原形不匹配)

(3) 函数返回前检查栈指针,确认未被修改。(防止越界访问和原形不匹配,与第二项合在一起可大致模拟帧指针省略 FPO )

通常 /GZ 选项会造成 Debug 版出错而 Release 版正常的现象,因为 Release 版中未初始化的变量是随机的,这有可能使指针指向一个有效地址而掩盖了非法访问。

除此之外,/Gm /GF 等选项造成错误的情况比较少,而且他们的效果显而易见,比较容易发现。

 

 

2,利用VS2005进行dump文件调试

前言:利用drwtsn32或NTSD进行程序崩溃处理,都可以生成可用于调试的dmp格式文件。使用VS2005打开生成的DMP文件,能很方便的找出BUG所在位置。本文将讨论以下内容:

1 程序编译选项

2 利用VS2005 分析dump文件

3 常见问题讨论

一、      程序编译选项

PDB files contains all debug information like type definition and function prototype.When application crashes, we need the PDB files to analyze the root cause, so make sure these PDB files will be created when building it. You must do the following setting:

C/C++\General\Debug Information Format=Program Database (/Zi).

clip_image001_thumb

1.1 调试信息格式

Linker\Debugging\Generate Program Database File=”Name and location of your PDB files”

clip_image002_thumb

1.2 PDB文件输出路径

PDB文件路径最好设置在同一个文件夹中,这样方便dmp文件调试时调用。

调试时,所有的PDB文件和源文件必须严格匹配(the PDB filesshould be the one generated by build the source code),并存储在一个安全的位置。当客户报告了一个错误时,你需要这些文件来帮忙以便定位错误于源代码中并解决问题。

二、      VS2005分析dump文件

In this simple application, there is an unhandled Access Violation Reading exception, because GetNameFromDatabase returns a NULL pointer, and this pointer is passed into IsPrefix and then it’s used directly without NULL pointer checking.

clip_image003_thumb

1.3 演示代码

 

利用Release模式编译该测试程序,在客户机上运行该程序,将根据NTSD设置生成相对应的DMP格式文件。

可以使用Visual Studio.Net、NTSD或是其他的调试工具对DMP格式文件进行分析。

 

l         Start Visual Studio.Net

Click File\Open Solution and make sure the files of type is *.dmp then click Open.

clip_image004_thumb

1.3 Open Dump File (GUI)

 

l         Set Symbol Path

Click Tools\Options, Debugging\Symbols,增加PDB文件路径。若调试的程序需要微软基础库的PDB信息,可以增加一个路径为:

http://msdl.microsoft.com/download/symbols

在界面下方Cache Symbol From symbol…选择本地存储这些Symbols的路径。

clip_image005_thumb

1.4 Symbol Path

如果DMP文件没有放入本身 PDB文件所在目录,也可以在此处增加一个本地目录。点OK后,VS2005将从网络中下载所需要的Symbols,需要等待一段时间。如果是多次调试同 一个程序错误所生成的DMP文件,可以在对话框中选择“Search the above locations only when symbols are loaded manually”。从而可以节省网络带宽。

 

l         Set Source code path

Open Solution Property Pages and set the source code path.

clip_image006_thumb

1.5 属性菜单

clip_image007_thumb

1.6 Debug Source Files

 

l         Start to Debug the Dump File

Click the Debug menu, it will ask you to save as a solution, save it. Then it will go to the line which caused the crash of your application.

clip_image008_thumb

1.7 调试窗口,定位到源代码

 

三、      常见问题讨论

1  Dump文件放在哪里?

Dump文件不用非要放在你编译出来的位置,你完全可以建立一个新的文件夹来放它。但若不是存放在编译出来的位置,需要将编译生成的PDB文件拷贝到Dump文件目录,或是利用VS2005打开Dump文件后,设置PDB文件路径。参照图1.4。

 

2 如何恢复当时的现场?

可能你要问,怎么可能,这个dump文件可是用户发给我的,我不可能去用户家里调试吧?这个恢复现场可不是指的非要到那台机器上去,而是要把产生dump文件对应的二进制文件拿到。

但是恢复现场需要所有的二进制文件都要对应,你一定要有导致用户崩溃的那些Exe和DLL。既然是你发布的程序,Exe文件当然你会有。所以这里只考虑DLL就行了。

Dump文件中记录了所有DLL文件的版本号和时间戳,所以你一定可以同过某种途径拿到它。如果你能从用户那里拿到最好,如果不方便,用户不可能用的是我们平常不常用的操作系统,所以找个有对应系统的机器一般都会有。但是记住不仅是文件名称要一致,还要核对版本和时间戳,如果不同一样没有办法用。

如果客户用了某个特殊的补丁怎么办?

其实这个问题也很好解决,只要它不阻碍阅读堆栈,就不用管它,调试Dump和运行程序不一样,缺少一两个DLL没有任何问题。

 

3 如果真的需要怎么办?

符号文件现在主要是指PDB文件。

如果没有符号文件,那么调试的时候可能导致堆栈错误。

如果你丢失了这个发布版本中你编译出来的那些exe和DLL的PDB,那么这个损失是严重的,重新编译出来的版本是不能使用的。

我自己的DLL都有了,可是缺的是系统的DLL的对应PDB文件怎么办?图1.4中已经介绍了方法。微软在它的符号数据库上为我们提供了所有的PDB文件,还有部分非关键DLL。设置好后程序将自动下载需要的PDB及DLL文件。

 

4 拿到需要的文件了,这些文件应该放在哪里?

符号数据库中的文件不用动,把其它的exe和DLL、PDB文件放在dump文件目录里就行了。

 

5 我用的是VS2005,明明有源代码,为什么显示不了?

这个是dump调试的最头痛问题,代码可能已经改过了,即使你从SVN拿到当时的版本,时间戳也是错的,VS2005就是不让你显示代码。其实只要在

Tools\OptionsDebugging\General中去掉

Require source files to exactly match the original version的复选就行了。

 

3, 关于MFC下检查和消除内存泄露的技巧

 

编译环境

VC++6.0

技术原理

检测内存泄漏的主要工具是调试器和 CRT 调试堆函数。若要启用调试堆函数,请在程序中包括以下语句:

1.#define CRTDBG_MAP_ALLOC
2.#include < stdlib.h >
3.#include < crtdbg.h >

注意 #include 语句必须采用上文所示顺序。如果更改了顺序,所使用的函数可能无法正确工作。

通过包括 crtdbg.h,将 malloc 和 free 函数映射到其“Debug”版本_malloc_dbg 和_free_dbg,这些函数将跟踪内存分配和释放。此映射只在调试版本(在其中定义了 _DEBUG)中发生。发布版本使用普通的 malloc 和 free 函数。

#define 语句将 CRT 堆函数的基版本映射到对应的“Debug”版本。并非绝对需要该语句,但如果没有该语句,内存泄漏转储包含的有用信息将较少。

在添加了上面所示语句之后,可以通过在程序中包括以下语句来转储内存泄漏信息:

1._CrtDumpMemoryLeaks();

当在调试器下运行程序时,_CrtDumpMemoryLeaks 将在“输出”窗口中显示内存泄漏信息。内存泄漏信息如下所示:

1.Detected memory leaks!
2.  
3.Dumping objects ->
4.  
5.C:PROGRAM FILESVISUAL STUDIOMyProjectsleaktestleaktest.cpp(20) : {18} normal block at 0x00780E80, 64 byteslong.
6.  
7.Data: <        > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
8.Object dump complete.

如果不使用 #define _CRTDBG_MAP_ALLOC 语句,内存泄漏转储如下所示:

1.Detected memory leaks! 
2.Dumping objects -> 
3.{18} normal block at 0x00780E80, 64 byteslong
4.Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
5.Object dump complete.

未定义 _CRTDBG_MAP_ALLOC 时,所显示的会是:

内存分配编号(在大括号内)。

块类型(普通、客户端或 CRT)。

十六进制形式的内存位置。

以字节为单位的块大小。

前 16 字节的内容(亦为十六进制)。

定义了 _CRTDBG_MAP_ALLOC 时,还会显示在其中分配泄漏的内存的文件。文件名后括号中的数字(本示例中为 20)是该文件内的行号。

转到源文件中分配内存的行

在”输出”窗口中双击包含文件名和行号的行。

-或-

在”输出”窗口中选择包含文件名和行号的行,然后按 F4 键。

1._CrtSetDbgFlag

如果程序总在同一位置退出,则调用 _CrtDumpMemoryLeaks 足够方便,但如果程序可以从多个位置退出该怎么办呢?不要在每个可能的出口放置一个对 _CrtDumpMemoryLeaks 的调用,可以在程序开始包括以下调用:

1._CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );

该语句在程序退出时自动调用 _CrtDumpMemoryLeaks。必须同时设置 _CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF 两个位域,如上所示。

说明

在VC++6.0的环境下,不再需要额外的添加

1.#define CRTDBG_MAP_ALLOC 
2.#include < stdlib.h > 
3.#include < crtdbg.h >

只需要按F5,在调试状态下运行,程序退出后在”输出窗口”可以看到有无内存泄露。如果出现

1.Detected memory leaks! 
2.Dumping objects ->

就有内存泄露。

确定内存泄露的地方

根据内存泄露的报告,有两种消除的方法:

第一种比较简单,就是已经把内存泄露映射到源文件的,可以直接在”输出”窗口中双击包含文件名和行号的行。例如

1.Detected memory leaks! 
2.Dumping objects -> 
3.C:PROGRAM FILESVISUAL STUDIOMyProjectsleaktestleaktest.cpp(20) : {18} normal block at 0x00780E80, 64 byteslong
4.Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
5.Object dump complete.
6.C:PROGRAM FILESVISUAL STUDIOMyProjectsleaktestleaktest.cpp(20)

就是源文件名称和行号。

第二种比较麻烦,就是不能映射到源文件的,只有内存分配块号。

1.Detected memory leaks! 
2.Dumping objects -> 
3.{18} normal block at 0x00780E80, 64 byteslong
4.Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
5.Object dump complete.

这种情况我采用一种”试探法”。由于内存分配的块号不是固定不变的,而是每次运行都是变化的,所以跟踪起来很麻烦。但是我发现虽然内存分配的块号是变化的,但是变化的块号却总是那几个,也就是说多运行几次,内存分配的块号很可能会重复。因此这就是”试探法”的基础。

1.先在调试状态下运行几次程序,观察内存分配的块号是哪几个值;

2.选择出现次数最多的块号来设断点,在代码中设置内存分配断点。

添加如下一行(对于第 18 个内存分配):

1._crtBreakAlloc = 18;

或者,可以使用具有同样效果的 _CrtSetBreakAlloc 函数:

1._CrtSetBreakAlloc(18);

3.在调试状态下运行序,在断点停下时,打开”调用堆栈”窗口,找到对应的源代码处;

4.退出程序,观察”输出窗口”的内存泄露报告,看实际内存分配的块号是不是和预设值相同,如果相同,就找到了;如果不同,就重复步骤3,直到相同。

5.最后就是根据具体情况,在适当的位置释放所分配的内存。

 

4,try catch   __try __except

以前都是用try{} catch(…){}来捕获C++中一些意想不到的异常, 今天看了Winhack的帖子才知道,这种方法在VC中其实是靠不住的。例如下面的代码:

  1. try
  2. {
  3. BYTE* pch ;
  4. pch = ( BYTE*)00001234 ;  //给予一个非法地址
  5.  
  6. *pch =6 ; //对非法地址赋值,会造成Access Violation 异常
  7. }
  8. catch()
  9. {
  10. AfxMessageBox( catched) ;
  11. }

这段代码在debug下没有问题,异常会被捕获,会弹出”catched”的消息框。 但在Release方式下如果选择了编译器代码优化选项,则VC编译器会去搜索try块中的代码, 如果没有找到throw代码, 他就会认为try catch结构是多余的, 给优化掉。 这样造成在Release模式下,上述代码中的异常不能被捕获,从而迫使程序弹出错误提示框退出。

那么能否在release代码优化状态下捕获这个异常呢, 答案是有的。 就是__try, __except结构, 上述代码如果改成如下代码异常即可捕获。

  1. __try
  2. {
  3. BYTE* pch ;
  4. pch = ( BYTE*)00001234 ;  //给予一个非法地址
  5.  
  6. *pch =6 ; //对非法地址赋值,会造成Access Violation 异常
  7. }
  8. __except( EXCEPTION_EXECUTE_HANDLER)
  9. {
  10. AfxMessageBox( catched) ;
  11. }

但是用__try, __except块还有问题, 就是这个不是C++标准, 而是Windows平台特有的扩展。 而且如果在使用过程中涉及局部对象析构函数的调用,则会出现C2712 的编译错误。 那么还有没有别的办法呢?

当然有, 就是仍然使用C++标准的try{}catch(..){}, 但在编译命令行中加入 /EHa 的参数。这样VC编译器不会把try catch模块给优化掉了。

发表评论