65.9K
CodeProject 正在变化。 阅读更多。
Home

第一部分:Windows 调试技术 - 应用程序崩溃调试 (Windbg)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (45投票s)

2014 年 1 月 7 日

CPOL

9分钟阅读

viewsIcon

146864

这是第一部分,涵盖了在 Windows 上调试应用程序的各种技术,重点关注应用程序崩溃。

引言

本文以简单易懂的方式讲解了 Windows 应用程序崩溃的调试方法。本文的范围仅限于用户模式调试。本文介绍了使用 WinDbg、procdump 进行的非常基础的调试。

注意:这是一个系列文章,共分为 5 部分。

先决条件

要完成下面文章中解释的实践练习,需要以下工具:

  1. Procdump
  2. Debugging Tools for Windows (Windows 调试工具)

背景

在使用或处理 Windows 应用程序时,我们都曾遇到过应用程序因未知原因停止工作的现象。一个我们都见过的通用对话框,大致是这样的。

当出现这种情况时,我们通常会选择“关闭程序”选项,然后尝试重新启动应用程序。如果同样的情况反复出现,并且这是一个第三方应用程序,那么我们会报告该问题并等待解决方案。

现在,让我们换个角度,站在负责分析此问题并尽快给出解决方案的团队的角度来看。因为这可能会导致客户站点停产。让我们详细了解一下,一步一步地找出应用程序究竟为什么崩溃,发生了什么,以及我们如何解决这个问题。

定义

应用程序崩溃是指程序正常运行意外中断的情况。让我们以以下示例源代码为例:

int main()
{
	int *p = NULL;
	cout<<"This is Start";
	*p = 10;
	cout<<"This is End";
	return 0;
}

当我们执行这个示例时,我们会看到上面显示的与应用程序崩溃相关的对话框。导致此应用程序崩溃的原因是什么?是“*p=10”,“对未分配的指针赋值”,或者换句话说,“对 NULL 指针赋值”。我们可以这么说,因为我们有源代码,而且它足够小,可以找出问题的根源。在数百万行代码中识别出这个问题并不容易,修复它则更加困难。因此,这得出的结论是,我们需要一种技术,能够让我们在不逐行查看整个代码的情况下,找到问题的精确根源(或至少是接近根源)。

调试技术

有很多不同的技术用于识别应用程序崩溃的原因,但不同技术之间有一些共同点。

步骤 1:识别故障模块

可以通过事件查看器来识别故障模块。以我们当前的例子,即AppCrash.exe为例,一旦它崩溃,就会在事件查看器中生成一个事件。转到“运行”,输入“eventvwr”。

查看“常规”选项卡中的文本,其中有两个有趣的点:

  1. 故障应用程序名称 (Faulting Application Name):指出有问题的应用程序。在本例中,它是AppCrash.exe
  2. 故障模块名称 (Faulting Module Name):指出应用程序或可执行文件中哪个模块行为异常。在本例中(同样),它是AppCrash.exe

这清楚地表明问题出在AppCrash.exe。如果故障模块是,例如,AppCrashLib.DLL",那么它就是罪魁祸首,我们则需要调试它。

另一个重要的点是异常代码 (Exception Code),它解释了此错误的确切含义。在当前情况下,异常代码是 0xC0000005,表示访问冲突 (Access Violation),意味着应用程序正在尝试访问无效的内存位置。要获取所有异常代码的列表,请参阅以下链接:

这对于锁定问题非常有帮助。

步骤 2:捕获崩溃转储 (Crash Dump)

崩溃转储基本上包含了异常终止的程序的当前工作状态。崩溃转储还可以提供内存(即 RAM)的完整状态,可用于分析问题。捕获崩溃转储最简单的方法是使用“procdump”。应在应用程序崩溃之前配置 procdumpprocdump -ma -x c:\dumps "E:\Study\Windows Internals\Training\Sample Code\AppCrash\x64\Release\AppCrash.exe"。这是 procdump 最基本的用法示例,可以探索更多选项。使用此选项,它将启动进程,并在应用程序崩溃时捕获完整的内存转储并将其保存到 c:\dumps

步骤 3:分析崩溃转储

现在我们有了转储文件,需要对其进行分析。分析转储文件的最佳方法是使用“Windbg”。(在撰写本文时)WinDbg 是 Windows 上所有可用调试工具的“祖父”。我们不会深入探讨 Windbg 的复杂性,这超出了本文的范围。我们将只关注如何使用 Windbg 分析转储文件。要开始分析转储文件,我们需要与已崩溃的可执行文件版本相对应的 PDB 文件。PDB(Program Database)是程序数据库,它包含调试应用程序所需的所有调试信息。唯一的限制是 PDB 文件和可执行文件必须具有相同的时戳,否则程序数据库符号将不匹配,因此我们无法分析转储文件。

在下一步中,我们将启动 Windbg 并配置 PDB 文件,如下所示:

  1. Windbg 中,转到 文件 (File) -> 打开崩溃转储 (Open Crash Dump),选择转储文件,然后单击打开。

  2. 转储文件成功加载后,将显示以下屏幕:

  3. 只需转到命令窗口并输入“!analyze -v”,如下所示:

  4. 输入上述命令后,我们将得到以下输出:

现在,我们需要关注不同的参数来识别问题。如果我们查看调用堆栈 (stack trace),它会显示崩溃发生在 Appcrash.exe 中,在 main 函数的偏移量 0x39 处。这并没有给出可能导致问题的确切故障源代码。

让我们看看下面的语句,**AppCrash!main+39 [e:\study\windows internals\training\sample code\appcrash\appcrash\source.cpp @ 9]**。这提供了崩溃发生的精确位置,下面的行提供了更多详细信息。

FAULTING_SOURCE_CODE:  
     5: {
     6: 	int *p = NULL;
     7: 	cout<<"This is Start";
     8: 	*p = 10;
>    9: 	cout<<"This is End";
    10: 	return 0;
    11: }

在上面的分析中,崩溃实际上发生在第 8 行,但 windbg 指向第 9 行。这是由于编译期间启用了优化。因此,如果我想识别出问题的确切行,那就是第 8 行。由于 NULL 指针被赋值,我试图写入一个不存在的位置。

步骤 4:修复问题并发布

既然我们知道问题所在,现在就可以为指针分配内存,然后进行赋值。因此,新代码将是:

int main()
{
	int *p = NULL;
	cout<<"This is Start";
	p = new(std::nothrow)int;
	if(p == NULL)
	{
		return false;
	}
	*p = 10;
	cout<<"This is End";
	return 0;
}

优化

我们讨论过,由于设置了优化,我们无法获得崩溃发生的精确点。让我们进一步讨论优化。

优化意味着我们要求编译器进行何种程度的优化。当我们提高级别时,例如“完全优化 (Full Optimization)”,意味着二进制文件的大小会更小,PDB 文件中包含的调试信息也会更少。当我们降低级别时,例如“禁用优化 (Disable Optimization)”,我们会获得更多的调试信息,二进制文件和 PDB 文件的大小也会更大。同样,如果我们以调试模式 (debug mode) 构建二进制文件,我们会获得更多的调试信息,二进制文件的大小也会更大。

我们看到,总共有四个可配置的选项。通常,在大多数项目中选择的选项是“最大化速度 (Maximize Speed)”,这足以调试客户报告的崩溃。在上述示例中,如果我们禁用优化,则会得到以下结果:

所以,在这里,我们看到它精确地指向了问题所在的位置,即 *p=10。这发生是因为调试信息足以识别问题的根源。因此,作为经验法则,在进行发布时,我们应该维护 PDB 文件,以便它们可以用于分析客户站点的崩溃转储。

如果问题可以在本地重现,那么建议禁用优化,然后重新构建 EXE,收集最新的转储文件并对其进行分析,这样会使工作更轻松。不建议使用调试模式 (Debug mode),因为在调试模式下会出现很多在发布模式下不会出现的问题。

PDB 文件

对于任何正在构建的非托管代码,都会在生成 EXE 文件时创建 PDB 文件。这些 PDB 文件包含调试任何问题所需的调试信息。换句话说,此文件也称为**符号文件 (Symbol file)**。符号文件包含对调试有用的各种符号。举几个例子:局部变量、全局变量、函数名、源文件行号等。这些信息中的每一个都被称为符号。有 2 种类型的符号可用:

  1. 私有符号 (Private Symbols):这包括函数、局部变量、全局变量、用户定义数据结构、源文件行号。
  2. 公共符号 (Public Symbols):函数、全局变量。

与私有符号相比,公共符号包含的信息相对少得多。公共符号只包含可以在不同文件中查看的信息。因此,这意味着局部变量不会作为公共符号的一部分提供。甚至公共符号中的大多数函数都会有修饰的名称。

使用私有符号调试甚至可以给出问题所在的确切行号(如上例所示),但这在公共符号的情况下是行不通的。

大多数公司维护两个符号服务器,一个私有的用于内部使用,一个公共的用于外部分发。

默认情况下,Visual Studio 生成私有符号。要将其设为公共,请在链接器部分添加 /pdbstripped 标志。有关更多详细信息,请参阅此链接

总结

这是一种非常简单直接的调试问题的方法。通常,会有比这更复杂的方法。这些复杂情况包括存在多个模块和多个线程,以及需要仔细分析的误导性调用堆栈。我们只介绍了一个非常基本的情景,还有很多内容需要探索。

请继续阅读第二部分:https://codeproject.org.cn/Articles/708098/Part-2-Windows-Debugging-Techniques-Debugging-Appl 了解其他技术(DebugDiag, AppVerifer)。

历史

  • 2014-01-07:文章上传
  • 2014-01-20:已更新到其他部分的链接
  • 2014-01-22:已更新异常代码的说明
  • 2014-02-25:已更新 PDB 文件信息
© . All rights reserved.