Debug 运行正常,Release版本不能正常运行(转)
2013/9/16 15:46:39 sunsmile8101 博客园
我要评论(0)
- 摘要:引言如果在您的开发过程中遇到了常见的错误,或许您的Release版本不能正常运行而Debug版本运行无误,那么我推荐您阅读本文:因为并非如您想象的那样,Release版本可以保证您的应用程序可以象Debug版本一样运行。如果您在开发阶段完成之后或者在开发进行一段时间之内从来没有进行过Release版本测试,然而当您测试的时候却发现问题,那么请看我们的调试规则1:规则1:经常性对开发软件进行Debug和Release版本的常规测试.测试Release版本的时间间隔越长,排除问题的难度越大
- 标签:debug 运行 ASE 版本
引言
如果在您的开发过程中遇到了常见的错误,或许您的Release版本不能正常运行而Debug版本运行无误,那么我推荐您阅读本文:因为并非如您想象的那样,Release版本可以保证您的应用程序可以象Debug版本一样运行。 如果您在开发阶段完成之后或者在开发进行一段时间之内从来没有进行过Release版本测试,然而当您测试的时候却发现问题,那么请看我们的调试规则1:
规则1: 经常性对开发软件进行Debug和Release版本的常规测试. 测试Release版本的时间间隔越长,排除问题的难度越大,至少对Release版本进行每周1次的测试,可以使您在紧凑的开发周期内节省潜在的排故时间. 不要随意删除Release版本需要的代码这点看起来似乎再明显不过,但却是开发人员无意中经常犯的错误,原因在于编译器编译Release版本时候会主动排除在代码中存在的宏,例如ASSERT和TRACE在Release版本会自动排除,这样导致的问题是您在这些宏当中运行的代码也被随之删除,这是非常危险的事情J,例如: ASSERT(m_ImageList.Create(MAKEINTRESOURCE(IDB_IMAGES), 16, 1, RGB(255,255,255))); 这样的代码在Debug模式不会出错,图像列表也自动创建了,然而在Release版本呢?后继使用m_ImageList对象只会造成程序的Crash!,因此ASSERT宏中尽量使用逻辑运算符作为验证。
规则 2: 不要将代码放置在仅在某种编译选项中执行的地方,对于使用_DEBUG等编译选项宏内部的代码必须不影响整个程序的使用。
规则 3: 不要使用规则2作为评判标准来删除ASSERT宏,ASSERT宏是个有用的工具,但容易使用错误. 使Debug编译模式接近Release模式如果您的Release版本存在的问题是由代码被编译器自动排除造成的,那么通过这个方法您的问题可能会重现. 一些问题的产生可能是由于不同编译选项之间预定义符号造成的,因此您可以更改编译模式下的预定义符号,从而使您的Debug模式接近Release模式,观察错误是否产生,更改编译预定义符号方法如下: a.. Alt-F7打开项目设置,在C++/C 页面,选择"General"类别,更改"_DEBUG"符号为"NDEBUG". b.. 在C++/C 页面, 选择"Preprocessor"类别,添加预定义符号"_DEBUG"到"Undefined Symbols"栏. c.. 使用"Rebuild All"重新编译如果通过上面设置,您在Release编译模式下面的问题在Debug模式下重现,那么请您依据以下步骤对您的代码进行修改: a.. 查找ASSERT排除其中的所有重要执行语句,或者将ASSERT修改为VERIFY. b.. 检查"#ifdef _DEBUG" 内所有代码,排除Release模式使用的代码. c.. 查找TRACE 排除其中的所有重要执行语句. TRACE和ASSERT一样,仅在Debug模式下编译. 如果通过上面修改更正了您在Debug模式下的问题,那么您可以重新编译Release模式,非常有可能您可以解决先前存在的问题!. 错误的假定造成编译模式错误您是否经常性的假定您的变量或者对象被初试化成某个指定的值(可能0)?您是否假定你所有关联到的资源在应用程序中都存在?这些也是Debug和Release模式下不同问题产生的原因。
规则 4: 除非您在代码中对变量进行初始化,否则不能作出如上假定. 包括全局变量,自动变量,申请对象和new对象. 这种情况还常常发生在内存顺序的问题,记得原来使用结构体的时候为了使用方便,比较两个结构体对象使用memcmp,在Debug版本工作正常,而Release版本计算出错误的解,看来的确不能进行错误的假定!
规则 5: 确保删除资源的所有引用都被删除,例如resource.h中的定义. 软件开发中,不同编译版本对变量和内存的初始化是不同的. 如果您假定变量初始化为0,那么在Win9x系统的Release模式下,会出现异常现象。因此对所有变量,内存显式清0是较为安全的做法. 如果您引用了已经被删除的资源,您的Debug版本可以正常工作,但是Release版本可能会crash. 您是否相信编译器? 编译器警告级别和编译噪音有着相当大的关系. 通过提高编译器警告级别可增加程序隐藏问题暴露的机会.通常设置警告级别在"Level 3"或者 "Level 4".编译并解决所有警告,这是发布Release版本应用程序的一个很好的建议.这能暴露会使您的应用程序出现问题的很多初始化问题和其它潜在的错误.
规则 6: 开始项目之前先将编译警告级别设置在"Level 3" 或者 "Level 4" ,登记代码之前确保消灭所有警告!. 总结报告编译模式下的调试曾经不止一次的听到一些VC开发者说Release模式下面不能进行调试,幸运的是:通过相应设置,可以在Release模式进行调试,因此那只不过是一个以讹传讹的荒谬说法而已。
规则 7: 当前面所有的方法都无效的时候,在Release模式下面进行调试. Release模式可以进行调试,第一步是打开符号表: a.. Alt-F7打开项目设置,在C++/C 页面,选择"General"类,修改Debug Info setting 为 "Program Database". b.. 在"Link" 页面,选择"Generate Debug Info". c.. "Rebuild All" 这些设置将允许您在Release模式下保留符号表,您也可以同时考虑以下设置: a.. 调试Release版本应用程序,您可以关闭优化选项. b.. 如果在Release模式下面不能设置断点,添加指令"__asm {int 3}" 可以使您的应用程序在该行停止(确定在发布应用程序时候排除这些代码). 在Release模式进行调试的几个限制. a.. 最大的问题在于您不能跟踪到MFC函数内部,原因在于Release版本的MFC动态链接库不包含调试信息和符号表. b.. 同上,想要调试调用的dll,您必须给它们全部加上调试信息和符号表. 编译器生成了错误的代码? 或许有的时候您会发现VC++编译器生成了’问题代码’,然而坦率的讲,人们通常抱怨的太早.您可以在Release模式下面关闭优化选项来进行测试. 如果这个操作解决了您的问题,或许您的编码习惯存在问题. 信不信由你, 极其可能在您的编码中存在模棱两可的求解或者看起来似乎正确,某些条件下也是正确的情况. 举个例子,下面的代码在Debug模式似乎一切’正常’,而在Release模式下面却会出错! #include int* func1(){int retval = 5;return &retval;} int main(int argc, char* argv[]){printf("%d/n", *func1());return 0;}我相信大多数程序员尤其是初学者容易遇到此类情况的。
规则 8: 如果关闭Release模式的优化选项可以使您的应用程序运行正常,而打开优化选项则出现问题的化,原因多半在于您的不良编码习惯造成的. 这意味着必须仔细检查您的代码,清理出那些错误的假设,悬空指针等等. 等同的这告诉您,在Debug模式和关闭优化选项的Release模式下您的应用程序工作正常全是因为系统隐含的运气,您必须着手更正存在隐患的代码,否则在日后可能会造成巨大的损失。
规则 9: 如果您已经彻底检查了您的代码,并且没有发现问题,那么您最好逐个打开优化选项将产生错误的原因限制在某个范围之内. BTW- 以上问题代码由C++编译器自动检出. 如果您已经遵循 规则 6 您或许在前面环节中已经解决了这些问题. 凭我的开发经验,编译器极少会产生错误的代码(当然要注意接口程序边界对齐的问题).通常在使用模板类时候VC6编译器或许会产生断言ASSERT错误,这种情况您只需更新补丁即可解决。
最后的思考在日常编码中只需稍微增加一点严格的检测,便能有效的避免新的Debug -v- Release模式问题的产生,以下是我的一些经验。
1. 取出(check out)需要修改的代码。
2. 修改代码,排除所有警告,编译Debug和Release版本.
3. 详细测试新代码,即单步调试新代码段之后进入工作代码,确保代码无误.
4. 更正所有问题.
5. 确认无误之后将新代码登记入库(check in).
6. 对登记入库的代码进行全新的编译,确保新登记代码与其它代码融合.
7. 重新详细测试代码.
8. 更正新问题(或许可以发现登记入库代码存在的问题) 严格按照以上步骤,您在设计开发过程中即可解决大量问题,避免在最后发布应用程序时候产生新的难以定位的问题. 后记本文是在我的开发历程中遇到Release版本应用程序发布,产生错误的时候苦苦求索得到的一些经验,原文来自于codeproject,经过本人润色,改写成为适合国内开发者的文章,希望能对大家有用,谢谢! 转载: Debug版本包括调试信息,所以要比Release版本大很多(可能大数百K至数M)。至于是否需要DLL支持,主要看你采用的编译选项。如果是基于ATL的,则Debug和Release版本对DLL的要求差不多。如果采用的编译选项为使用MFC动态库,则需要MFC42D.DLL等库支持,而Release版本需要MFC42.DLL支持。Release Build不对源代码进行调试,不考虑MFC的诊断宏,使用的是MFC Release库,编译十对应用程序的速度进行优化,而Debug Build则正好相反,它允许对源代码进行调试,可以定义和使用MFC的诊断宏,采用MFC Debug库,对速度没有优化。
一、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 版中,这个变量很可能被优化调,此时数组越界会破坏栈中有用的数据。当然,实际的情况会比这复杂得多。与此有关的错误有: ● 非法访问,包括数组越界、指针错误等。例如 void fn(void) { int i; i = 1; int a[4]; { int j; j = 1; } a[-1] = 1;//当然错误不会这么明显,例如下标是变量 a[4] = 1; } j 虽然在数组越界时已出了作用域,但其空间并未收回,因而 i 和 j 就会掩盖越界。而 Release 版由于 i、j 并未其很大作用可能会被优化掉,从而使栈被破坏。 3. _DEBUG 与 NDEBUG :当定义了 _DEBUG 时,assert() 函数会被编译,而 NDEBUG 时不被编译。除此之外,VC++中还有一系列断言宏。这包括: ANSI C 断言 void assert(int expression ); C Runtime Lib 断言 _ASSERT( booleanExpression ); _ASSERTE( booleanExpression ); MFC 断言 ASSERT( booleanExpression ); VERIFY( booleanExpression ); ASSERT_VALID( pObject ); ASSERT_KINDOF( classname, pobject ); ATL 断言 ATLASSERT( booleanExpression ); 此外,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 等选项造成错误的情况比较少,而且他们的效果显而易见,比较容易发现。 在使用VC开发软件的过程中,正当要享受那种兴奋的时候突然发现: release与debug运行结果不一致,甚至出错,而release又不方便调试,真的是当头一棒啊,可是疼归疼,问题总要解决,下面将讲述一下我的两点经验,看看是不是其中之一: 1. 变量。大家都知道,debug跟release在初始化变量时所做的操作是不同的,debug是将每个字节位都赋成0xcc,而release的赋值近似于随机(我想是直接从内存中分配的,没有初始化过,但debug为什么不是0xff或0x00或其他什么的就不得而知了)。这样就明确了,如果你的程序中的某个变量没被初始化就被引用,就很有可能出现异常:用作控制变量将导致流程导向不一致;用作数组下标将会使程序崩溃;更加可能是造成其他变量的不准确而引起其他的错误。所以在声明变量后马上对其初始化一个默认的值是最简单有效的办法,否则项目大了你找都没地方找。代码存在错误在debug方式下可能会忽略而不被察觉到,如debug方式下数组越界也大多不会出错,在release中就暴露出来了,这个找起来就比较难了:( 还是自己多加注意吧 2. 自定义消息的消息参数。 MFC为我们提供了很好的消息机制,更增加了自定义消息,好处我就不用多说了。这也存在debug跟release的问题吗?答案是肯定的。在自定义消息的函数体声明时,时常会看到这样的写法:afx_msg LRESULT OnMessageOwn(); 当你在多线程或进程间使用了消息传递时就会导致无效句柄之类的错误。这个原因就是消息体的参数没有添加,即应该写成:afx_msg LRESULT OnMessageOwn(WPARAM wparam, LPARAM lparam); (具体原因我也不太清楚,是不是因为调用时按着默认的参数多分配了WPARAM+LPARAM的空间而破坏了应用程序的内存空间?还请高手多指点) 避免的方法: 1. 注意变量的初始化 2. 自定义消息的标准写法 3. 使用调试语句TRACE等时使用后最好注释掉 4. 尽量使用try - catch(...) VERIFY 和 ASSERT 的区别:一个在Release下面可以执行,一个不可以 DEBUG和RELEASE 版本差异及调试相关问题: 内存分配问题 1. 变量未初始化。下面的程序在debug中运行的很好。 thing * search(thing * something) BOOL found; for(int i = 0; i < whatever.GetSize(); i++) { if(whatever[i]->field == something->field) { /* found it */ found = TRUE; break; } /* found it */ } if(found) return whatever[i]; else return NULL; 而在release中却不行,因为debug中会自动给变量初始化found=FALSE,而在release版中则不会。所以尽可能的给变量、类或结构初始化。 2. 数据溢出的问题 如:char buffer[10]; int counter; lstrcpy(buffer, "abcdefghik"); 在debug版中buffer的NULL覆盖了counter的高位,但是除非counter>16M,什么问题也没有。但是在release版中,counter可能被放在寄存器中,这样NULL就覆盖了buffer下面的空间,可能就是函数的返回地址,这将导致ACCESS ERROR。 3. DEBUG版和RELEASE版的内存分配方式是不同的 。如果你在DEBUG版中申请 ele 为 6*sizeof(DWORD)=24bytes,实际上分配给你的是32bytes(debug版以32bytes为单位分配), 而在release版,分配给你的就是24bytes(release版以8bytes为单位),所以在debug版中如果你写ele[6],可能不会有什么问题,而在release版中,就有ACCESS VIOLATE。 II. ASSERT和VERIFY 1. ASSERT在Release版本中是不会被编译的。 ASSERT宏是这样定义的 #ifdef _DEBUG #define ASSERT(x) if( (x) == 0) report_assert_failure() #else #define ASSERT(x) #endif 实际上复杂一些,但无关紧要。假如你在这些语句中加了程序中必须要有的代码 比如 ASSERT(pNewObj = new CMyClass); pNewObj->MyFunction(); 这种时候Release版本中的pNewObj不会分配到空间 所以执行到下一个语句的时候程序会报该程序执行了非法操作的错误。这时可以用VERIFY : #ifdef _DEBUG #define VERIFY(x) if( (x) == 0) report_assert_failure() #else #define VERIFY(x) (x) #endif 这样的话,代码在release版中就可以执行了。 III. 参数问题: 自定义消息的处理函数,必须定义如下: afx_msg LRESULT OnMyMessage(WPARAM, LPARAM); 返回值必须是HRESULT型,否则Debug会过,而Release出错 IV. 内存分配 保证数据创建和清除的统一性:如果一个DLL提供一个能够创建数据的函数,那么这个DLL同时应该提供一个函数销毁这些数据。数据的创建和清除应该在同一个层次上。 V. DLL的灾难 人们将不同版本DLL混合造成的不一致性形象的称为 “动态连接库的地狱“(DLL Hell) ,甚至微软自己也这么说(http://msdn.microsoft.com/library/techart/dlldanger1.htm)。 如果你的程序使用你自己的DLL时请注意: 1.不能将debug和release版的DLL混合在一起使用。debug都是debug版,release版都是release版。 解决办法是将debug和release的程序分别放在主程序的debug和release目录下 2.千万不要以为静态连接库会解决问题,那只会使情况更糟糕。 VI. RELEASE板中的调试 : 1.将ASSERT() 改为 VERIFY() 。找出定义在"#ifdef _DEBUG"中的代码,如果在RELEASE版本中需要这些代码请将他们移到定义外。查找TRACE(...)中代码,因为这些代码在RELEASE中也不被编译。 请认真检查那些在RELEASE中需要的代码是否并没有被便宜。 2.变量的初始化所带来的不同,在不同的系统,或是在DEBUG/RELEASE版本间都存在这样的差异,所以请对变量进行初始化。 3.是否在编译时已经有了警告?请将警告级别设置为3或4,然后保证在编译时没有警告出现. VII. 将Project Settings" 中 "C++/C " 项目下优化选项改为Disbale(Debug)。编译器的优化可能导致许多意想不到的错误,请参考http://www.pgh.net/~newcomer/debug_release.htm 1.此外对RELEASE版本的软件也可以进行调试,请做如下改动: 在"Project Settings" 中 "C++/C " 项目下设置 "category" 为 "General" 并且将"Debug Info"设置为 "Program Database"。 在"Link"项目下选中"Generate Debug Info"检查框。 "Rebuild All" 如此做法会产生的一些限制: 无法获得在MFC DLL中的变量的值。 必须对该软件所使用的所有DLL工程都进行改动。 另: MS BUG:MS的一份技术文档中表明,在VC5中对于DLL的"Maximize Speed"优化选项并未被完全支持,因此这将会引起内存错误并导致程序崩溃。