发布版本和调试版本运行不同的原因有哪些?

60

我有一个使用 Visual Studio 2005 C++ 编写的程序,在发布模式下与调试模式下运行不同。在发布模式下,出现了(表面上)间歇性崩溃的情况。在调试模式下,程序不会崩溃。有哪些原因会导致 Release 构建与 Debug 构建不同?

值得一提的是,我的程序相当复杂,使用了多个第三方库进行 XML 处理、消息传递等操作...

谢谢!


2
虽然我试图回答你的问题,但也许这并不能很好地解决问题。首先,获取一个好的复现。其次,在发布版本中启用调试信息。崩溃还在吗?不是 --> 可能是时序或初始化问题。是 --> 使用(远程)调试器。 - peterchen
11个回答

135
Surviving the Release Version 给出了一个很好的概述。
我遇到的问题 - 大多数已经被提到了 变量初始化 是迄今为止最常见的。在Visual Studio中,调试构建会显式地将分配的内存初始化为给定值,例如在此处查看内存值。这些值通常很容易发现,当作为索引使用或作为指针使用时会导致越界错误或访问冲突。未初始化的布尔值为true,但可能会导致未检测到多年的未初始化内存错误。
在内存没有被明确初始化的发布版本中,它只保留之前的内容。这会导致“有趣的值”和“随机”崩溃,但也往往导致确定性的崩溃,需要先执行一个表面上无关的命令才能实际崩溃的命令。这是由于第一个命令“设置”具有特定值的内存位置,而当内存位置被回收时,第二个命令将其视为初始化。这比未初始化的堆栈变量更常见,但后者也曾经发生过。
在从Visual Studio(带有调试器附加)开始和从资源管理器开始时,原始内存初始化也可能不同。这使得“最好的”发布版本错误很少出现在调试器下。 有效的优化 在我的经验中排名第二。C++标准允许进行许多意想不到的优化,例如当两个指针别名相同的内存位置时,不考虑初始化顺序,或多个线程修改相同的内存位置,并且您期望线程B看到线程A所做更改的某种顺序。通常,编译器会因此被责备。然而不要太快下结论,年轻的绝地武士!-请参阅以下内容 时间 发布版本不仅仅是“运行更快”,由于各种原因(优化、提供线程同步点的日志记录函数、未执行调试代码等),操作之间的相对时间也发生了巨大变化。由此发现的最常见问题是竞态条件,但也包括死锁和基于消息/定时器/事件的代码的简单“不同顺序”执行。尽管它们是时间问题,但它们可以在构建和平台之间稳定地显示,并且可以有“总是生效,除了PC 23”的重现。 守卫字节。调试构建通常在选定的实例和分配周围放置(更多)守卫字节,以防止索引溢出并有时发生下溢。在代码依赖于偏移量或大小(例如序列化原始结构)的罕见情况下,它们是不同的。 其他代码差异 某些指令-例如asserts-在发布版本中不执行任何操作。有时它们具有不同的副作用。这在宏诡计中很普遍,如经典的(警告:多个错误)
#ifdef DEBUG
#define Log(x) cout << #x << x << "\n";
#else 
#define Log(x)
#endif

if (foo)
  Log(x)
if (bar)
  Run();

在发布版本中,if (foo && bar) 将被求值。

编译器错误 这种情况真的几乎不会发生。嗯——其实也有可能会发生,但在大部分职业生涯中,您最好假设它不会发生。在与VC6合作的十年中,我只发现了一个问题,我仍然坚信这是未修复的编译器错误,相比于使用标准不足的模式(即所谓的“经文”)的数十个模式(甚至数百个实例)。


2
好的答案 +1!建议采纳。 - John Sibly
2
很棒,但请添加一些关于断言副作用的内容。在assert中的表达式可能会产生副作用,并且不会在发布版本中运行。例如:病态案例:assert(n++ == 2);有时候有一个替代宏,总是评估表达式,例如VC++中的VERIFY。 - Daniel Earwicker
1
哦,我看到链接的文章提到了... - Daniel Earwicker
“工作始终如一,除了PC 23以外”是什么意思?这是指某个参考吗? - KeyC0de
2
@KeyC0de:并非故意而为之。它的意思是:可能有数十台机器运行正常,但由于略有不同的配置,一台机器会经常遇到问题。也就是说,这是一个难以重现和分析的错误,但足够“丑陋”,让你感到困扰。 - peterchen

6

在调试版本中常常启用断言和/或调试符号。这可能会导致内存布局不同。如果出现错误指针、数组溢出或类似的内存访问问题,你可能会在一种情况下访问关键的坏内存(例如函数指针),而在另一种情况下只是访问一些非关键的内存(例如仅文档字符串被破坏)。


5

没有明确初始化的变量在发布版本中可能会被清零。


2

正式版构建(希望如此)比调试版构建运行更快。如果您使用多个线程,则可能会看到更多交错,或者仅有一个线程比其他线程运行得更快,这在调试构建中可能没有注意到。


2

发布版本通常在编译器中启用了优化,而调试版本通常没有。

在某些语言或使用许多不同库时,这可能会导致间歇性崩溃 - 尤其是当所选的优化级别非常高时。

我知道这适用于gcc C ++编译器,但我不确定微软的编译器是否也是如此。


2

我不久前也遇到过类似的问题,最终发现是由于在发布版本中堆栈的处理方式不同而引起的。其他可能会有所不同的因素包括:

  • 在VS编译器中,调试构建中的内存分配处理方式不同(例如,在清除内存时写入0xcc等)
  • 循环展开和其他编译器优化
  • 指针对齐方式

2
这与编译器供应商和您使用DEBUG标志进行编译的库有关。虽然DEBUG代码不应影响运行代码(不应该产生副作用),但有时确实会出现影响。
特别地,在DEBUG模式下,变量可能仅初始化,而在RELEASE模式下,则可能未初始化。Visual Studio编译器中的STL在DEBUG和RELEASE模式下也不同。其思想是,在DEBUG中完全检查迭代器以检测可能的错误(例如,如果插入操作发生在检索迭代器之后,则指向vector的迭代器将失效)。
第三方库也是如此,我能想到的第一个就是QT4。如果与创建图形对象的线程不同的线程执行绘图操作,则它将使用assert终止程序。
随着所有更改,您的代码和内存占用在两种模式下都将不同。如果可以读取该位置,则指针(读取数组末尾一行)问题可能未被检测到。
断言旨在在DEBUG期间杀死应用程序,并从RELEASE构建中消失,因此我不认为断言是您的问题。我的首要嫌疑是恶意指针或访问超出范围的指针。以前曾经存在编译器优化破坏代码的问题,但最近没有听到过投诉。可能存在优化问题,但这不是我的首要嫌疑。

1

https://web.archive.org/web/20090503153840/https://www.debuginfo.com/tips/userbpntdll.html

由于调试版本会添加保护字节,因此您可能能够“安全地”访问数组(特别是动态数组)之外越界的内存,但这将在发布版本中导致访问冲突。这种错误可能会被忽略,导致堆栈损坏,并可能在与原始错误无关的地方发生访问冲突。
使用PageHeap(或如果您安装了调试工具,则可以使用gflags)来发现与堆栈损坏相关的错误。

https://web.archive.org/web/20090210190738/https://support.microsoft.com/kb/286470


0
在 Release 模式下可以执行的优化中(而不是 Debug),复制省略可能会产生不同的结果。更具体地说,RVO(返回值优化)取决于您的构造函数的设计方式。 什么是复制省略和返回值优化?

0
根据我的经验,最常见的原因似乎是配置在发布/构建设置方面存在更多差异。例如,不同的库被包含,或者调试构建与发布构建具有不同的堆栈大小。
为了避免这种情况发生在我们的Visual Studio 2005项目中,我们广泛使用属性表。这样,发布和调试配置可以共享公共设置。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接