只有在发布版本中程序崩溃,如何调试?

110
我这里遇到了类似于“薛定谔的猫”问题 -- 我的程序(实际上是测试套件,但仍然是一个程序)在发布模式下启动命令行时崩溃。通过类似于穴居人式的调试(即,在各个地方添加一些nasty printf()消息),我已经确定了导致代码崩溃的测试方法,但不幸的是,实际的崩溃似乎发生在某个析构函数中,因为我看到的最后一条跟踪消息是在其他执行良好的析构函数中。
当我尝试在Visual Studio内运行此程序时,它并没有崩溃。从WinDbg.exe启动也是一样的情况。只有从命令行启动时才会出现崩溃。顺便说一句,这在Windows Vista下发生,不幸的是我现在没有XP机器可以测试。
如果能让Windows打印出栈跟踪或者其他有意义的信息,而不是像正常退出程序一样终止程序,那就太好了。是否有人能给出任何建议,以便我能够在这里获取更多有意义的信息并修复此错误?
编辑:问题确实是由一个越界数组引起的,我在这篇文章中更详细地描述了它。感谢大家帮助找到这个问题!

你能提供那个测试方法的样例吗? - akalenuk
不好意思,这段代码太复杂了,很难在这里轻松粘贴。正如我之前提到的,问题并不是发生在测试方法本身,而是在析构函数之后。虽然在这个方法中没有未初始化的指针或其他类似的问题。 - Nik Reiman
3
大多数答案只是猜测。有一些常见的技巧可以在不连接调试器的情况下分析崩溃版本的构建:https://dev59.com/snVC5IYBdhLWcg3wykQt#18513077?stw=2 - Sebastian
也许这不是你的错:在g++中,优化级别-O3是否危险? - Brent Bradburn
29个回答

156

在我所见或听说的所有情况中,当一个C或C++程序在调试器中运行正常但在外部运行失败时,原因通常是写入了函数局部数组末尾之后的内存空间。(调试器在堆栈上分配更多内存,因此你不太可能覆盖到重要的数据。)


36
有人给这位先生一枝雪茄!在我的情况下,我传递了一个StringBuilder到一个P/Invoke函数中,但它的容量不够大。我想这就像某人在你睡觉时用魔术笔在你脸上涂写:在调试器下,他们会在你的额头上乱涂乱画,你没有察觉到;但是如果没有调试器,他们最终会戳你的眼睛...差不多就是这样。感谢这个提示! - Nicholas Piasecki
1
在我的情况下,这是一个使用Obj-C的ARM处理器上的对齐问题。 - Almo
1
11年过去了,这仍然是真实的...不要忘记保留你的向量。 - dav
1
好的,那么如何更改调试模式的行为以便进行实际调试呢? - Paul Childs
1
现在知道在哪里出了问题,但是调试时所有的工作都是如何告诉你问题出在哪里的呢?虽然我认为你的答案在大多数情况下是正确的,知道要寻找什么是一个好的开始,但是在大型代码库中查找问题确切的位置可能会非常昂贵。 - Paul Childs
显示剩余7条评论

61

当我以前遇到类似的问题时,通常是因为变量初始化不足。在调试模式下,变量和指针会自动初始化为零,但在发布模式下则不会。因此,如果你有这样的代码

int* p;
....
if (p == 0) { // do stuff }

在调试模式下,if语句中的代码不会被执行,但在发布模式下,变量p包含一个未定义的值,很可能不是0,因此该代码经常被执行,导致崩溃。

建议检查您的代码是否存在未初始化的变量,这也适用于数组的内容。


典型情况是在构造函数的成员初始化列表中忘记放置一个成员变量。虽然效果相同,但如果你不知道应该寻找适当的成员初始化,那么很难发现这个问题。 - steffenj
1
在调试模式下,变量通常被初始化为一些“编译器定义的常量”,可以在调试过程中用于指示变量所处的状态。例如:指针NULL或0xDeadBeef很受欢迎。 - Martin York
调试运行时通常会将内存初始化为某些非零值,特别是为了使空指针测试导致代码表现得好像指针不是NULL。否则,你的代码在调试模式下可以正常运行,但在发布模式下会崩溃。 - Michael Burr
2
不,变量根本没有被初始化,直到它们被赋值之前,“使用”它们仍然是未定义的行为。但是,底层内存内容通常预先填充了0x0000000或0xDEADBEEF或其他可识别的模式。 - Lightness Races in Orbit

33

到目前为止,还没有任何答案试图对可用的调试发布应用程序的技术进行严肃的概述:

  1. 发行版和调试版因许多原因而行为不同。 这里有一个很好的概述。 这些差异中的每一个都可能在发布版本中引起不存在于调试版本中的错误。

  2. 调试器的存在也可能改变程序的行为,对于发布版和调试版都是如此。 请参见此答案。 简而言之,至少在附加到程序时,Visual Studio调试器会自动使用调试堆。您可以通过使用环境变量_NO_DEBUG_HEAP关闭调试堆。您可以将其指定在计算机属性或Visual Studio项目设置中。这样可能使崩溃与已连接的调试器一起重现。

    更多关于调试堆破坏的内容在此处。

  3. 如果以前的解决方案无效,则您需要捕获未处理的异常并在崩溃发生时附加后期调试器。您可以使用例如WinDbg对此进行处理,有关可用的后期调试器及其在MSDN上的安装的详细信息

  4. 您可以改进异常处理代码,如果这是一个生产应用程序,则应该:

    a. 使用std::set_terminate安装自定义终止处理程序

    如果你想在本地调试这个问题,可以在终止处理程序中运行一个无限循环,并输出一些文本到控制台以通知你已经调用了std::terminate。然后附加调试器并检查调用堆栈。或者按照这个答案的描述打印堆栈跟踪。 在生产应用程序中,您可能希望发送一个错误报告回家,最好还要搭配一个小型内存转储,如此处所述,以便分析问题。 使用Microsoft的结构化异常处理机制,它允许您捕获硬件和软件异常。请参阅MSDN。您可以使用SEH保护代码的部分,并像a)中使用相同的方法来调试问题。 SEH提供有关发生的异常的更多信息,您可以在生产应用程序中发送错误报告时使用该信息。

