软件工程师必须了解如何分析进程转储,以便确定应用程序崩溃或行为异常的原因。但是,很难知道从哪里开始。这篇文章旨在为一种非常常见的情况提供一个起点:调试在 Windows 上运行的 .NET 应用程序的崩溃转储。当 .NET Framework 记录错误“由于未处理的异常,进程已终止”时,很容易在 Windows 事件日志中检测到崩溃的应用程序。
这篇文章介绍了如何在崩溃时捕获失败应用程序的转储文件,然后分析转储以查明发生了什么。我们首先创建一个崩溃的“hello world”应用程序。我们回顾应用程序崩溃期间 Windows 中发生的情况,并介绍一些在应用程序失败时自动捕获转储文件的步骤。最后,我们回顾了启动崩溃转储分析的基本调试器命令。
编写 Hello World 崩溃程序
在本练习中,我们使用 Visual Studio IDE 编写和编译崩溃应用程序。您可以在此处下载免费社区版。首先创建一个新的 C# 控制台应用程序(.NET Framework),然后选择框架的最新版本,例如 4.5 或更高版本。将项目和解决方案命名为“HelloWorldCrasher”。
当新项目准备好编辑时,打开 Program.cs 文件并用以下代码替换全部内容:
using System;
namespace HelloWorldCrasher
{
class Program
{
static void Main(string[] args)
{
FirstMethod("Hello World! Lets crash!");
}
static void FirstMethod(string crashingText)
{
SecondMethod(crashingText);
}
static void SecondMethod(string crashingText)
{
throw new InvalidOperationException(crashingText);
}
}
}
此代码会抛出程序未处理的异常,从而导致程序崩溃。保存文件并构建解决方案(F6 键)。在程序的输出文件夹 (bin\debug) 中,您应该会找到已编译的应用程序 (*.exe) 和符号文件 (*.pdb)。
导航到构建输出目录并运行 *.exe 文件。它应该运行,在控制台窗口中显示堆栈跟踪,然后在几秒钟后退出。下一节将讨论决定 Windows 如何应对崩溃的因素。
Windows 中的用户模式异常处理程序
当发生用户模式异常时,Windows 会运行有序的异常处理程序列表(如下所述)来确定要做什么。如果以下任何问题的答案为否,则它将继续执行下一个处理程序。以下是它的执行方式:
- 是否有用户模式调试器程序(如 Visual Studio)附加到出现错误的进程?
- 如果是,则进入该进程进行实时调试。
- 执行代码是否有自己的异常处理程序?
- 如果是,则让应用程序处理该错误。
- 是否附加了内核调试器(例如 WinDbg)并且具有断点中断?
- 如果是,Windows 将尝试联系内核调试器。
- Windows 注册表中是否定义了事后调试器?
- 如果是,则根据指定的程序和参数激活指定的调试器来创建转储文件或附加到实时进程。
- 如果以上步骤均不适用:
- Windows 错误报告 (WER)接管。
- WER 处理崩溃,如果有解决方案,可能会显示错误消息。
- 如果配置了此项设置,则可以将进程转储文件写入磁盘(默认关闭)。
有关完整详细信息,请参阅此处的MSDN 参考文档。
那么在大多数 PC 上会发生什么情况?对于默认安装,您不会配置连接的实时调试器程序或事后调试器。这意味着 WER 会处理异常并写入崩溃事件。
但是,WER 的默认配置是不在磁盘上保存转储文件。在这种情况下,您应该会在 Windows 事件日志的应用程序日志中看到一些与崩溃相关的 WER 事件,但在 WER 存储崩溃数据的文件夹中看不到内存转储文件 (*.dmp)。
不存储转储文件是默认设置,原因有很多,包括安全性、隐私性和磁盘空间。这是因为完整的转储文件包含崩溃时进程的整个内存空间。如果该进程内存大小为几 GB 怎么办?如果它包含密码或个人身份信息 (PII) 等敏感数据怎么办?
但是有时我们确实需要捕获完整的内存转储,以便排除应用程序崩溃的原因。记录到事件日志中的堆栈跟踪可能并不总是足够的,我们希望更深入地探索以找出崩溃时进程中发生的事情,或者使用专门的工具对其进行分析。完整转储允许您探索您开发或支持的程序崩溃的根本原因。只需记住在不再需要时将其关闭即可。
如何自动捕获崩溃进程的转储文件?
因为默认情况下不会创建转储文件,所以我们需要配置注册表设置来启用此功能。您有两种启用捕获的选择。第一种是配置上一节第 4 步中提到的事后调试器设置。MSDN 上有一篇关于如何执行此操作的很棒的文章 。当您使用此选项时,您可以指定一个调试器程序(例如 Visual Studio、ProcDump、WinDbg 或 ADPlus),并为这些程序提供命令开关以创建转储文件,或者如果它们支持,则附加到进程进行实时调试。
第二种选择是配置 WER,以便在特定应用程序崩溃时为其创建转储文件。如果您不想进行实时调试,而只需要在磁盘上创建转储文件以供以后分析,则可以采用这种方式。此功能包含在 Windows 中,因此无需安装额外的软件来创建转储。WER 转储设置通过注册表配置如下:
- 首先创建基本 WER 本地转储注册表项(如果尚不存在)。
- HKLM:\SOFTWARE\Microsoft\Windows\Windows 错误报告\LocalDumps
- 然后,创建特定于应用程序的转储文件设置键。
- 如果要保存转储的应用程序是 HelloWorldCrasher.exe,则执行以下操作:
- 新密钥:HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\HelloWorldCrasher.exe
- 该项下的注册表值:
- DumpFolder:[REG_EXPAND_SZ] 存储转储文件的文件夹路径。
- DumpType:[REG_DWORD] 指定值2表示完整转储,或指定1表示小型转储(较小,但不是完整的进程内存空间)。
- DumpCount:[REG_DWORD] 在覆盖旧转储文件之前要保存的转储数量。默认值为 10。
- 该项下的注册表值:
它看起来应该是这样的:
在注册表中为我们的 hello world 崩溃应用程序配置了这些设置后,继续重新运行可执行文件。这次当它崩溃时,我们应该会在我们指定的文件夹中看到在磁盘上创建的转储文件。
转储分析步骤 1:安装调试工具
我们将使用 WinDbg 程序来分析此转储文件。它是一款免费工具,随 Windows 驱动程序工具包 (WDK) 或 Windows 软件开发工具包 (SDK) 一起提供。如果您尚未安装它并且只需要 WinDbg,您可以下载其中一个安装程序并取消选中除“Windows 调试工具”之外的所有功能。
例如,我从 Windows 10 SDK 安装它之后,包括 WinDBG 在内的调试工具位于以下目录下:
C:\Program Files (x86)\Windows Kits\10\Debuggers\x64
C:\Program Files (x86)\Windows Kits\10\Debuggers\x86
转储分析步骤2:打开WinDbg和转储文件
打开与崩溃应用程序的平台目标匹配的 WinDbg 版本(x86 或 x64)。例如,如果您的应用程序是 64 位的,请运行 64 位版本的 WinDbg。我们的崩溃应用程序是使用默认的 anycpu/x86 平台目标构建的,因此请打开 x86 WinDbg,除非您将项目修改为 x64 目标。
从文件菜单中,选择“打开崩溃转储”(Ctrl+D),然后选择之前创建的转储文件。最大化程序内部的转储窗口后,它应该看起来像这样:
转储分析步骤 3:加载应用程序符号
下一步是加载应用程序 符号。这很重要,因为在调试会话中加载符号有助于恢复在构建时已从中删除信息的应用程序二进制文件的上下文。这包括行号、变量、函数名称等。
让我们在 WinDbg 底部的命令框中输入接下来的几个命令来设置我们的符号缓存和加载首选项。
启用详细符号日志记录:
!sym noisy
将符号搜索路径设置为 Microsoft 的公共符号服务器(用于 Microsoft 拥有的二进制文件)和我们的应用程序编译输出文件夹(我们的 .pdb 符号文件所在的位置)。还设置本地缓存文件夹以存储下载的符号。将缓存文件夹和应用程序私有文件夹调整为您计算机上的确切文件夹。如果路径不完全匹配,符号将不会加载。
.sympath srv*https://msdl.microsoft.com/download/symbols
.sympath+ cache*C:\debug\symbols
.sympath+ D:\Source\HelloWorldCrasher\HelloWorldCrasher\bin\Debug
强制重新加载此转储的符号。
.reload
ld*
如果一切正常,我们应该看到调试器正确地为我们的应用程序加载了私有符号:
转储分析步骤 4:加载 SOS 扩展
SOS是一个调试工具扩展,它通过提供有关 .NET 命令语言运行时 (CLR) 环境的详细信息,使调试托管代码(.NET 应用程序)更加容易。它会在您安装 .NET Framework 组件时安装。这里的问题在于,您需要与应用程序中使用的框架版本相匹配的正确 SOS 版本,并且为旧框架版本加载它的过程略有不同。对于 .NET 4 及更高版本(如我们的示例应用程序),只需运行以下命令:
.loadby sos clr
如果成功加载,您将看不到任何输出。但是,如果您想验证它是否确实加载,请运行以下命令来打印已加载的扩展:
.chain
您应该会看到如下图所示的 SOS:
转储分析步骤 5:运行调试命令
最后,我们可以对崩溃转储做一些有趣的事情了。最好的开始方式是使用 -v 开关运行 !analyze 扩展。这将检查转储并提供大量立即有用的输出。
!analyze -v
在这个简单的崩溃情况下,我们获得了错误消息的数据和带有行号的堆栈跟踪。代码中的“Hello World!让我们崩溃!”消息直接打印在屏幕上供我们查看:
让我们回顾一些其他命令,以便进一步探究。首先是 !threads,它用于打印崩溃时正在运行的线程列表。在我们的例子中,只有两个线程。您可以看到两个线程中的哪一个发生了崩溃。
!threads
请注意,在线程列表的左侧,有一个线程 ID。在上面的例子中,第一个线程为 0,第二个线程为 5。
我们可以通过将上下文切换到该线程号(“~”字符,加上线程 ID,加上“s”字符)并运行 !clrstack 来找到特定线程的堆栈跟踪。我们碰巧已经处于这个线程上下文中,但我在这里使用命令来演示如何在需要时进行切换。
不要忘记将 -a 参数传递给 !clrstack。此参数会打印参数和局部变量!
~0s
!clrstack -a
请注意,实际的参数名称 (crashingText) 已提供,您可以看到它旁边有一个内存地址。该地址(蓝色)是提供给该参数的值的内存地址。您可以直接单击该链接,它将针对该特定内存地址运行 !dumpobj 命令。这对于遍历内存中的对象非常有用,并且比手动输入要转储的内存地址更容易。
当我们转储这个特定对象时,我们可以看到它是一个 System.String 对象,我们可以看到值和其他有用的信息。通过遍历堆栈和转储对象,我们可以很好地了解崩溃发生时应用程序的状态。
提示和常见问题
- Microsoft 符号服务器是否抛出了证书错误?请尝试在地址中使用 http 而不是 https。
- 尝试加载 SOS 时出现“ Win32 错误 0n193 %1 不是有效的 Win32 应用程序”错误是什么意思?这意味着您可能加载了错误的 WinDbg 平台目标(与应用程序的平台目标不匹配,无论是 x86 还是 x64)。
- 为什么 Visual Studio 会提示实时调试应用程序崩溃?当您将 Visual Studio 设置为事后调试器并启用该调试器时,就会发生这种情况。
- 如何学习更多要运行的 WinDbg 命令?阅读这些有关WinDbg 命令和SOS 命令的有用资源。
摘要和入门套件
我们已经完成了创建崩溃进程、捕获崩溃转储以及对转储文件进行基本分析的整个过程。让我们做最后的回顾,并将我们使用的关键命令组合到崩溃转储调试入门工具包中以供将来参考:
准备符号并加载SOS:
# 设置符号设置和路径
!sym noisy
.sympath srv*https://msdl.microsoft.com/download/symbols
.sympath+ cache*C:\Debug\SymbolCache
.sympath+ D:\Source\MyApplication\MyApplication\bin\Debug
# 重新加载符号
.reload
ld*
# 加载 SOS 扩展
.loadby sos clr
关键调试命令:
#查看异常分析,查看线程状态
!analyze -v
!threads
# 线程切换,查看堆栈,转储对象
~0s
!clrstack -a
!dumpobj /d <address>
这实际上只是对转储分析的简单介绍,但它应该可以让您初步了解大多数 .NET 应用程序崩溃的情况。祝您好运,调试愉快!