为什么Minidumps无法提供良好的调用堆栈?

21

多年来,我在许多游戏项目中都使用了minidumps,在这些项目中,它们似乎有约50%的机会具有有效的调用堆栈。那么,我该怎么办才能使它们具有更好的调用堆栈呢?

我尝试将最新的dbghelp.dll放入exe目录中,这似乎对一些情况有所帮助。

使用Visual Studio 2008或2010是否更好?(我还在使用VS 2005)

我使用的代码看起来像这个示例

5个回答

33

您可以做的一件事是使用除Visual Studio之外的调试器,特别是使用WinDbg或其他使用dbgeng.dll(而不是Visual Studio使用的“Visual Studio Debugger”调试引擎)的工具,以提高在转储中找到的调用堆栈的准确性。

从我们的经验来看,WinDbg能够从相同的转储中生成良好的调用堆栈,并且可靠度达到100%,而在这些情况下,Visual Studio会产生无法使用或极其不准确的调用堆栈。据我所知,在未处理的异常是崩溃源的情况下,WinDbg会自动执行重建/恢复异常调用堆栈的棘手过程,但Visual Studio则不行(或无法?)。这两个调试器使用不同的启发式算法解释堆栈

WinDbg可能一开始会让人望而却步,因此这是我关于如何使其更容易甚至避免直接使用它的快速指南。

一个普通人提取良好调用堆栈的指南

这些选项按从“最快/最简单”到“最慢/最难解释”的顺序排列。

  1. 最简单的选择:使用来自微软的DbgDiag

这是一个鲜为人知的工具,自动化了许多常见问题的分析,足够简单易懂,即使是非程序员或客户也可以使用。它快速且几乎无误,已成为我快速分析传入崩溃转储的“首选”工具。

  • 启动“DebugDiag Analysis”应用程序
  • 在主页面上选择“CrashHangAnalysis”复选框
  • 将转储文件拖放到主页面上的“数据文件”窗格中
  • 点击“开始分析”


几秒钟到几分钟后,它将输出一个漂亮的.mhtml文件,其中包含有关问题的分析、所有相关线程的信息、完整的调用堆栈等。所有内容都带有超链接,易于使用。

DebugDiag甚至自动化了一些在WinDbg中可能很复杂但痛苦的分析(比如追踪应用程序中的350个线程中哪一个负责死锁)。

注意:由于安全原因,Chrome不会下载或打开.mhtml文件,因此您必须在Internet Explorer或Microsoft Edge中打开才能使用。这很麻烦,我已向DebugDiag团队(dbgdiag@microsoft.com)提出了更改格式为纯HTML的请求

  1. 中间选项:将WinDbg安装为Visual Studio的备用调试引擎
  • 如果您还没有安装Visual Studio,则需要先安装。这需要在下一步之前完成。
  • 安装Windows Driver Kit (WDK)
  • 启动Visual Studio,并且(这部分很重要!)使用新的“文件 -> 打开 -> 崩溃转储...”选项打开转储。这将使用Windows调试器调试崩溃转储(如果您改为将转储拖放到Visual Studio上或使用标准的“文件 -> 打开 -> 文件...”选项打开转储,则会使用旧版Visual Studio调试引擎进行调试...因此请小心选择正确的选项)。
  • 现在,您应该能够看到正确的调用堆栈并使用Visual Studio GUI进行导航,尽管有些事情工作方式不同(监视窗口需要使用不熟悉的WinDbg语法,线程ID不同等)。 注意:Visual Studio UI可能非常缓慢,特别是如果涉及许多线程并且打开了“线程”或“并行堆栈”窗口。
  1. 高级选项:直接使用WinDbg
  • 打开WinDbg.exe
  • 将转储文件拖到WinDbg窗口中
  • 键入!analyze -v并按Enter键。稍等片刻,WinDbg将输出崩溃调用堆栈,以及其对问题来源的估计。如果您正在分析死锁,则可以尝试!analyze -v -hang,WinDbg通常会显示涉及的依赖链。


