程序只会崩溃作为发布版本 - 如何调试?

时间:2008-10-09 07:18:43

标签: c++ debugging

我在这里有一个“Schroedinger's Cat”类型的问题 - 我的程序(实际上是我的程序的测试套件,但是程序仍然是)崩溃,但只有在发布模式下构建时才会发生命令行。通过穴居人调试(即整个地方讨厌的printf()消息),我已经确定了代码崩溃的测试方法,但遗憾的是实际崩溃似乎发生在某些析构函数中,因为我看到的最后一条跟踪消息是其他执行干净的析构函数。

当我尝试在Visual Studio中运行此程序时,它不会崩溃。从WinDbg.exe启动时也是如此。仅从命令行启动时才会发生崩溃。这是在Windows Vista,btw下发生的,不幸的是我现在无法访问XP机器进行测试。

如果我可以让Windows打印出堆栈跟踪,或某些,而不是简单地终止程序,就好像它已经完全退出一样,那将是非常好的。有没有人对如何在这里获得更有意义的信息有任何建议,希望能解决这个问题?

编辑:问题确实是由一个越界数组which I describe more in this post引起的。感谢大家帮忙找到这个问题!

29 个答案:

答案 0 :(得分:119)

在我见过或听说过的100%的情况下,C或C ++程序在调试器中运行良好但在外部运行时失败,原因是写入函数本地数组的末尾。 (调试器会在堆栈上放置更多内容,因此您不太可能覆盖重要的内容。)

答案 1 :(得分:49)

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

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

在调试模式下,if中的代码未执行但在发布模式下p包含未定义的值,该值不太可能为0,因此代码执行时经常导致崩溃。

我会检查你的代码是否有未初始化的变量。这也适用于数组的内容。

答案 2 :(得分:20)

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

  1. 发布和调试版本的行为有很多原因。 Here is an excellent overview.这些差异中的每一个都可能导致版本构建中的一个错误,而这个错误在Debug版本中是不存在的。

  2. 对于发布版本和调试版本,调试器的存在也可能会改变程序的行为See this answer.简而言之,至少Visual Studio Debugger在附加到程序时会自动使用Debug Heap。您可以使用环境变量_NO_DEBUG_HEAP关闭调试堆。您可以在计算机属性中或在Visual Studio的“项目设置”中指定此项。这可能会使附加的调试器重现崩溃。

    More on debugging heap corruption here.

  3. 如果以前的解决方案不起作用,您需要捕获未处理的异常并附加事后调试器发生崩溃的实例。你可以用例如WinDbg,details about the avaiable post-mortem debuggers and their installation at MSDN

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

    一个。 使用std::set_terminate

    安装自定义终止处理程序

    如果要在本地调试此问题,可以在终止处理程序中运行无限循环,并将一些文本输出到控制台以通知您已调用std::terminate。然后附加调试器并检查调用堆栈。 Or you print the stack trace as described in this answer.

    在生产应用程序中,您可能希望将错误报告发回国内,最好是together with a small memory dump that allows you to analyze the problem as described here.

    使用Microsoft的结构化异常处理机制,可以捕获硬件和软件异常。 See MSDN。您可以使用SEH保护代码的一部分,并使用与a)相同的方法来调试问题。 SEH提供了有关从生产应用程序发送错误报告时可能使用的异常的更多信息。

答案 3 :(得分:15)

需要注意的事项:

数组溢出 - visual studio调试器插入可能会停止崩溃的填充。

竞争条件 - 您是否涉及多个线程,如果这样,竞争条件很多只会在直接执行应用程序时出现。

链接 - 是你的发布版本,引入正确的库。

要尝试的事情:

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

答案 4 :(得分:12)

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

windbg /I

更多详情here

至于原因,它很可能是其他答案所暗示的一个酉变量。

答案 5 :(得分:9)

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

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

这是fencepost错误(off-by-one错误)并由以下人员修正:

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

奇怪的是,我在代码的各个部分周围调用了几次_CrtCheckMemory(),并且它们总是返回1.我能够通过放置“return false”找到问题的根源。在测试案例中调用,然后最终通过反复试验确定故障的位置。

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

