如何在没有有用的调用堆栈的情况下调试难以重现的崩溃?

Posted

技术标签:

【中文标题】如何在没有有用的调用堆栈的情况下调试难以重现的崩溃?【英文标题】:How do I debug a difficult-to-reproduce crash with no useful call stack? 【发布时间】:2011-06-10 18:56:40 【问题描述】:

我在我们的软件中遇到了一个奇怪的崩溃,我在调试它时遇到了很多麻烦,所以我寻求 SO 的建议来解决它。

崩溃是读取 NULL 指针的访问冲突:

$00CF0041 的第一次机会例外。 带有消息的异常类 $C0000005 '在 0x00cf0041 的访问冲突:读取 地址为 0x00000000'。

它只发生在“有时”——我还没有弄清楚什么时候发生的任何韵律或原因——而且只发生在主线程中。当它发生时,调用堆栈包含一个不正确的条目:

对于主线程,它应该显示一个装满其他项目的大堆栈。

此时,所有其他线程都处于非活动状态(主要位于WaitForSingleObject 或类似函数中。)我只看到在主线程中发生了这种崩溃。在同一个地址的同一个方法中,它始终具有一个条目的相同调用堆栈。此方法可能相关,也可能不相关——我们确实在应用程序中使用了 VCL。不过,我敢打赌,某些东西(可能是很久以前)正在破坏堆栈,而它崩溃的地址实际上是随机的。请注意,它在多个构建中一直是相同的地址 - 它可能不是真正随机的。

这是我尝试过的:

试图在某个点可靠地重现它。我没有发现任何东西每次都可以重现它,还有一些偶尔会做或不做的事情,没有明显的原因。这些操作不足以将其缩小到特定的代码部分。这可能与时间有关,但在 IDE 中断时,其他线程通常什么都不做。我不能排除线程问题,但认为不太可能。 使用额外的调试语句(额外的调试信息、额外的断言等)构建。这样做之后,就不会发生崩溃。 在启用Codeguard 的情况下构建。这样做之后,崩溃就不会发生,Codeguard 也不会显示任何错误。

我的问题:

1.如何找到导致崩溃的代码?我该怎么做相当于往回走?

2。对于如何追踪此次崩溃的原因,您有什么一般性建议?

我正在使用Embarcadero RAD Studio 2010(该项目主要包含C++ Builder代码和少量Delphi。)

编辑:我想我应该添加实际造成这种情况的原因。有一个线程调用ReadDirectoryChangesW,然后使用GetOverlappedResult 等待事件继续并处理更改。该事件也被发出信号,以便在设置状态标志后终止线程。问题是当线程退出时它从未调用CancelIO。结果,Windows 仍在跟踪更改,并且在目录更改时可能仍在写入缓冲区,即使缓冲区、重叠结构和事件不再存在(创建它们的线程上下文也不存在)。当CancelIO被调用,没有更多的崩溃。

【问题讨论】:

我不熟悉 CodeGaurd - 它是否也引入了堆栈金丝雀和验证?我问是因为您正在混合 C++ 和 Delphi - 这意味着您可能会在没有意识到的情况下混合调用约定。这会很快弄乱你的堆栈,表现为主线程上看似随机的崩溃,调用堆栈损坏。 Codeguard 用字节模式填充堆栈的未初始化部分。它还(尝试)验证访问释放的内存、分配的内存溢出等事情。调用约定错误肯定会导致这样的事情,是的(感谢您的建议!)但如果是这样,我不知道在哪里:C++ Builder 旨在与 Delphi 代码互操作,我们必须在某个地方的声明中出错,而且大多数是 IDE 或编译器管理的。我想关键问题是,我将如何找到一个错误声明的方法? 我没有把它作为答案,因为它含糊不清,但您可能想尝试不同的调试器。你可以给例如如果调用堆栈被损坏或混淆,WinDbg 会提示(或一切)重建真正的调用堆栈。 【参考方案1】:

即使 IDE 提供的堆栈跟踪不是很完整,这并不意味着堆栈上仍然没有有用的信息。打开 CPU 视图并查看堆栈窗格;对于每个 CALL 操作码,都会将返回地址压入堆栈。由于堆栈向下增长,您会在当前堆栈位置上方找到这些返回地址,即通过在堆栈窗格中向上滚动。

主线程的堆栈将在 $00120000 或 $00180000 左右(Vista 及更高版本中的地址空间随机化使其更加随机)。主要可执行文件的代码将在 00400000 美元左右。您可以通过右键单击堆栈条目并选择 Follow -> Near Code 来推测性地调查堆栈中看起来不像整数数据(低值)或堆栈地址($00120000+ 范围)的元素,这将导致反汇编窗口跳转到该代码地址。如果它看起来像无效代码,则它可能不是堆栈跟踪中的有效条目。如果它是有效代码,它可能是 OS 代码(通常约为 77000000 美元及以上),在这种情况下,您将没有有意义的符号,但您会经常遇到实际正确的堆栈条目。