此时您可能已经获得所需的所有信息!但是,如果您想在Visual Studio调试器中检查进程状态,可以执行以下其他步骤:

  • 在Visual Studio中打开崩溃转储文件
  • 右键单击调用堆栈窗口,选择“转到反汇编”
  • 将WinDbg输出调用堆栈顶部行的十六进制地址粘贴到反汇编窗口的“地址”栏中,然后按Enter键。现在,您位于崩溃位置,查看反汇编代码。
  • 右键单击反汇编窗口,选择“转到源代码”以进入该位置的源代码。现在,您正在查看崩溃现场的源代码。
注意:以上所有内容都需要正确配置符号服务器路径,否则您将无法解析调用堆栈中的符号。我建议设置_NT_SYMBOL_PATH环境变量,这样它就可以自动可用于Visual Studio、WinDbg和DebugDiag。

另外需要注意的是:为了获得更愉悦的调试体验,可以使用特殊的编译器标志将额外的信息注入到您的 PDB 中,从而使您能够正确地跟踪优化代码并在调用堆栈(以及分析器跟踪)中看到内联函数。这个标志在 VS2010 中作为未记录的标志“/d2Zi+”可用,然后在 VS2013 Update 3 中更改为官方标志“/Zo”。有关更多信息,请参见 http://randomascii.wordpress.com/2013/09/11/debugging-optimized-codenew-in-visual-studio-2012/。 - Chris Kline
更新了 DebugDiag 的链接:https://www.microsoft.com/zh-cn/download/details.aspx?id=58210#:~:text=Debug%20Diagnostic%20Tool%20(DebugDiag)%20是一款,in%20any%20user%2Dmode%20process. - John Dyer

6
您的调用栈中缺少什么?您是否有一堆地址,这些地址无法解析为有效的函数名称(例如0x8732ae00而不是CFoo:Bar())?如果是这样,那么您需要将您的.PDB文件放在调试器可以找到它们的位置,或者设置symbol server并在“模块”窗格的右键上下文菜单中设置“符号路径”。
我们每次有人检查新的Perforce changelist时都会存储每个二进制文件的每个.PDB,因此当来自办公室内部或任何零售客户的转储返回时,我们都有与他们运行的游戏版本相对应的.PDB。通过设置符号服务器和路径,我只需双击.mdmp文件,它就可以正常工作。
还是您的调用栈似乎只有一个函数?比如,在堆栈中没有其他函数,只有0x8538cf00?如果是这样,那么您的崩溃实际上是堆栈本身被损坏了。如果回溯链中的返回地址已被覆盖,调试器自然无法解析它们。
有时候你会发现实际触发 minidump 的线程并不是导致崩溃的异常抛出线程。在“线程”窗口中查看是否有其他线程中包含有问题的代码。
如果你正在调试一个“发布”版本——也就是说,开启了所有优化标志进行编译——你将不得不接受调试器无法找到本地变量和一些其他数据的事实。这是因为开启优化意味着允许编译器在寄存器中保留数据、折叠计算,并通常执行各种操作,从而防止数据实际写入堆栈。如果这是你的问题,那么你需要打开反汇编窗口手动追踪数据,或重新构建一个调试二进制文件并重现可以查看它的问题。

0x8732ae00是一个不太可能的地址,它在内核空间中(使用x86-32的2GB设置)。0x7_______地址更常见,因为Windows DLL紧贴着2GB边界。这减少了所需的重定位数量。如果您看不到它们的符号,请使用_Microsoft_符号服务器。 - MSalters
1
我只是随机获取地址作为例子(在这种情况下,这就是特定的机顶盒喜欢重新定位用户模式DLL的地方)。 - Crashworks

4

如果您需要堆栈转储,请关闭帧指针优化。帧指针用于显式定义堆栈帧。没有帧指针,调试器必须推断每个帧的位置。


这是一个好主意。不过,有了PDB和原始的DLL,MSVC的调试器可以使用FPO来解析堆栈帧,但它的工作变得更加困难。我知道这一点,因为我们使用FPO进行编译,我经常从minidumps中获取堆栈信息。 - Crashworks
如果“崩溃”是由于手动的INT 3断点引起的,那当然很容易解决。问题是,大多数崩溃并不会恰好发生在有缺陷的指令上。CPU会继续运行一段时间,直到触发故障。在此期间,执行的代码不会按预期工作,并且可能会相当程度地破坏程序状态。这可能包括执行您没有打算执行的指令(特别恶心的是:通过错误解释的虚表进行间接跳转)。虚表课程 - MSalters
1
是的,没有帧指针确实让手动回溯堆栈的任务更加困难。即使程序通过跳转wild vfunc指针而崩溃,通常你也可以找出它来自哪里,因为CALL操作会将IP推送到堆栈上,但找到它,然后弄清楚所有局部变量都去了哪里,可能需要逐个操作反向工作,这可能会变成一项艰巨的任务。如果你确实陷入这种境地,windbg有一个有用的"dps"命令,它可以在内存中搜索可能的已知符号和函数地址;那可以帮助你寻找旧的EIP。 - Crashworks

1
记录 minidump 的代码可能不太相关。minidump 记录的主要内容是模块信息(用于获取符号)和所有线程堆栈的完整内容。除了这些基本信息(始终被记录),其他都不重要。
获取好的符号(包括 PE 文件)对于堆栈跟踪至关重要。更多详细信息可以在这里找到:https://randomascii.wordpress.com/2013/03/09/symbols-the-microsoft-way/ 我发现 Visual Studio 通常可靠地显示调用堆栈。它自动显示与异常记录相关的调用堆栈,并使更改线程变得容易,以便您可以查看所有线程的调用堆栈。它有时会尝试“隐藏”它认为可能会让您困惑的细节——无论这是好还是坏取决于您的技能水平。
Windbg 默认显示记录崩溃转储的代码的调用堆栈,而不是崩溃的调用堆栈。Windbg 要求您输入“.ecxr”或“!analyze -v”才能查看崩溃堆栈。我觉得这很烦人。Windbg 还需要更多配置才能发挥作用。
这两个调试器确实具有不同的堆栈遍历启发式算法。例如,如果您调用或返回地址为零,则需要这些启发式算法,因为该地址没有解开信息。对于在正常代码中失败的“干净”崩溃,这些启发式算法就不那么重要了。
堆栈遍历在过去十年中肯定得到了改进。VS 2015 Community Edition非常强大且免费,因此您可以尝试使用它。
如果您使用windbg,则可以尝试一些实验:
!vc7fpo - toggles some of the windbg heuristics.
!stackdbg d, 7, f - turns on windbg stack walk
k1 - walks one level of the stack, spitting diagnostics as controlled by !stackdbg
dds esp - dumps the raw contents of the stack, doing a symbol lookup on each pointer

如果您升级到VS 2015仍然存在问题,则很可能堆栈跟踪失败是与您看到的崩溃特定相关的。如果缓冲区溢出在崩溃之前破坏了堆栈,那么调用堆栈将不可挽回地受损。您的问题提供的信息太少,无法给出明确的诊断。我发现两个调试器的堆栈显示都相当可靠,但我通常也知道为什么它们有时会失败,当发生这种情况时,我仍然可以提取所需的信息。

0

我不使用小型转储,而是通过“手动”将堆栈转储到日志文件中(请参见www.ddj.com/cpp/185300443How to Log Stack Frames with Windows x64)。

我遇到了与您类似的行为:有时存在有效的调用堆栈,有时则没有。在少数情况下,堆栈可能真的已经损坏了。在所有情况中,安装的异常处理程序都没有被调用!我猜这可能是Windows结构化异常处理的问题。


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