答案 6 :(得分:7)

即使您已将exe作为发布版本构建,您仍然可以生成PDB(程序数据库)文件,这些文件将允许您堆叠跟踪,并执行有限数量的变量检查。 在构建设置中,有一个创建PDB文件的选项。打开它并重新链接。然后尝试先从IDE运行,看看你是否遇到了崩溃。如果是这样,那就太好了 - 你们都准备好看待事物。如果没有,那么当从命令行运行时,您可以执行以下两项操作之一:

  1. 运行EXE,在崩溃之前执行“附加到进程”(Visual Studio上的“工具”菜单)。
  2. 崩溃后,选择启动调试器的选项。
  3. 当要求指向PDB文件时,浏览以查找它们。如果将PDB放在与EXE或DLL相同的输出文件夹中,它们可能会被自动拾取。

    PDB提供了一个到源的链接,其中包含足够的符号信息,可以查看堆栈跟踪,变量等。您可以正常检查这些值,但请注意,由于优化过程可能会得到错误的读数意思是只出现在寄存器中,或者事情的顺序与你期望的不同。

    注意:我在这里假设一个Windows / Visual Studio环境。

答案 7 :(得分:3)

这样的崩溃几乎总是因为IDE通常会将未初始化的变量的内容设置为零,null或其他一些“敏感”值,而在本地运行时,您将获得系统拾取的任何随机垃圾

因此,您的错误几乎可以肯定,您正在使用类似于使用指针之前已经正确初始化并且您在IDE中使用指针,因为它没有任何危险 - 或者值是由您的错误检查处理 - 但在发布模式下它会做一些令人讨厌的事情。

答案 8 :(得分:3)

为了拥有可以分析的崩溃转储:

  1. 为您的代码生成pdb文件。
  2. 你可以将你的exe和dll加载到同一个地址。
  3. 启用事后调试程序,例如Dr. Watson
  4. 使用crash finder等工具检查崩溃失败地址。
  5. 您还应该查看Debugging tools for windows中的工具。 您可以监控应用程序并查看第二次机会异常之前的所有第一次机会异常。

    希望它有所帮助...

答案 9 :(得分:2)

有时会发生这种情况,因为你已经将重要的操作包裹在内部&#34;断言&#34;宏。你可能知道,&#34;断言&#34;仅在调试模式下计算表达式。

答案 10 :(得分:2)

当我的应用程序与您的行为相似时,我遇到了问题。事实证明,这是sprintf中一个令人讨厌的缓冲区溢出。当然,它在附带调试器的情况下运行。我做的是安装一个未处理的异常过滤器(SetUnhandledExceptionFilter),其中我只是无限地阻塞(使用具有超时值INFINITE的虚假句柄上的WaitForSingleObject)。

所以你可以采取以下措施:

long __stdcall MyFilter(EXCEPTION_POINTERS *)
{
    HANDLE hEvt=::CreateEventW(0,1,0,0);
    if(hEvt)
    {
        if(WAIT_FAILED==::WaitForSingleObject(hEvt, INFINITE))
        {
            //log failure
        }
    }

}
// somewhere in your wmain/WinMain:
SetUnhandledExceptionFilter(MyFilter);

然后我在bug出现之后附加了调试器(gui程序停止响应)。

然后你可以采取转储并在以后使用它:

.dump / ma path_to_dump_file

立即调试。最简单的方法是跟踪运行时异常处理机制保存处理器上下文的位置:

s-d esp 范围 1003f

如果搜索的长度,命令将搜索堆栈地址空间以查找CONTEXT记录。我通常使用类似'l?10000'的东西。注意,不要使用非常大的数字作为通常靠近无法处理的异常过滤帧的记录。 1003f是用于捕获处理器状态的标志组合(我相信它对应于CONTEXT_FULL)。 您的搜索结果与此类似:

0:000&GT; s-d esp l1000 1003f
0012c160 0001003f 00000000 00000000 00000000?...............

获得结果后,请使用cxr命令中的地址:

.cxr 0012c160

这将带您进入这个新的上下文,恰好在崩溃时(您将在应用程序崩溃时获得完全堆栈跟踪)。 另外,使用:

.exr -1

确切地找出发生了哪个例外。

希望它有所帮助。

答案 11 :(得分:1)

Vista SP1实际上有一个非常好的崩溃转储生成器内置到系统中。不幸的是,默认情况下它没有打开!

看到这篇文章: http://msdn.microsoft.com/en-us/library/bb787181(VS.85).aspx

这种方法的好处是不需要在受影响的系统上安装额外的软件。抓住它并撕开它,宝贝!

答案 12 :(得分:1)

根据我的经验,这主要是内存损坏问题。

例如:

char a[8];
memset(&a[0], 0, 16);

: /*use array a doing some thing */

当运行代码时,很可能在调试模式下正常。

但是在发布时,那可能会崩溃。

对我来说,翻找内存不合适的事情太过于无聊了。

使用Visual Leak Detector(windows)或valgrind(linux)这样的工具是更明智的选择。

答案 13 :(得分:1)

附带调试器的Ntdll.dll

从IDE或WinDbg启动程序而不是从命令行/桌面启动程序之间的一个小小的已知区别是,当使用附加的调试器(即IDE或WinDbg)启动时,ntdll.dll使用执行的不同堆实现关于内存分配/释放的一些小验证。

您可以在unexpected user breakpoint in ntdll.dll中阅读一些相关信息。可以帮助您确定问题的一个工具是PageHeap.exe

崩溃分析

你没有写下你遇到的“崩溃”。一旦程序崩溃并提供您将错误信息发送给Microsoft,您应该能够单击技术信息并至少检查异常代码,并且通过一些努力,您甚至可以执行事后分析(请参阅{ {3}}获取指示)

答案 14 :(得分:1)

调试这样的错误的好方法是为调试版本启用优化。

答案 15 :(得分:1)

我已经看到了很多正确的答案。然而,没有一个帮助我。就我而言,使用未对齐内存时, SSE指令的使用有误。看看你的数学库(如果你使用的话),并尝试禁用SIMD支持,重新编译并重现崩溃。

示例:

项目包括mathfu,并使用带有STL向量的类: std :: vector&lt; mathfu :: vec2&gt; 。这种用法可能会在构建 mathfu :: vec2 项时导致崩溃,因为STL默认分配器不保证所需的16字节对齐。在这种情况下,为了证明这个想法,可以在每个包含mathfu之前定义#define MATHFU_COMPILE_WITHOUT_SIMD_SUPPORT 1,在Release配置中重新编译并再次检查。

调试 RelWithDebInfo 配置适用于我的项目,但不适用于发布。这种行为背后的原因可能是因为调试器处理分配/释放请求并执行一些内存记录来检查和验证对内存的访问。

我在Visual Studio 2015和2017环境中遇到过这种情况。

答案 16 :(得分:1)

关于获取诊断信息的问题,您是否尝试过使用adplus.vbs替代WinDbg.exe?要附加到正在运行的进程,请使用

adplus.vbs -crash -p <process_id>

或者在崩溃快速发生的情况下启动应用程序:

adplus.vbs -crash -sc your_app.exe

有关adplus.vbs的完整信息,请访问:http://support.microsoft.com/kb/286350

答案 17 :(得分:0)

在这种情况下,我曾有人可能会有所启发。它仅在Qt Creator的发行版中崩溃-不在调试中崩溃。我使用的是.ini文件(因为我更喜欢可以复制到其他驱动器的应用程序,而不是那些在注册表损坏时丢失设置的应用程序)。这适用于将其设置存储在应用程序目录树下的所有应用程序。如果调试版本和发布版本位于不同的目录下,则您也可以使用不同的设置。我选择了其中一个未选中的偏好。原来是我坠机的根源。好东西,我找到了。

我不想这么说,但是我只是在MS Visual Studio Community Edition中诊断出崩溃;安装VS之后,让我的应用在Qt Creator中崩溃,并选择在 Visual Studio的调试器中打开它。虽然我的Qt应用程序没有符号信息,但事实证明Qt库中有一些。它把我引向了冒犯性的路线;因为我可以看到正在调用什么方法。 (不过,我认为Qt是一个方便,强大且跨平台的LGPL框架。)

答案 18 :(得分:0)

我也有这个问题。在我的情况下,RELEASE模式在链接器定义中包含msvscrtd.dll。我们将其删除,问题得以解决。

或者,将/ NODEFAULTLIB添加到链接器命令行参数也可以解决此问题。

答案 19 :(得分:0)

我将为未来的读者添加另一种可能性:检查您是否从没有控制台窗口的应用程序登录到 stderr 或 stdout(即您与 /SUBSYSTEM:WINDOWS 链接)。这可能会崩溃。

我有一个 GUI 应用程序,我在调试和发布中都登录到 stderr 和一个文件,因此始终启用日志记录。我在调试中创建了一个控制台窗口,以便于查看日志,但不在发布中。但是,如果 VS 调试器附加到发布版本,它会自动通过管道将 stderr 传送到 VS 输出窗口。所以只有在没有调试器的版本中,当我写到 stderr 时它才会真正崩溃。

更糟糕的是,printf 调试显然不起作用,直到我找到根本原因(通过在各个位置插入无限循环来痛苦地将代码库一分为二),我才明白为什么。

>

答案 20 :(得分:0)

在发生异常时让程序生成一个小型转储,然后在调试器中打开它(例如,在WinDbg中)。要看的关键功能:MiniDumpWriteDump,SetUnhandledExceptionFilter

答案 21 :(得分:0)

您可以在启用Global Flags的情况下运行您的软件(查看Windows调试工具)。它经常有助于解决问题。

答案 22 :(得分:0)

尝试使用 _CrtCheckMemory()查看已分配内存的状态。 如果一切顺利, _CrtCheckMemory 会返回 TRUE ,否则 FALSE

答案 23 :(得分:0)

调试版本构建可能会很麻烦,因为优化会更改代码行的执行顺序。它真的会让人感到困惑!

至少缩小问题范围的一种方法是使用MessageBox()显示快速语句,说明代码所属程序的哪一部分(“Starting Foo()”,“Starting Foo2()”);开始将它们放在你怀疑的代码区域中的函数顶部(当它崩溃时你在做什么?)。当您可以分辨哪个函数时,将消息框更改为代码块甚至是该函数中的各个行,直到将其缩小到几行为止。然后,您可以开始打印出变量的值,以查看它们在崩溃时的状态。

答案 24 :(得分:0)

怀疑它会发生在调试器之外,而不是在内部;在调试器中运行通常不会更改应用程序行为。我会检查控制台和IDE之间的环境差异。此外,显然,编译版本没有优化和调试信息,看看是否会影响行为。最后,查看其他人在这里建议的验尸调试工具,通常你可以从中获得一些线索。

答案 25 :(得分:0)

我发现这个this article对您的方案很有用。 ISTR的编译器选项有点过时了。查看Visual Studio项目选项,了解如何为发布版本生成pdb文件等。

答案 26 :(得分:0)

GCC曾经发生类似的事情。事实证明,这是一种过于激进的优化,仅在创建最终版本时才启用,而不是在开发过程中启用。

嗯,说实话这是我的错,而不是gcc的,因为我没有注意到我的代码依赖于那个特定优化不会完成的事实。

我花了很多时间来追踪它,我只是来到它,因为我在一个新闻组上询问,有人让我考虑一下。所以,让我回复你的好意,以防万一发生这种情况。

答案 27 :(得分:-3)

我遇到了这个错误,即使在尝试清理时也没有崩溃!我的项目。所以我从Release目录中手动删除了obj文件,之后它构建得很好。

答案 28 :(得分:-6)

我同意罗尔夫。因为可重现性非常重要,所以不应该使用非调试模式。您的所有构建都应该是可调试的。调试两个目标可以使调试负载增加一倍以上。只需发布“调试模式”版本,除非它不可用。在这种情况下,让它可用​​。

相关问题