release与debug版编译选项组合差异及不一致的情形

我们知道,编译时可以有不同的编译选项及组合。在编译器中,有两种编译选项组合,分别是release与debug,编译时,选择release或者debug,编译出来的程序分别称为release版或者debug版,前者优化较多,文件较小,后者因为调试的需要,文件较大。当然,不管是releas选项,还是debug选项,其中的一些编译选项可以在工程设置中做修改,从而得到优化过的调试版本或是带跟踪语句的发布版本。

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版本大很多(可能大数百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版的的编译选项组合及一些编译选项的具体差异和细节:

1 Runtime Library

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

2 优化

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

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

char* pjPath() { 	char * path = new char[66]; 	::GetCurrentDirectory(66,path); 	CString cpath = path;     cpath += "test.txt"; 	return cpath.GetBuffer(0);// release版OK,debug版是随机值 }

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

2.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 并未使用可能会被优化掉,从而使栈被破坏。

如:

char buffer[10];  int counter;  lstrcpy(buffer, "abcdefghik"); 

在debug版中buffer的NULL覆盖了counter的高位,但是除非counter>16M,什么问题也没有。但是在release版中,counter可能被放在寄存器中,这样NULL就覆盖了buffer下面的空间,可能就是函数的返回地址,这将导致ACCESS ERROR。

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 之类的条件编译,也要注意一下。

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版中就可以执行了。

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

4 /GZ 选项

GZ 选项主要包括:

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

下面的一段代码在debug中运行的很好,而在release中却不行,因为debug中会自动给变量初始化found=FALSE,而在release版中则不会:

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;  

所以尽可能的给变量、类或结构初始化。

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

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

通常 /GZ 选项会造成 Debug 版出错而 Release 版正常的现象,因为 Release 版中未初始化的变量是随机的,这有可能使指针指向一个有效地址而掩盖了非法访问。 除此之外,/Gm /GF 等选项造成错误的情况比较少,而且他们的效果显而易见,比较容易发现。


5 Debug版OK,Release版异常的情形

5.1 数组越界

    CTime m_day = CTime::GetCurrentTime();     CString week = m_day.Format("%w");        int i = atoi(week.GetBuffer(0)); // 当星期天时i=0     //CString weeks = _T("一二三四五六七");     //weeks = weeks.Mid(i*2-2,2);     CString weeks[] = {"Sun.","Mon.", "Tue.", "Wed.", "Thu.", "Fri.", "Sat."};     CString str = "hi,\r";     str += "" + m_day.Format("%Y-%m-%d " + weeks[i-1]);//当星期天时i=0,改为weeks[i-1]

另外需要注意的是,规模大一点的程序与玩具程序的错误是有所区别的,原因在于栈上数据的覆盖,及内存地址篡改的概率不一致。

5.2 自定义消息的参数

MFC为我们提供了很好的消息机制,更增加了自定义消息,好处我就不用多说了。这也存在debug跟release的问题吗?答案是肯定的。在自定义消息的函数体声明时,时常会看到这样的写法:afx_msg LRESULT OnMessageOwn(); 当你在多线程或进程间使用了消息传递时就会导致无效句柄之类的错误。这个原因就是消息体的参数没有添加,即应该写成:afx_msg LRESULT OnMessageOwn(WPARAM wparam, LPARAM lparam); 否则Debug会过,而Release出错。

6 Debug版异常,Release版OK的情形

6.1 内存泄漏

pDC=GetDC(); ....... ReleaseDC(pDC); //正确!不能忘记,不能是pDC->DeleateDC(); 

要养成结对编程的习惯。

6.2 指针的初始化

    //CTime m_tQueryTime;     SYSTEMTIME* m_tQueryTime; // 未初始化     m_dayCtrl.GetCurSel(m_tQueryTime); // m_dayCtrl是日历控件的control变量    // 更改为以下即可:     SYSTEMTIME m_tQueryTime;     m_dayCtrl.GetCurSel(&m_tQueryTime);

6.3 申请动态内存与实际需要内存的不一致

         // 写入UTF-8的BOM文件头         char header[3] = {(char)0xEF, (char)0xBB, (char)0xBF};         fwrite(header, sizeof(char), 3, fp);         char *chs = (char *)m_textblock.GetBuffer(0);         int k = m_textblock.GetLength();         wchar_t *wc = (wchar_t *)malloc(sizeof(wchar_t)*k+2); // 此处要+2          // 将ANSI编码的多字节字符串转换成宽字符字符串         int n = MultiByteToWideChar(CP_ACP, 0, chs, strlen(chs), wc, k);         if ( n > 0 )         {             wc[n] = 0;             char *mb = (char *)malloc(sizeof(char)*k*4);             // 将宽字符字符串转换成UTF-8编码的多字节字符串             n = WideCharToMultiByte(CP_UTF8, 0, wc, wcslen(wc), mb, k*4, NULL, NULL);             if ( n > 0 )             {                 mb[n] = 0;                 fwrite(mb, sizeof(char), strlen(mb), fp);             }             free(mb);         }                  free(wc); // 如果上面动态内存字节数未+2,         //会出现DAMAGE: after Normal block(#4213) at 0x的错误

综上,release版和debug版的本质差异在于编译选项的组合不一致,需要在release版和debug版之间交替编译,以及早发现潜在的错误。

-End-

版权声明:

作者: freeclashnode

链接: https://www.freeclashnode.com/news/article-557.htm

来源: FreeClashNode

文章版权归作者所有,未经允许请勿转载。

免费节点实时更新

热门文章

最新文章

归档