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


95

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

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

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

编辑:问题确实是由越界数组引起的,我将在本文中对此进行更多描述。感谢大家为发现此问题所提供的帮助!


您可以提供该测试方法的样本吗?
2008年

不用担心,代码太复杂了,无法轻松地粘贴到这里,正如我提到的,它不是在测试方法本身中发生的,而是随后的析构函数。但是,此方法中没有未初始化的指针或类似的东西。
Nik Reiman

3
大多数答案不过是猜测而已。有一些常见的技术可以在不附加调试器的情况下分析崩溃的发行版:stackoverflow.com/a/18513077/214777?stw=2
Sebastian

Answers:


127

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


31
有人给这个男人雪茄!就我而言,我传入的StringBuilder的容量不足以支持P / Invoke函数。我想这就像有人在睡觉时用魔术笔在你的脸上写字:在调试器下,他们最终在你的额头上乱涂乱画,所以你没有注意到,但是如果没有调试器,他们最终会刺伤你眼睛...类似的东西。感谢您的提示!
尼古拉斯·皮亚塞斯基

1
就我而言,事实证明这是使用Obj-C的ARM处理器上的对齐问题。
Almo

1
11年后,这仍然是正确的...不要忘记保留您的向量。
戴维·特伦

1
好的,那么如何改变调试模式的行为,以便可以实际调试。
Paul Childs

1
“现在知道要看的地方了”,但是调试中的所有工作如何告诉您问题出在哪里。尽管我认为您的答案在大多数情况下是正确的,并且知道要查找的内容是一个不错的开始,但浏览大型代码库以准确确定问题出在哪里可能会非常昂贵。
Paul Childs

54

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

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

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

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


典型的情况是忘记将成员变量放在构造函数成员初始化列表中的一个中。具有相同的效果,但是如果您不知道还应该寻找正确的成员初始化,就很难找到它。
08年

1
在调试模式下,变量通常被初始化为一些“编译器定义的常量”,可在调试中使用该常量指示变量处于什么状态。例如:指针NULL或0xDeadBeef很流行。
马丁·约克

调试运行时通常将内存初始化为一些非零值,特别是使NULL指针测试将导致代码的行为就像我们的指针非NULL一样。否则,您将在调试模式下正确运行的代码使发布模式崩溃。
Michael Burr

1
不,这些变量根本没有初始化,在分配给它们之前,仍然要“使用”它们。但是,底层内存内容通常预先填充有0x0000000或0xDEADBEEF或其他可识别的模式。
Lightness Races in Orbit

26

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

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

  2. 调试器的存在也可能会改变程序的行为,包括发行版和调试版。看到这个答案。简而言之,至少在连接到程序时,Visual Studio Debugger会自动使用Debug Heap。您可以使用环境变量_NO_DEBUG_HEAP关闭调试堆。您可以在计算机属性或Visual Studio的“项目设置”中指定此设置。这可能会使崩溃与附带的调试器一起再现。

    有关调试堆损坏的更多信息,请参见此处。

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

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

    一个。使用以下命令安装自定义终止处理程序std::set_terminate

    如果要在本地调试此问题,可以在终止处理程序内运行一个无限循环,并将一些文本输出到控制台以通知您std::terminate已被调用。然后连接调试器并检查调用堆栈。 或者,您按照此答案中的说明打印堆栈跟踪。

    在生产应用程序中,您可能希望将错误报告发送回国内,理想情况下是将其与一个小的内存转储一起发送,以便您按此处所述分析问题。

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


16

要注意的事情:

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

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

链接-是您的发行版本引入正确的库。

尝试的事情:

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


1
嗨-我对此答案进行了匿名否决。我想知道为什么吗?
morechilli 2013年

12

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

windbg /I

更多细节在这里

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


2
而且不要忘记,即使不是默认版本,您也可以让编译器为发布版本生成PDB文件。
Michael Burr

真正唯一回答这个问题的答案。
塞巴斯蒂安

10

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

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有了很多了解!:)


8
今天,我一直在调试类似的问题,并且_CrtCheckMemory()始终返回1。但是后来我意识到了原因:在发布模式下,_CrtCheckMemory是#defined的((int)1)。
Brian Morearty 2011年

7

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

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

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

PDB提供了到源的链接,其中提供了足够的符号信息,从而可以查看堆栈跟踪,变量等。您可以正常检查值,但要注意,由于优化通过可能仅意味着问题,因此可能会得到错误的读数。出现在寄存器中,或者事情发生的顺序与您预期的不同。

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


3

像这样的崩溃几乎总是造成的,因为IDE通常会将未初始化变量的内容设置为零,空值或其他类似的“明智”值,而在本地运行时,系统会拾取任何随机垃圾。

因此,几乎可以肯定,您的错误是您正在使用某种指针,就像在正确对其进行初始化之前正在使用指针一样,并且您正在IDE中摆脱它,因为它没有指向任何危险的地方-或该值由您处理。错误检查-但在发布模式下,它会令人讨厌。


3

为了进行故障转储,您可以分析:

  1. 为您的代码生成pdb文件。
  2. 您需要重新设置基础,以便将exe和dll加载到同一地址。
  3. 启用验尸调试器,例如Dr. Watson
  4. 使用崩溃查找器之类的工具检查崩溃失败的地址。

您还应该检出Windows调试工具中的工具。您可以监视应用程序,并查看在第二次机会异常之前的所有第一次机会异常。

希望能帮助到你...



2

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

因此,您可以按照以下方式进行操作:

长__stdcall MyFilter(EXCEPTION_POINTERS *)
{
    处理hEvt = :: CreateEventW(0,1,0,0);
    如果(hEvt)
    {
        if(WAIT_FAILED == :: WaitForSingleObject(hEvt,INFINITE))
        {
            //日志失败
        }
    }

}
//在您的wmain / WinMain中的某个位置:
SetUnhandledExceptionFilter(MyFilter);

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

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

.dump / ma path_to_dump_file

或立即对其进行调试。最简单的方法是跟踪运行时异常处理机制将处理器上下文保存在何处:

sd esp 范围 1003f

如果搜索长度不变,命令将在堆栈地址空间中搜索CONTEXT记录。我通常使用类似'l?10000'的名称。请注意,请勿使用过大的数字,因为您通常要在未处理的异常过滤器框架附近找到记录。1003f是用于捕获处理器状态的标志(我相信它对应于CONTEXT_FULL)的组合。您的搜索将类似于以下内容:

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

返回结果后,请使用cxr命令中的地址:

.cxr 0012c160

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

.exr -1

找出发生异常的确切原因。

希望能帮助到你。



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


1

带有调试器的Ntdll.dll

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

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

崩溃分析

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


1

Vista SP1实际上在系统中内置了一个非常好的崩溃转储生成器。不幸的是,默认情况下未启用它!

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

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


1

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

例如 :

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

: /*use array a doing some thing */

当一个人运行代码时,很有可能在调试模式下是正常的。

但是在发行版中,这可能会崩溃。

对我而言,在内存超出范围的范围内翻阅实在太麻烦了。

使用诸如Visual Leak Detector(windows)或valgrind(linux)之类的工具更为明智。


1

我看到了很多正确的答案。但是,没有什么可以帮助我。在我的情况下,SSE指令使用了错误的内存。查看您的数学库(如果使用的话),并尝试禁用SIMD支持,重新编译并重现崩溃。

例:

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

调试RelWithDebInfo配置运作良好,我的项目,而不是发布一个。此行为背后的原因可能是因为调试器处理分配/取消分配请求并进行一些内存记账以检查和验证对内存的访问。

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


0

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

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

我花了很多时间来追踪它,我之所以来到这里是因为我在一个新闻组上询问,有人让我考虑了一下。因此,让我回馈您,以防万一您也遇到这种情况。


0

我发现本文对您的情况很有用。ISTR的编译器选项有些过时了。看看您的Visual Studio项目选项,看看如何为发行版本等生成pdb文件。


0

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


0

由于优化更改了代码行的执行顺序,因此调试发布版本可能会很痛苦。确实会造成混乱!

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


他已经尝试过使用printfs,所以消息框不会给聚会带来任何新鲜事物。
格雷格·惠特菲尔德

0

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


0

您可以在启用了全局标志的情况下运行软件(在Windows调试工具中查找)。它通常会帮助解决问题。


0

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


0

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

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


-3

我遇到了这个错误,即使尝试!clean,vs也崩溃了!我的项目。因此,我从Release目录中手动删除了obj文件,然后就可以正常构建了。


-6

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


这可能适用于10%的应用程序,但肯定不适用于所有应用程序。您是否要玩在构建DEBUG时发布的游戏?甚至在与PDB一起时,以易于拆卸的方式放弃您的商标秘密安全代码?我猜不会。
steffenj

Steffenj:我希望游戏开发人员能够发现错误。理想情况下,在他们发货之前,但如果要在以后,我希望他们能够获得足够的信息来进行复制和跟踪。如果是密码,则商标不适用。PDB?蛋白质数据库?python调试器?
08年

恕我直言,这是一个坏主意。可执行文件更大,它们没有经过优化,运行速度也慢得多。这些情况确实很少见。即使发生这种情况时尤其令人发疯。您不应该一直交付劣质的产品,而要担心极其罕见的最坏情况的调试。(我的不是众多不赞成投票的人之一。)我为NASA做过一些编程。我们说过,至少每一行代码都应该测试一次。单元测试也可以提供帮助。
CodeLurker
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.