16

需要注意的事项:

数组越界 - Visual Studio调试器会插入填充物,这可能会阻止崩溃。

竞态条件 - 如果涉及到多个线程,那么只有在直接执行应用程序时才会出现竞态条件。

链接 - 您的发布版本是否正确地引用了库。

尝试以下操作:

Minidump - 非常容易使用(只需在MSDN中查找),将为每个线程提供完整的崩溃转储。您只需将输出加载到Visual Studio中,就好像在崩溃时进行调试一样。


1
嗨 - 我的回答遭到了匿名的负评。我想知道为什么? - morechilli

13

您可以将WinDbg设置为您的死机后调试器。这将在崩溃发生时启动调试器并将其附加到进程上。要安装用于死机后调试的WinDbg,请使用/I选项(请注意它是大写):

windbg /I

更多细节在这里

至于原因,很可能是未初始化的变量,正如其他答案所建议的那样。


2
不要忘记,即使是发布版本的构建,编译器也可以生成PDB文件,虽然这不是默认设置。 - Michael Burr
这个问题的唯一真正答案。 - Sebastian

11

经过多小时的调试,我终于找到了问题的原因,它确实是由缓冲区溢出所引起的,导致了一个字节的差异:

char *end = static_cast<char*>(attr->data) + attr->dataSize;

这是一个栅栏柱错误(即偏移一位错误),已被修复:

char *end = static_cast<char*>(attr->data) + attr->dataSize - 1;

奇怪的是,我在代码的各个部分放置了多次_CrtCheckMemory()的调用,但它们总是返回1。通过在测试用例中放置“return false;”调用,最终通过试错法确定了故障源。

感谢大家的评论——今天我学到了很多关于windbg.exe的知识!:)


10
今天我在调试一个类似的问题,_CrtCheckMemory() 一直返回1。但后来我发现原因了:在 Release 模式下,_CrtCheckMemory 被 #define 定义为 ((int)1)。 - Brian Morearty

7
即使您将exe构建为发布版本,仍然可以生成PDB(程序数据库)文件,以便您进行堆栈跟踪,并进行有限的变量检查。 在构建设置中,有一个选项可以创建PDB文件。打开此选项并重新链接。然后首先尝试从IDE运行以查看是否崩溃。如果是,则很好-您已准备好查看事物。如果没有,则在从命令行运行时,可以执行以下两个操作之一:
1. 运行EXE,在崩溃之前执行“附加到进程”(Visual Studio上的“工具”菜单)。 2. 崩溃后,选择启动调试器选项。
当要求指向PDB文件时,请浏览以找到它们。如果PDB与您的EXE或DLL一起放置在同一输出文件夹中,则可能会自动选择它们。
PDB提供了与源代码的链接,具有足够的符号信息,使得可以查看堆栈跟踪,变量等。您可以像平常一样检查值,但请注意,由于优化过程可能意味着某些东西只出现在寄存器中,或者发生的顺序与您预期的不同,因此您可能会得到错误的读数。
注:我在这里假设使用Windows / Visual Studio环境。

4

这种崩溃几乎总是由于IDE通常会将未初始化变量的内容设置为零、null或其他类似的“合理”值,而当本地运行时,您将得到系统捕获的任意随机垃圾。

因此,您的错误几乎肯定是在正确初始化指针之前使用了某些东西,而在IDE中,您可以逃过这个错误,因为它不指向任何危险的地方-或者该值由您的错误检查处理-但在发布模式下,它会做一些令人讨厌的事情。


3

调试此类错误的好方法是为您的调试构建启用优化。


OP的问题在于它只在发布模式下崩溃,而不是调试器太慢。 - an inconspicuous semicolon

3
有时会出现这种情况,因为你已经将重要操作包含在“assert”宏中。你可能知道,“assert”仅在调试模式下评估表达式。

是的,这个问题困扰了我一段时间。我最终使用 VSCode 和搜索正则表达式 assert(.*( 找到了问题所在。 - Matt

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