为啥 Minidumps 不能提供好的调用堆栈?

Posted

技术标签:

【中文标题】为啥 Minidumps 不能提供好的调用堆栈?【英文标题】:Why don't Minidumps give good call stacks?为什么 Minidumps 不能提供好的调用堆栈? 【发布时间】:2010-12-05 21:13:47 【问题描述】:

多年来,我在许多游戏项目中使用了小型转储,它们似乎有大约 50% 的机会拥有有效的调用堆栈。我能做些什么来让它们拥有更好的调用堆栈?

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

Visual Studio 2008 或 2010 更好吗? (我仍在使用 VS 2005)。

我使用的代码类似于this sample。

【问题讨论】:

【参考方案1】:

您的调用堆栈中缺少什么?您是否有一堆地址无法解析为有效的函数名称(即 0x8732ae00 而不是 CFoo:Bar())?如果是这样,那么您需要将.PDB 放在调试器可以找到它们的位置,或者设置symbol server 并在“模块”窗格的右键单击上下文菜单中设置“符号路径”。

每次有人签入新的 Perforce 更改列表时,我们都会存储每个二进制文件中的每个 .PDB,这样当办公室内的任何人或零售店的任何客户返回转储时,我们就有与版本对应的 .PDB他们正在运行的游戏。设置好符号服务器和路径后,我只需双击 .mdmp 即可,每次都能正常工作。

或者您是否有一个似乎只有一个函数的调用堆栈?就像,0x8538cf00 在堆栈中没有其他任何东西?如果是这样,那么您的崩溃实际上是堆栈本身被损坏。如果回链中的返回地址被覆盖,调试器自然无法解析。

有时您还会发现实际发出 minidump 的线程并不是引发导致崩溃的异常的线程。查看“线程”窗口以查看其他线程之一中是否包含有问题的代码。

如果您正在调试“发布”版本——也就是说,编译时打开了所有优化标志——您将不得不接受调试器将无法找到局部变量和其他一些数据的事实.这是因为启用优化意味着允许编译器将数据保存在寄存器中、折叠计算,并且通常会执行各种阻止数据实际写入堆栈的操作。如果这是您的问题,那么您需要打开反汇编窗口并手动跟踪数据,或者重新构建调试二进制文件并在您可以查看的地方重现问题。

【讨论】:

0x8732ae00 是一个不太可能的地址,它在内核空间中(x86-32 的 2GB 设置)。 0x7_______ 地址更常见,因为 Windows DLL 紧靠 2GB 边界。这减少了所需的重定位次数。如果您没有看到它们的符号,请使用 Microsoft 符号服务器。 例如,我只是随机提取地址(在这种情况下,这是特定机顶控制台喜欢重新定位用户模式 ​​DLL 的地方)。【参考方案2】:

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

我遇到了与您类似的行为:有时有有效的调用堆栈,有时没有。在少数情况下,堆栈可能真的被破坏了。在所有情况下,可能有 1/3 根本不调用已安装的异常处理程序!我猜它在某种程度上是 Windows 结构化异常处理的问题。

【讨论】:

【参考方案3】:

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

【讨论】:

这是个好主意。但是,使用 PDB 和原始 DLL,MSVC 的调试器无论如何都可以使用 FPO 计算堆栈帧,但当然它的工作变得更加困难。我知道这一点,因为我们使用 FPO 进行编译,而且我总是从小型转储中获取堆栈。 如果“崩溃”是由于手动 INT 3 断点造成的,那当然很容易。问题是,大多数崩溃并不是完全按照错误的指令发生的。 CPU 会绊倒一段时间,直到触发故障。同时,执行的代码没有按预期工作,可能会严重破坏程序状态。这可能包括执行您不打算执行的指令(特别讨厌:通过误解的 vtable 进行间接跳转)。 vtable课程 是的,没有帧指针肯定会使手动回溯堆栈的任务变得更加困难。即使程序因跳过一个狂野的 vfunc 指针而死,您通常也可以弄清楚它来自哪里,因为 CALL 操作会将 IP 压入堆栈,但是找到它然后计算出所有本地人都去了哪里可能会成为一项艰巨的练习一次向后工作一个操作。如果你发现自己在这条小溪上,windbg 有一个有用的dps 命令,它可以在内存中搜索可能已知的符号和函数地址;这可以帮助您寻找旧的 EIP。【参考方案4】:

要提高转储中调用堆栈的准确性,您可以做的一件事是使用除 Visual Studio 之外的调试器 - 具体来说,使用 WinDbg 或其他使用 dbgeng.dll 中的“Windows 调试器”调试引擎的工具(相对于 Visual Studio 使用的“Visual Studio Debugger”调试引擎)。

根据我们的经验,WinDbg 可以 100% 可靠地从 Visual Studio 生成不可用或非常不准确的调用堆栈的相同转储中生成良好的调用堆栈。据我所知,如果未处理的异常是崩溃的根源,WinDbg 会自动执行tricky process of reconstructing/recovering the exception callstack,但 Visual Studio 不会(或不能?)。两个调试器使用different heuristics for interpreting stacks

WinDbg 一开始可能会让人望而生畏,所以这里是我的快速指南,说明如何使它更容易甚至避免直接使用它。

提取良好调用堆栈的凡人指南

这些排序从“最快/最容易”到“最慢/最难解释”。

    最简单的选择:使用DbgDiag from Microsoft

这是一个鲜为人知的工具,可以自动分析大量常见问题,并且非常简单,可以提供给非程序员甚至客户。它快速且几乎万无一失,并且已成为我快速分析传入故障转储的“首选”工具。

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

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

DebugDiag 甚至可以自动执行一些在 WinDbg 中可能但很痛苦的更复杂的分析(例如跟踪应用程序中的 350 个线程中的哪个线程导致了死锁)。

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

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

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

在 Visual Studio 中打开故障转储 在调用堆栈窗口中单击鼠标右键并选择“转到反汇编” 将 WinDbg 输出调用堆栈顶行的十六进制地址粘贴到反汇编窗口的“地址”栏中,然后按 Enter。您现在位于崩溃的位置,正在查看反汇编代码。 在反汇编窗口中单击鼠标右键并选择“转到源代码”以转到该位置的源代码。现在您正在查看崩溃站点的源代码。

注意:以上所有都需要配置正确的符号服务器路径,否则您将无法解析调用堆栈中的符号。我建议设置 _NT_SYMBOL_PATH environment variable 以便 Visual Studio、WinDbg 和 DebugDiag 自动使用它。

【讨论】:

另一个注意事项:为了获得更多更愉快的发布模式崩溃转储调试体验,有一个特殊的编译器标志可以将额外的信息注入你的 PDB,允许你正确单步执行优化代码并查看调用堆栈中的内联函数(以及分析器跟踪!)。这在 VS2010 中作为未记录的标志“/d2Zi+”提供,然后在 VS2013 Update 3 中更改为官方标志“/Zo”。有关更多信息,请参阅randomascii.wordpress.com/2013/09/11/…【参考方案5】:

记录小型转储的代码不太可能相关。 minidump 记录的主要内容是模块信息(用于获取符号)和所有线程堆栈的全部内容。除了那些基本信息(总是被记录下来的)之外,其他的都不重要了。

获得好的符号(包括 PE 文件)对于堆栈遍历至关重要。更多细节可以在这里找到:https://randomascii.wordpress.com/2013/03/09/symbols-the-microsoft-way/

我发现 Visual Studio 在显示调用堆栈方面通常很可靠。它会自动显示异常记录中的相关调用堆栈,并且使更改线程变得容易,以便您可以看到所有线程的调用堆栈。它有时确实会尝试“隐藏”它认为可能会让您感到困惑的细节——这是好是坏取决于您的技能水平。

Windbg 默认显示记录崩溃转储的代码的调用堆栈,而不是崩溃的调用堆栈。 Windbg 要求您使用“.ecxr”或“!analyze -v”来查看崩溃堆栈。我觉得这很烦人。 Windbg 还需要更多配置才能发挥作用。

这两个调试器确实有不同的堆栈遍历启发式。例如,如果您调用或返回地址零,则需要这些启发式方法,因为该地址没有展开信息。对于失败指令在正常代码中的“干净”崩溃,这些启发式方法不太重要。

在过去的十年中,几乎可以肯定堆栈遍历有所改进。 VS 2015 社区版功能强大,免费,不妨一试。

如果你使用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 并且仍然遇到问题,那么堆栈遍历失败很可能是您所看到的崩溃所特有的。如果缓冲区溢出在崩溃之前超出堆栈,则调用堆栈将被不可挽回地损坏。您的问题关于您所看到的故障的信息太少,无法给出明确的诊断。我发现这两个调试器的堆栈显示相当可靠,但我通常也能理解为什么它们有时会失败,并且当这种情况发生时,我仍然可以提取我需要的信息。

【讨论】:

以上是关于为啥 Minidumps 不能提供好的调用堆栈?的主要内容,如果未能解决你的问题,请参考以下文章

为啥分配堆内存比分配堆栈内存快得多?

为啥在调用它的析构函数后我可以访问这个堆栈分配的对象? [复制]

计算机内存为啥要有堆栈区?

为啥.net(仍然)需要拳击?

操作系统参与堆栈操作

为啥函数执行后没有释放堆上的元素?