这种技术虽然有些费力,但可以在调试器无法跟踪事物时为您提供有意义的堆栈跟踪信息。但是,如果 ESP(堆栈指针)被搞砸了,它对您没有帮助。幸运的是,这种情况很少见。

【讨论】:

谢谢巴里!这是非常有帮助的 - 无论如何都是非常有用的信息。 这刚刚解决了可能是这个错误的问题(或其他问题 - 无论哪种方式,它都非常有帮助。我最近发现了一些随机代码!)感谢您抽出宝贵时间回答 - 我'刚刚将其标记为问题的答案。【参考方案2】:

这就是我制作 Process Stack 查看器的原因 :-) http://code.google.com/p/asmprofiler/wiki/ProcessStackViewer

它可以通过raw堆栈跟踪显示堆栈,因此在无法进行正常堆栈跟踪时会显示完整的堆栈。 但请注意:原始堆栈跟踪将显示“误报”!将列出堆栈上可以找到函数名称的任何地址。

当我遇到与您相同的问题时,它帮助了我很多次(由于堆栈状态无效,Delphi 无法正常堆栈遍历)

编辑:新版本上传,网站上是旧版本(我自己经常使用新版本) http://asmprofiler.googlecode.com/files/AsmProfiler_Sampling%20v1.0.7.13.zip

【讨论】:

rrrr最近太忙了,已经了解了,谢谢提醒!! +1 听起来你已经自动化了我描述的过程。 听起来很有用!我试试看。 Andre,我已经尝试过了,我通过它显示的默认堆栈跟踪获得了更多信息,但仍然不多。 “原始堆栈跟踪”复选框已禁用,因此我无法启用它。知道为什么吗?它发生在 Vista64 和 32 上。 大卫,抱歉,新版本上传【参考方案3】:

线程可能是这里的原因。通常的嫌疑人是在堆栈上使用 OVERLAPPED 结构的线程和将指向堆栈上对象的指针发送到其他线程的线程。

如果您使用Deubgging Tools For Windows 并使用“dps”命令,可能会恢复部分堆栈信息。

【讨论】:

谢谢约翰,我会调查一下。我已经编写了我们的大部分线程代码,它在传递对象的地方肯定是动态分配的。不过,我仍然会仔细检查! Windows 调试工具是否可以使用未使用 Microsoft 编译器编译且不使用其调试信息格式的代码?例如,Embarcadero 的工具不会生成 PDB 文件。 Windows 工具需要兼容的符号格式(最好是 PDB,但为此目的,甚至 DBG 文件也可以)。 您可以将 Delphi 地图文件转换为(旧)Windows DBG 格式:code.google.com/p/map2dbg 但是,最新的 Visual Studio(2008、2010)不支持 DBG,但 windbg 仍然接受它?跨度> 【参考方案4】:

我不能 100% 确定,但从您提供的图像来看,我相信在执行过程中的某个地方,您正试图访问 TList 中为 NULL 的对象。即:

AList[Index].SomeProperty/SomeMethod/etc. <-- error if (AList[Index] == NULL)

关于调试和找到引发异常的实际位置从来都不是一件容易的事,尤其是在信息不多或难以重现的情况下,在这种情况下,我通常:

从主窗体的执行开始一步一步进行(如果没有例外)

在逐步进行的过程中,如果我发现任何不安全的代码,我会将其放在 try...except 和索引条件之间(如果我有数组、列表、要传递的预期值等)

如果上面没有找到问题,请检查是否某些库失败

使用 Eureka 日志,它有时也会失败(很少几次),但它通常会为您指明正确的方向

我遇到过很多与你类似的问题,我可以告诉你,这个问题几乎是一个非常容易解决的问题,但是当错误弹出时,我没有得到错误的“点附近”。

【讨论】:

我知道代码似乎正在访问 TList,但可能不是。堆栈被破坏了,所以谁知道它的那一部分是否有效。 Eureka Log 是一个有趣的建议:我听说过但从未使用过! @David M 你应该这样,它节省了很多时间,当我第一次听说它时,我持怀疑态度,但经过几次测试后,我对它节省了多少时间印象深刻再次,Eureka 在某些情况下会失败,但这种情况很少。

以上是关于如何在没有有用的调用堆栈的情况下调试难以重现的崩溃?的主要内容,如果未能解决你的问题,请参考以下文章

使用Windows事件查看器调试崩溃

没有堆栈时如何调试iOS断言失败崩溃

当 gcc 中的应用程序在没有 gdb 的情况下崩溃时,如何生成堆栈转储和转储的寄存器值?

Xcode 4.2 调试不象征堆栈调用

sigabrt 没有错误信息

QProgressDialog::setValue 产生随机堆栈溢出