繁体   English   中英

程序仅在发布版本时崩溃——如何调试?

[英]Program only crashes as release build — how to debug?

我在这里遇到了“薛定谔的猫”类型的问题——我的程序(实际上是我的程序的测试套件,但仍然是一个程序)崩溃了,但只有在发布模式下构建时,并且只有从命令行启动时. 通过穴居人调试(即到处都是讨厌的 printf() 消息),我确定了代码崩溃的测试方法,尽管不幸的是,实际崩溃似乎发生在某些析构函数中,因为我看到的最后一条跟踪消息在其他执行干净的析构函数。

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

这将是非常好的,如果我能得到的Windows打印出堆栈跟踪,或其他的东西比简单地结束,如果它已经退出干净方案。 有没有人对我如何在这里获得更有意义的信息并希望修复此错误有任何建议?

编辑:问题确实是由越界数组引起的, 我在这篇文章中对此进行了更多描述 感谢大家帮助找到这个问题!

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

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

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

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

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

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

  1. 由于多种原因,发布和调试版本的行为不同。 这是一个很好的概述。 这些差异中的每一个都可能导致发布版本中存在调试版本中不存在的错误。

  2. 调试器的存在也可能改变程序的行为,无论是发布版本还是调试版本。 看到这个答案。 简而言之,至少 Visual Studio 调试器在附加到程序时会自动使用调试堆。 您可以使用环境变量 _NO_DEBUG_HEAP 关闭调试堆。 您可以在计算机属性或 Visual Studio 的项目设置中指定此项。 这可能会使连接调试器的崩溃重现。

    更多关于在此处调试堆损坏的信息。

  3. 如果之前的解决方案不起作用,您需要捕获未处理的异常并在发生崩溃的实例上附加一个事后调试器 您可以为此使用例如 WinDbg, 有关 MSDN 上可用的事后调试器及其安装的详细信息

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

    一种。 使用std::set_terminate安装自定义终止处理程序

    如果你想在本地调试这个问题,你可以在终止处理程序中运行一个无限循环并向控制台输出一些文本来通知你std::terminate已被调用。 然后附加调试器并检查调用堆栈。 或者您按照本答案中的描述打印堆栈跟踪。

    在生产应用程序中,您可能希望将错误报告发送回家,最好连同一个小型内存转储一起发送,以便您按此处所述分析问题。

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

需要注意的事项:

数组溢出 - Visual Studio 调试器插入可能会阻止崩溃的填充。

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

链接 - 您的发布版本是否包含了正确的库。

尝试的事情:

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

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

windbg /I

更多细节在这里

至于原因,正如其他答案所暗示的那样,它很可能是一个未初始化的变量。

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

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的知识! :)

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

  1. 运行 EXE,并在崩溃之前执行附加到进程(Visual Studio 上的工具菜单)。
  2. 崩溃后,选择启动调试器的选项。

当要求指向 PDB 文件时,浏览以找到它们。 如果 PDB 与 EXE 或 DLL 放在相同的输出文件夹中,它们可能会被自动提取。

PDB 提供了一个带有足够符号信息的源链接,可以查看堆栈跟踪、变量等。您可以照常检查这些值,但请注意,您可能会得到错误读数,因为优化传递可能仅意味着某些事情出现在寄存器中,或者事情发生的顺序与您预期的不同。

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

像这样的崩溃几乎总是引起的,因为 IDE 通常会将未初始化的变量的内容设置为零、空或其他一些此类“合理”值,而在本机运行时,您将获得系统拾取的任何随机垃圾。

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

为了获得可以分析的故障转储:

  1. 为您的代码生成 pdb 文件。
  2. 你变基让你的 exe 和 dll 加载到相同的地址。
  3. 启用事后调试器,例如Dr. Watson
  4. 使用诸如crash finder 之类的工具检查崩溃失败地址。

您还应该查看Debugging tools for windows中的 工具 您可以监视应用程序并查看第二次机会异常之前的所有第一次机会异常。

希望能帮助到你...

调试此类错误的一个好方法是为您的调试版本启用优化。

有一次,当应用程序的行为与您的相似时,我遇到了问题。 结果证明是 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);

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

然后,您可以进行转储并稍后使用它:

.dump /ma path_to_dump_file

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

1003f sd esp1003f

命令将为 CONTEXT 记录搜索堆栈地址空间,提供搜索长度。 .我通常使用类似东西。 请注意,不要使用异常大的数字作为通常靠近未处理异常过滤器帧的记录。 1003f 是用于捕获处理器状态的标志组合(我相信它对应于 CONTEXT_FULL)。 您的搜索将类似于以下内容:

0:000> sd esp l1000 1003f
0012c160 0001003f 00000000 00000000 00000000 ......................

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

.cxr 0012c160

这将带您进入这个新的 CONTEXT,恰好在崩溃时(您将准确地获得应用程序崩溃时的堆栈跟踪)。 此外,使用:

.exr -1

以准确找出发生了哪个异常。

希望能帮助到你。

有时发生这种情况是因为您在“assert”宏中包含了重要的操作。 您可能知道,“assert”仅在调试模式下评估表达式。

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

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

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

关于获取诊断信息的问题,您是否尝试过使用 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

带有调试器的 Ntdll.dll

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

您可能会在 ntdll.dll中的意外用户断点中阅读一些相关信息。 一种可能可以帮助您识别问题的工具是PageHeap.exe

崩溃分析

您没有写下您正在经历的“崩溃”是什么。 一旦程序崩溃并让您将错误信息发送给 Microsoft,您应该能够单击技术信息并至少检查异常代码,并且通过一些努力您甚至可以执行事后分析(请参阅Heisenbug : WinApi 程序在某些计算机上崩溃)以获取说明)

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

例如 :

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

: /*use array a doing some thing */

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

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

对我来说,翻找记忆出界的地方太费劲了。

使用一些工具,如Visual Leak Detector (windows) 或valgrind (linux) 是更明智的选择。

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

例子:

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

DebugRelWithDebInfo配置适用于我的项目,但不适用于Release 1。 这种行为背后的原因可能是因为调试器处理分配/解除分配请求并执行一些内存簿记以检查和验证对内存的访问。

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

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

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

我花了很多时间去追踪它,我之所以找到它,是因为我在一个新闻组上提问,有人让我思考它。 所以,让我回报一下,以防万一这也发生在你身上。

我发现这篇文章对您的场景很有用。 ISTR 编译器选项有点过时了。 环顾您的 Visual Studio 项目选项,了解如何为您的发布版本等生成 pdb 文件。

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

由于优化更改了代码行的执行顺序,因此调试发布版本可能会很痛苦。 它真的会让人困惑!

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

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

您可能会在启用全局标志的情况下运行您的软件(查看 Windows 调试工具)。 它通常有助于解决问题。

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

这是我遇到的一个案例,有人可能会觉得很有启发性。 它只在 Qt Creator 的发布中崩溃 - 而不是在调试中。 我使用的是 .ini 文件(因为我更喜欢可以复制到其他驱动器的应用程序,而不是那些在注册表损坏时丢失设置的应用程序)。 这适用于将其设置存储在应用程序目录树下的任何应用程序。 如果调试和发布版本位于不同的目录下,您也可以在它们之间使用不同的设置。 我有偏好签入一个没有签入另一个。 原来它是我崩溃的根源。 还好我找到了。

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

我也有这个问题。 就我而言,RELEASE 模式在链接器定义中包含 msvscrtd.dll。 我们删除了它并解决了问题。

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

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

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM