百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

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

cac55 2024-09-20 13:02 29 浏览 0 评论

我们知道,编译时可以有不同的编译选项及组合。在编译器中,有两种编译选项组合,分别是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-

相关推荐

小车五位自动循环往返控制_小车自动往返控制系统

需求描述:用三相异步电动机拖动一辆小车在A、B、C、D、E五点之间自动循环往返运行,小车初始在A点,按下启动按钮,小车依次前进到B、C、D、E点,并分别停止2s返回到A点停止。按下停止...

自动灌溉系统_自动灌溉系统by

需求描述:PLC时钟设定每日6:00、18:00自动启动灌溉系统,每次运行15分钟后停止;非定时时段按下手动灌溉按钮,立即启动并运行15分钟;土壤湿度传感器检测到湿润时,跳过本次定时灌溉...

主板ERP开启还是关闭好_主板设置erp是什么

主板功能的开启与关闭,本质是在“节能环保”和“使用便利”之间做选择。为帮你快速决策,先给出直接结论,再深入解析原理、影响及操作步骤,让你根据自身需求精准设置。一、直接结论:ERP功能如何选?...

新电脑必做5项设置!做完再玩,流畅安全多用三年

刚拿到新电脑,兴奋之余先别急着开机畅玩!做好以下这5大设置,能让你的爱机长期保持流畅如新,安全又省心。尤其是最后一招,很多老用户都不知道!1关闭隐私常规,杜绝数据偷跑新电脑首次开机进行系统初始化时,...

属于 PHP 开发者的 Supervisor 实用指南

属于PHP开发者的Supervisor实用指南在PHP开发中,我们经常需要运行一些后台进程:队列处理、长时间运行的脚本、WebSocket服务器等。这些进程可能会因为各种原因意外退出,手...

领导半夜12点微信派活?三句高情商回复,反手拿捏让他不敢再烦

友友们大家来啦!今天来和大家一起分享精彩话题老规矩先点赞再看文!0102别在这里害人了,现在能保住工作就烧高香了,再得瑟,明天早上去办离职0304很简单,把他一起拉上,每半小时打电话或语音汇报,一两次...

&quot;零点黑科技!硬盘自动备份+离线神操作,服务器数据安全躺赢&quot;

公司有一台服务器,数据库需要每天零点进行数据库备份,要求在本机备份一次,再在移动硬盘上异地备份一次。备份完成后硬盘自动离线。具体思路如下:数据库自动备份时间为每天0点,备份过程约需1分钟。0点时开启硬...

峰谷电:白天贵、晚上便宜,你家真的适合开通吗?

电费单又超预算了?别急着关掉空调,其实你可能错过了一个"电费打折"的机会——峰谷电。它就像电影院的日场和夜场票,白天贵、晚上便宜,聪明利用,电费真的能省下来。一、峰谷电是什么?峰谷电把...

电脑开机密码设置全指南:从基础到进阶的安全防护

在数字化时代,电脑存储着大量个人隐私和重要数据,设置开机密码是保护信息安全的第一道防线。本文将系统介绍Windows、macOS、Linux三大主流操作系统及BIOS层面的密码设置方法,同时涵盖密码管...

自动喷香机_香薰机自动喷香机

需求描述:PLC时钟设定每日9:00、14:00、18:00自动启动喷雾,每次喷雾3秒后停止;非定时时段按下手动喷雾按钮,立即喷雾3秒;香薰液缺液传感器检测到液位过低时,停止喷雾并亮报警...

macbook系统自动启动项在哪里查看

了解和管理MacBook的开机自动启动项,是优化系统启动速度和运行效率的好方法。下面我来为你介绍几种查看和管理这些启动项的方法。查看和管理MacBook启动项1.通过系统设置(最简单直接的方法)...

想让电脑自己到点开机和关机?这4个超实用的设置方法快收好!

嘿,你是不是也经常忙到忘记关电脑?或者早上想用电脑时发现还没开机?别慌,今天我就跟你分享几个超实用的方法,帮你轻松搞定电脑的定时开关机设置。不管你是电脑小白还是有点基础,这篇教程都能让你秒懂操作,省时...

定时关机这样操作小白也会 一招设定工作日关机 指定时间关机

在日常使用电脑的过程中,我们常常会遇到这样的情况:晚上睡觉前忘记手动关机,导致电脑整夜运行,既浪费电又缩短硬件寿命;或者在下载大文件时,需要等待很长时间才能完成,却不能一直守在电脑前,下载完成后也无法...

日本无线电操作证试题,这些问题你能答的上来吗?

一直以来,我们对于日本的业余无线电的印象都停留在“操作能力强,爱好者数目众多”上,然而我们对于他们的业余无线电体系所知甚少。日本业余无线电操作证的等级分作四级,最基本的四级操作证书具有8MHz以下、2...

你知道吗?单边带信号就像DNA分子一样!

我们在准备B级操作证书的过程中,避免不了的要接触到一个新的名词——SSB。单边带是传统AM模式的一种特殊的形式,在传送相同的信息的过程中,其占用的带宽仅为AM模式的一半,那么SSB模式到底是怎样的一种...

取消回复欢迎 发表评论: