发布版中出现的错误但在调试模式下不存在的常见原因

74

在发布编译模式下出现的程序错误和异常行为,而在调试模式下却没有出现,这通常是由于哪些原因引起的?


7
你们这些人怎么了?这真是个很棒的问题! - user151323
同意,这是一个好问题。涉及到很多微妙的细节,而且它们会在最糟糕的时刻咬你一口(即当您不再拥有完整的调试支持来解决问题时,根据定义)。 - stusmith
18个回答

36

在C++中的调试模式下,很多时候所有变量都会被初始化为null值,而在发布模式下则不会,除非明确声明。

检查是否有调试宏和未初始化的变量。

如果程序使用了线程,则优化也可能导致发布模式下出现一些问题。

还要检查所有异常,例如与发布模式没有直接关系,但有时我们会忽略一些关键的异常,比如在VC++中的内存访问冲突,但同样的问题在其他操作系统(如Linux、Solaris)中可能是个问题。理想情况下,程序不应该捕获此类关键异常,例如访问空指针。


9
我总觉得这种行为完全是相反的。调试模式的工作肯定是要“暴露”问题,而不是隐藏它们吧? - walkytalky
在C++中这很奇怪,但在C#中幸运的是一切都默认为NULL初始化。 - Priyank Bolia
5
一个小细节:一般来说,在调试模式下,变量不会被填充为 null,而是填充为某些在自然界中很少出现的特定值(例如 MSVC 中的 0xCCCCCCCC)。 - atzz
2
是的,你说得对,为了扩展你的答案:http://priyank.co.in/uninitialized-memory-values-in-debug-mode-vc-denotes - Priyank Bolia
为了扩展atzz的答案,MSVC使用0xCC填充未初始化的堆栈数据,使用0xCD填充未初始化的堆数据,并使用0xDD删除对象。更多神奇值 - rustyx

22

一个常见的陷阱是在ASSERT内部使用具有副作用的表达式。


这在gcc中会产生一个警告,但Visual Studio不会对此发出警告。一个例子是:assert(MyObj->LoadFromFile(File));。在发布版本中,LoadFromFile根本不会被调用,而且你在编译时也不会得到任何通知。 - Benlitz
你救了我的一天,谢谢:在“assert”内部进行这样的调用除非出于某些调试原因,否则真的很愚蠢。 - Adel Ben Hamadi

12

我过去曾遇到多个被调试版通过但发布版中崩溃的问题。 这其中有很多潜在原因(包括当然已在此主题中总结的原因),我曾遇到以下所有情况:

  • 成员变量或成员函数在#ifdef _DEBUG中,因此在调试版本中类的大小不同。 有时在发布版本中使用#ifndef NDEBUG
  • 同样,在两个版本中仅存在于一个版本中的不同#ifdef
  • 调试版本使用系统库的调试版本,特别是堆和内存分配函数
  • 在发布版本中的内联函数
  • 头文件包含的顺序。 这不应该引起问题,但如果您有像#pragma pack这样未被重置的东西,那么可能会导致严重问题。 使用预编译头文件和强制包含也可能出现类似问题
  • 缓存:您可能有代码(例如缓存)仅在发布版本中使用,或者缓存大小限制不同
  • 项目配置:调试和发布配置可能具有不同的构建设置(在使用IDE时可能会发生这种情况)
  • 竞态条件、定时问题和作为调试专用代码结果产生的其他副作用

这些年我积累了一些有助于解决调试/发布错误的技巧:

  • 如果可以,请尝试在调试版本中复制异常行为,并更好地编写单元测试以捕获它
  • 考虑两者之间的区别:编译器设置、缓存、仅限调试的代码。 尝试暂时最小化这些差异
  • 创建一个已关闭优化的发布版本(因此您更有可能在调试器中获得有用的数据),或者是已优化的调试版本。 通过最小化调试和发布之间的更改,您更有可能隔离导致错误的差异。

9

其他可能存在的区别包括:

  • 在垃圾回收语言中,收集器在发布模式下通常更加积极;
  • 内存布局通常不同;
  • 内存初始化可能不同(例如,在调试模式下可能被清零,或在发布模式下可以更积极地重新使用);
  • 本地变量可能会在发布中晋升为寄存器值,这可能会导致浮点值的问题。

2
在垃圾回收语言中,垃圾回收器通常在释放模式下更加积极。这听起来相当荒谬。一个对象要么是可达的,要么不是。如果垃圾回收器删除了一个可达的对象,那就是错误的;如果它没有删除一个不可达的对象,那也不会导致错误——因为该对象无论如何都不可达。 - idmean
看起来很荒谬,但事实却是如此。很久以前,在.NET 2.0时代,我们有一些托管的C++代码。我们发现在调试模式下,“this”似乎被认为是GC根,但在发布模式下,一个对象甚至可以在运行自己的实例方法时被收集,只要该方法代码从那时起不再引用自己的成员。在这种情况下,一点GC::KeepAlive的帮助就很有用了:https://msdn.microsoft.com/zh-cn/library/system.gc.keepalive(v=vs.110).aspx - stusmith
2
@idmean,这一点完全不荒唐。调试二进制文件的创建目的是为了中断执行、查看所有范围内的变量和维护代码到二进制的对称性。发布版本的创建是为了速度和/或最小尺寸。如果它知道不需要它们,则可以省略整个函数调用或变量定义。这会创建一个非常不同的内存景观。 - diox8tony

3

是的!如果您有条件编译,则可能存在时间错误(优化后的发布代码与非优化的调试代码之间),内存重用与调试堆。


3

如果你在C领域中,它可能会有影响。

一种可能的原因是DEBUG版本可能添加了代码来检查杂散指针并保护您的代码免于崩溃(或行为不正确)。如果是这种情况,您应该仔细检查编译器给出的警告和其他消息。

另一个可能的原因是优化(通常在发布版本上开启,在调试模式下关闭)。代码和数据布局可能已经被优化,而你的调试程序只是访问未使用的内存,发布版本现在正在尝试访问保留的内存甚至指向代码!

编辑:我看到其他人提到了:当没有在DEBUG模式下编译时,你可能会完全排除某些代码部分。如果是这种情况,我希望那确实是调试代码,而不是程序本身正确性所必需的重要内容!


3

CRT库函数在debug和release模式下的行为是不同的(/MD和/MDd)。

例如,调试版本通常会预先填充您传递给指定长度的缓冲区,以验证您的声明。例如strcpy_sStringCchCopy等。即使字符串提前终止,您的szDest也必须是n个字节长!


2

Sure, for example, if you use constructions like

#if DEBUG

//some code

#endif

1

你需要提供更多的信息,但是是可以做到的。这取决于你的调试版本所做的事情。你可能会在其中记录或添加额外的检查,这些内容不会编译到发布版本中。这些仅用于调试的代码路径可能会产生意想不到的副作用,从而以奇怪的方式改变状态或影响变量。调试构建通常运行较慢,因此这可能会影响线程并隐藏竞争条件。同样适用于来自发布编译的简单优化,这是可能的(尽管现在不太可能),发布编译可能会将某些东西作为优化而短路。


1

如果没有更多细节,我会假设“不好”意味着它不能编译或在运行时引发某些错误。检查一下你的代码是否依赖于编译版本,可以通过#if DEBUG语句或标记有Conditional属性的方法来实现。


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