wenmo8 发布的文章

简介

编写整洁的应用程序是一回事。但是当用户告诉你你的软件已经崩溃时,你知道在添加其他功能之前最好先解决这个问题。如果你够幸运的话,用户会有一个崩溃地址。这将大大有助于解决这个问题。但是你怎么能用这个崩溃地址来判断出了什么问题呢?

创建Map文件

首先,你需要一个Map文件。如果没有,使用崩溃地址几乎不可能找到应用程序崩溃的位置。首先,我将向您展示如何创建一个好的Map文件。为此,我将创建一个新项目(MAPFILE)。你也可以这样做,或者调整你自己的项目。我在VC++ 6.0中使用Win32应用程序选项创建一个新项目,选择“典型的”Hello World!应用程序'。

 而vs2013里如下,从vs2005开始不在支持/MAPINFO:LINES

 

 

现在,您可以编译并链接您的项目了。链接后,您将在中间目录中找到一个.map文件(连同您的EXE文件)。

读取Map文件

我们将使用一个崩溃示例来实现这一点。所以首先:如何崩溃你的应用程序。我在InitInstance()函数的末尾添加了这两行:

char* pEmpty = NULL;
*pEmpty = 'x'; //This is line 119

我相信你能找到其他会使你的申请失败的指令。现在重新编译并链接。如果启动应用程序,它将崩溃,您将收到如下消息:“0x004011a1处的指令”引用了“0x00000000”处的内存。无法“写入”内存。

现在,是时候用记事本或类似的东西打开Map文件了。您的Map文件将如下所示:Map文件的顶部包含模块名、指示项目链接的时间戳和首选加载地址(除非使用DLL,否则可能是0x00400000)。头之后是显示链接器从各种OBJ和LIB文件中引入的节的节信息。

MAPFILE

 Timestamp is 3df6394d (Tue Dec 10 19:58:21 2002)

 Preferred load address is 00400000

 Start         Length     Name                   Class
 0001:00000000 000038feH .text                   CODE
 0002:00000000 000000f4H .idata$5                DATA
 0002:000000f8 00000394H .rdata                  DATA
 0002:0000048c 00000028H .idata$2                DATA
 0002:000004b4 00000014H .idata$3                DATA
 0002:000004c8 000000f4H .idata$4                DATA
 0002:000005bc 0000040aH .idata$6                DATA
 0002:000009c6 00000000H .edata                  DATA
 0003:00000000 00000004H .CRT$XCA                DATA
 0003:00000004 00000004H .CRT$XCZ                DATA
 0003:00000008 00000004H .CRT$XIA                DATA
 0003:0000000c 00000004H .CRT$XIC                DATA
 0003:00000010 00000004H .CRT$XIZ                DATA
 0003:00000014 00000004H .CRT$XPA                DATA
 0003:00000018 00000004H .CRT$XPZ                DATA
 0003:0000001c 00000004H .CRT$XTA                DATA
 0003:00000020 00000004H .CRT$XTZ                DATA
 0003:00000030 00002490H .data                   DATA
 0003:000024c0 000005fcH .bss                    DATA
 0004:00000000 00000250H .rsrc$01                DATA
 0004:00000250 00000720H .rsrc$02                DATA

在节信息之后,您将获得public function信息。注意“public”部分。如果您有静态声明的C函数,它们不会显示在映射文件中。幸运的是,行号仍将反映静态函数。public function信息的重要组成部分是函数名和Rva+Base列中的信息,Rva+Base列是函数的起始地址。

Address         Publics by Value              Rva+Base     Lib:Object

0001:00000000       _WinMain@16                00401000 f   MAPFILE.obj
0001:000000c0       ?MyRegisterClass@@YAGPAUHINSTANCE__@@@Z 004010c0 f   MAPFILE.obj
0001:00000150       ?InitInstance@@YAHPAUHINSTANCE__@@H@Z 00401150 f   MAPFILE.obj
0001:000001b0       ?WndProc@@YGJPAUHWND__@@IIJ@Z 004011b0 f   MAPFILE.obj
0001:00000310       ?About@@YGJPAUHWND__@@IIJ@Z 00401310 f   MAPFILE.obj
0001:00000350       _WinMainCRTStartup         00401350 f   LIBC:wincrt0.obj
0001:00000446       __amsg_exit                00401446 f   LIBC:wincrt0.obj
0001:0000048f       __cinit                    0040148f f   LIBC:crt0dat.obj
0001:000004bc       _exit                      004014bc f   LIBC:crt0dat.obj
0001:000004cd       __exit                     004014cd f   LIBC:crt0dat.obj
0001:00000591       __XcptFilter               00401591 f   LIBC:winxfltr.obj
0001:00000715       __wincmdln                 00401715 f   LIBC:wincmdln.obj
//SNIPPED FOR BETTER READING
0003:00002ab4       __FPinit                   00408ab4     <common>
0003:00002ab8       __acmdln                   00408ab8     <common>

entry point at        0001:00000350

Static symbols

0001:000035d0       LeadUp1                    004045d0 f   LIBC:memmove.obj
0001:000035fc       LeadUp2                    004045fc f   LIBC:memmove.obj
 //SNIPPED FOR BETTER READING
0001:00000577       __initterm                 00401577 f   LIBC:crt0dat.obj
0001:0000046b       _fast_error_exit           0040146b f   LIBC:wincrt0.obj

public function部分后面是行信息(如果您在链接选项卡中使用了/MAPINFO:LINES,在C/C++选项卡中选择了“行号”,Vs2005开始没有了)。之后,如果项目包含导出的函数,并且在“链接”选项卡中包含/MAPINFO:EXPORTS,则将获得导出信息。

Line numbers for .\Release\MAPFILE.obj(F:\MAPFILE\MAPFILE.cpp) segment .text

    24 0001:00000000    30 0001:00000004    31 0001:0000001b    32 0001:00000027
    35 0001:0000002d    53 0001:00000041    40 0001:00000047    43 0001:00000050
    45 0001:00000077    47 0001:00000088    48 0001:0000008f    52 0001:000000ad
    53 0001:000000b3    71 0001:000000c0    80 0001:000000c3    81 0001:000000c8
    82 0001:000000ff    86 0001:00000114    88 0001:00000135    89 0001:00000145
   102 0001:00000150   108 0001:00000155   110 0001:00000188   122 0001:0000018d
   115 0001:0000018e   116 0001:0000019a   119 0001:000001a1   121 0001:000001a8
   122 0001:000001ae   135 0001:000001b0   143 0001:000001cc   172 0001:000001ee
   175 0001:0000020d   149 0001:00000216   157 0001:0000022c   175 0001:00000248
   154 0001:00000251   174 0001:0000025f   175 0001:00000261   151 0001:0000026a
   174 0001:00000287   175 0001:00000289   161 0001:00000294   164 0001:000002a8
   165 0001:000002b6   166 0001:000002d8   174 0001:000002e7   175 0001:000002e9
   169 0001:000002f2   174 0001:000002fa   175 0001:000002fc   179 0001:00000310
   186 0001:0000031e   193 0001:0000032e   194 0001:00000330   188 0001:00000333
   183 0001:00000344   194 0001:00000349

现在我们来看看坠机地点。首先,我们将确定哪个函数包含崩溃地址。在“Rva+Base”列中搜索第一个地址大于崩溃地址的函数。映射文件中的前一项是导致崩溃的函数。在我们的示例中,崩溃地址是0x004011a1。这在0x00401150和0x004011b0之间,所以我们知道崩溃函数是?InitInstance@@YAHPAUHINSTANCE__@@H@Z 。以问号开头的任何函数名都是C++修饰的名称。要转换名称,请将其作为命令行参数传递给平台SDK程序UNDNAME.EXE(在bin dir中)。大多数时候,您不需要这样做,因为您可以通过查看它来了解它(这里是MAPFILE.obj中的InitInstance())。
这是追踪错误的一大步。但情况变得更好了:我们可以查出车祸发生在哪条线上!我们需要做一些基本的十六进制数学,所以没有计算器就做不到的人:现在是使用它的时候了。第一步是以下计算:crash_address-preferred_load_address-0x1000。
地址是从第一个代码段开始的偏移量,因此我们需要进行此计算。减去首选加载地址是合乎逻辑的,但为什么我们需要再减去一个0x1000?崩溃地址是代码段开头的偏移量,但二进制文件的第一部分不是代码段!二进制文件的第一部分是可移植可执行文件(PE),长度为0x1000字节。谜团解开了。在我们的示例中,这是:0x004011a1-0x00400000-0x1000=0x1a1
现在是时候查看Map文件的行信息部分了。这些行如下所示:30 0001:00000004。第一个数字是行号,第二个数字是该行所在代码段开头的偏移量。如果我们想查找行号,我们只需做与函数相同的事情:确定第一次出现的偏移量大于我们刚刚计算的偏移量。崩溃发生在前面的条目中。在我们的示例中:0x1a1在0x1a8之前。所以我们的崩溃发生在MAPFILE.CPP的119行。

如果我们是VS2005及以后的版本,只能借助其他手段来获取代码行了。

保留Map文件

每个版本都有自己的映射文件。在EXE发行版中包含映射文件并不是一个坏主意。这样,您就可以确定这个EXE的映射文件是正确的。你可以在你的系统中保留每个带有EXE的映射文件,但是我们都知道这可能会给以后带来一些麻烦。映射文件不包含任何您不希望用户看到的信息(除非可能是类和函数名?)。用户将无法使用它,但如果您自己没有副本,至少可以请求映射文件。

简介

开发人员面临的最大问题之一是生产问题何时发生。开发人员必须快速找到问题的根本原因,或者在开发环境中复制相同的问题来解决问题。由于时间是至关重要的,在这方面,迟迟得不到根本原因可能导致客户的金钱损失或导致公司的声誉损失。有效地解决生产问题和缩短周转时间的方法很少。

调试问题的方法

  • 检查调用堆栈的日志
  • 启用跟踪并检查跟踪输出
  • 在开发环境中复制场景并使用Visual Studio进行调试
  • 使用程序数据库(PDB)文件
  • 远程调试

检查调用堆栈的日志

这是调试问题的最常见方法,即获取用户日志文件并检查其中的异常。如果异常是已知的并用自定义消息处理,那么它是有用的,但是如果异常是未处理的,那么很难找出异常是如何发生的。调用堆栈在这种方法中很有用,但调用堆栈仍然提供方法级信息。方法级信息不显示是哪一行导致了问题。为了获得准确的行信息,我们需要使用程序数据库(PDB)文件和应用程序。PDB将在下面详细介绍。日志的另一个缺点是我们不能在应用程序中进行大量的日志记录,特别是如果日志是在输出文件中写入的。这会大大降低应用程序的性能。因此,应根据需要限制日志记录。

启用跟踪并检查跟踪输出

可以通过配置启用跟踪,并且可以重新运行应用程序以检查跟踪输出。如果trace被禁用,程序将忽略trace语句。因此,可以在代码中使用跟踪开关使用跟踪语句。但是这种方法仍然有局限性,因为它需要为每一个逻辑代码编写,而这些代码的维护有点繁琐。

在开发环境中复制场景并使用Visual Studio进行调试

由于通常的做法是只在客户机上部署程序集(编译的代码)和框架,因此很明显,客户机将没有用于调试的应用程序代码或Visual Studio IDE/Debugger。所以为了找到根本原因,开发人员需要用类似的场景设置开发环境,并在开发人员的机器上调试它。

这是找到问题根源的最可靠的方法,但它有严重的局限性。

  1. 开发环境和生产环境中的数据不能相同,因此与数据相关的问题很难重新创建。
  2. 这是一项耗费时间的任务,而且根据应用程序工作所涉及的多个系统,在开发过程中查看复制问题的关键性可能很难设置。

使用程序数据库(PDB)文件

这是在客户机上调试问题的最简单方法。但是什么是PDB文件?我们通常在编译应用程序时在编译文件夹中看到它们。程序可以在没有它们存在的情况下运行。因此,堆栈跟踪可以更加丰富。当我们将PDB文件与EXE/DLL一起包含,然后复制场景时,当前堆栈跟踪还将提供发生错误的行号。这有助于我们了解错误发生的确切位置,然后对其进行修正或提供修正。让我们举一个例子来说明PDB文件的使用。

PDB文件是特定于DLL/EXE的生成文件。它们包含链接到相应DLL/EXE的GUID。因此,即使代码没有更改,一个生成的PDB文件也不能与另一个生成的DLL/EXE一起使用。因此,一旦创建了一个内置发布模式,就需要将PDB存储在某个存储库(即“Symbol Server”)中。向PDB提供版本构建是可选的。但是提供它并没有太大的威胁,因为您的程序集和EXE已经包含了您的源代码。要反向工程您的可执行文件是可能的,可能类似的努力也可以放在PDB文件。因此,总的来说,除非您的安装包是特定于大小的,否则不会有太多问题,因为pdb文件可能会占用大量空间。

远程调试

PDB文件是开始调试的好方法,但如果您可以连接到客户端的计算机,则可以从计算机调试应用程序。远程调试很容易实现。您只需执行以下操作:

您需要在客户机上设置远程调试工具。这些工具可以在Microsoft网站上找到,例如https://www.Microsoft.com/en-us/download/details.aspx?id=48155。URL可能会更改,因此最好搜索“Visual Studio 20XX的远程工具”。

 

 

这些就是开发人员如何加快调试应用程序的方法。还有其他一些方法,如分析应用程序内存转储并找出崩溃或内存泄漏的原因。

这篇文章解释了如何使用驱动程序验证工具来分析崩溃转储文件。

使用Microsoft驱动程序验证工具

如果您曾经使用Windows的调试工具来分析崩溃转储,那么毫无疑问,您已经使用WinDbg打开了一个崩溃转储文件。WinDbg将对崩溃文件执行内部分析,并建议您从!analyze命令开始。该命令输出堆栈以及许多其他信息。执行此操作时,堆栈底部将显示转换为内核模式的线程,然后从那里开始,您向上遍历堆栈以查看是否存在罪魁祸首驱动程序。虽然这是一种可靠的调试技术,但有时崩溃转储或其中的一组将无法分析。内存中没有任何模式可以指出导致系统崩溃的原因,或者内存可能已损坏,因为崩溃转储文件实际上指向Ntsokrnl.exe或win32k.sys。
有一种方法可以通过使用Microsoft驱动程序验证工具将无法处理的崩溃转换为可分析的崩溃。此工具随Windows的每个版本一起提供,不是单独安装的。它不可见,因此不在“开始”菜单或“控制面板”中的“管理工具”中。通过在“开始”菜单的“运行”框中键入“verifier”来启动它,但是为了使用它,您应该知道它是如何工作的。本文将介绍如何使用此工具将无法分析的崩溃转换为可分析的崩溃。驱动程序验证工具包含许多选项,其中一些应该严格避免。
要启动驱动程序验证程序,请在“开始”菜单的“运行”框中键入“Verifier.exe”。第一个数据框显示列表选项。选择的选项是“创建自定义设置(用于代码开发人员)”。避免使用默认的“标准设置”选项。单击“下一步”后,我们选择“从完整列表中选择各个设置”。注意,我们没有选择任何默认设置。单击Next之后,我们会看到一个选项列表,范围从“特殊池”到“杂项检查”。实际上,除了“低资源模拟”之外,我们都选择了它们。低资源模拟正是这样一种设置,因此我们不希望重新启动并让任何设备驱动程序实际测试其行为,因为它的资源是模拟的,并且正在被有目的地耗尽。“特殊池”选项将在本文后面讨论。让我们首先检查“强制IRQL检查”。

假设一个驱动程序接触到一段分页内存。IRQL当前处于被动级别,这是所有用户模式代码运行的IRQL。但是,由于驱动程序已经接触到一块内存,内存管理器必须将该数据引入物理RAM并将其连接到该分页虚拟地址。现在,驱动程序执行一个操作,使IRQL上升到DISPATCH_LEVEL,并立即再次引用同一个分页缓冲区。要想让它显示为被操作系统捕获的bug,内存管理器需要在第一次引用、IRQL提升和第二次引用之间的少量指令发生的很短时间内,决定是否需要重用(或发送到分页文件)。这是极不可能的。“强制IRQL检查”所做的是,在启用该选项验证驱动程序时,它会导致IRQL移动到分派级别或更高级别。内存管理器获取连接到分页池虚拟内存的系统工作集的所有页,并断开这些页与物理内存的连接。

请注意,工作集是分配给进程的物理内存量,该量由内存管理器确定,因为操作系统已根据内存需求和分页速率监视该进程的行为。所以现在,如果这个驱动程序在DISPATCH_LEVEL再次访问这个缓冲区,它将生成一个页面错误,因为内存管理器将不得不去修复那个虚拟内存和那个物理内存之间的连接。此时内存管理器将检查当前的IRQL。它将看到它是DISPATCH_LEVEL或更高级别,然后确定这是一个非法操作,从而导致系统崩溃。这就是你想要的。您希望找到哪个驱动程序有足够的错误,以至于在执行某些非法操作时被抓到,这些操作将使系统在某些最终用户的计算机上或在某些不受控制的环境中崩溃。强制IRQL检查将揭示哪种驱动程序确实存在这些类型的错误,并因此暴露罪魁祸首驱动程序。

池跟踪选项对于驱动程序内存泄漏非常有用。I/O验证和增强的I/O验证使操作系统驱动程序验证程序代码对驱动程序传递到的和驱动程序传递回系统的数据结构执行一些严格的检查。这种数据结构称为中断请求包(IRP)。IRP的结构有一些特殊的规则。它必须指向有效的结构。它必须有一组一致的值。因此,驱动验证器将在驱动程序对其进行操作后检查该包,以确保其仍处于一致状态。

要创建一组步骤来将无法分析的崩溃转换为可分析的崩溃,我们必须认识到某些崩溃与某些条件相关,这些条件由这些选项描述。因此,在这些选项后单击Next,选择“从列表中选择驱动程序”。不要选择“自动选择系统上安装的所有驱动程序”。此时,菜单将加载驱动程序列表。在该列表下面是一个按钮,说明“将当前加载的驱动程序添加到列表中”。也许,你知道有一个驱动程序是有问题的,你可能想添加。拖放Provider部分以隔离那些不是来自Microsoft的驱动程序,并快速清点。在选择这些可疑的驱动程序、对这些驱动程序启用验证器并重新启动后,查看系统是否崩溃。如果系统没有崩溃,则采取另一个步骤,选择所有未签名的驱动程序和/或第三方设备驱动程序,并在这些驱动程序上运行验证程序。如果系统没有崩溃,那么作为最后手段,对每个驱动程序运行驱动程序验证程序。但是,不要一蹴而就。一次选择大约10或20个驱动程序,启用验证程序,然后重新启动。如果选择所有配置了这些选项的驱动程序,则系统可能需要20分钟才能重新启动。系统的行为可能在短时间内看起来有所不同,但最终会进入稳定状态(如果您将此作为练习,而不是尝试将无法分析的崩溃转化为可分析的崩溃)。

使用Notmyfault.exe测试驱动程序

目前受雇于微软的Mark Russonivich编写了一个名为“Notmyfault.exe”的测试驱动程序。此实用程序包含一个设备驱动程序myfault.sys,它将导致符合特定操作系统条件的特定类型的崩溃。在他编写的其他工具中,这个工具尤其是对于使用和理解系统崩溃以及如何避免它们是非常宝贵的。虽然不是必需的,但此工具最好在虚拟环境中运行。虚拟环境是作为计算环境运行的软件层。如果您下载了一个VMWare Workstation的试用版并安装了它,您将能够在正在运行的操作系统内(但与该操作系统分离)安装一个操作系统。试试看,安装一个旧版本的XP。

 

即使您不关心性能,但如果您的目标是所有驱动程序,那么还有另一个原因要追求成批的驱动程序,这就是特殊池选项。当我们调用NotmyFault.exe程序将控制请求发送到myfault.sys驱动程序以执行缓冲区溢出时,myfault.sys驱动程序将从内核内存分配一个缓冲区,然后写入缓冲区数组的末尾。这将损坏内存,如图所示:

 

注意,我们检查了缓冲区溢出选项。当我们按下“DoBug”按钮时,一些随机缓冲区将在内核内存中被覆盖。因此,被覆盖的内存已损坏。但是,仅仅损坏内存可能会导致系统崩溃,也可能不会,直到引用了损坏内存的内容。然后,系统就会崩溃。因此,在内存损坏和检测到损坏之间可能有很长的延迟。通常,另一个驱动程序或内核进行引用。MyFault.sys分配一个非分页的池缓冲区,并在末尾写入一个字符串,从而损坏池头和后面的数据结构。所以,我们按一次“Do bug”,什么也没发生。也许,按那个按钮十次,还是什么都没发生。不过,有一点是肯定的,我们现在有一个非常病态的内核内存。如果仍然没有崩溃,那么运行类似于Internet Explorer的程序,看看它是否引用了足以检测内存损坏的操作系统。如果这不起作用,那么运行一些更重的程序,更有可能导致系统崩溃,比如WindowsMessenger。
假设系统现在崩溃了。但当系统崩溃时,是不是Windows Messenger导致了系统崩溃?当然没有。但是,在一些内核模式的软件中发生了一些事情(不是Windows Messenger),这些事情被间接调用,导致系统崩溃。当内核检测到损坏的池时,蓝屏显示启用驱动程序验证程序。它告诉你发生了什么,但不是为什么。此时,我们将检查崩溃文件。崩溃文件显示了堆栈,但随后在跟踪堆栈上显示了一个Microsoft设备驱动程序,这可能是一个非常重要的驱动程序。但是,驱动程序只引用了损坏的内存。它实际上并没有损坏内存,这就是系统崩溃的原因。
现在,我们执行相同的测试,但是启用了驱动程序验证程序,启用了所有选项(特别是特殊池,但再次声明,不启用低资源模拟)。当您按下“DoBug”按钮时,驱动程序将尝试写入到其分配的末尾,但会在实际操作中被捕获。系统立即崩溃,但更重要的是,它直接指向myfault.sys。也就是说,我们当场抓到一名司机。触发的验证选项是特殊池选项。当使用特殊池验证选项集验证驱动程序时,Windows会尝试从内存的特殊区域满足对它的内存分配,因此命名为特殊池。此区域是特殊的,因为此区域中的每个其他页都是无效的内存页,因为它将驱动程序缓冲区与分配缓冲区的内存顶部对齐。所以,当司机从缓冲区的末端溜走时,它不会坐在其他缓冲区中,但最终会碰到这些无效的内存页中的一页。而且,仅仅是它接触到一个无效的内存页就触发了一个页面错误。页面错误处理程序查看引用的内容,发现从内核模式访问的内存页无效,并将立即使系统崩溃并告诉您驱动程序有问题:

 

但是,有两个条件,即使您在特定驱动程序或系统范围内启用了特殊池。发送到特殊池的分配必须略少于一页。也就是说,在x86系统上,分配的页大小必须小于4kb或4096字节。所以,当一个驱动程序进行大的内存分配时,它不会来自特殊的池。这意味着,如果它覆盖了缓冲区,它就没有myfault.sys演示中的保护和检查。特殊池是不需要重新启动的驱动程序验证选项之一,但请记住它是一个有限的资源。因此,当特殊池用完时,正在验证的驱动程序将把它们的池分配发送到普通池。换言之,它们将在没有上述保护的情况下进行验证。
另一个有用的测试是系统代码覆盖测试。当驱动程序中有一个错误损坏指针,并且指针指向操作系统内核或其他引导启动驱动程序的代码时,系统代码覆盖就会发生。大多数情况下,这种访问不会被检测到。在这些情况下,当它被检测到时,Windows将识别出一个驱动程序正在试图覆盖操作系统或另一个驱动程序的部分代码。要做到这一点,Windows必须有一个称为系统代码写入保护的工具。系统代码写入保护是内存管理器将操作系统和驱动程序的代码页标记为只读的机制。因此,如果驱动程序试图写入这些页面,则会触发页面错误,内存管理器会用停止代码来指示驱动程序试图修改代码,从而使系统崩溃。但是,由于性能原因,系统代码写入功能被关闭。也就是说,为了提高性能,内核在大多数系统上没有标记为只读。Windows为了节省CPU上作为缓存的translation look aside缓冲区的空间,将虚拟地址映射到物理地址,将操作系统代码和引导启动驱动程序映射到一个大的内存页中。在典型的x86机器上,标准或“小”页是4KB。但是,像驱动程序或内核这样的映像是在4kb的段中定义的,其中代码和数据在映像中。如果操作系统将包含代码页和数据页的整个映像加载到一个4 MB的大页中,则它别无选择,只能将该页上的内存保护设置为读/写。否则,内核和驱动程序将无法修改它们自己的数据(也映射到那个大页面)。因此,系统几乎总是设置为关闭系统代码覆盖。

简短的说明

一个错误的想法是,如果像记事本这样的可执行文件被启动,整个image就会被加载。实际上,只加载其中的一部分,这被称为“惰性分配器”。随着更多功能的使用,这个可执行文件的更多image将从磁盘中读取。必要的dll也不会全部加载。只加载这些dll的引用部分。这称为“虚拟分配”。Windows中任何可共享的内存都是共享的。这意味着代码和dll,但不是数据。一种误解是,如果加载两个记事本实例,则会有两个加载的记事本image。Windows意识到有一个映像的第二个实例已经将它的一部分加载到物理RAM中,并自动将两个虚拟映像连接到相同的底层页面。但是,在每个加载的记事本实例中键入的数据是对应实例的私有数据。因此,数据不是共享的,但是执行记事本的代码是共享的,dll也是共享的。
有一种方法可以在注册表中显式启用系统代码覆盖,但这不是必需的。当验证器打开时,即使使用最小的设置,也会启用系统代码覆盖。因此,当您打开verifier时,内核将用小页面映射自身和驱动程序,因此尝试写入代码将立即生成蓝屏。如果在Notmyfault.exe上选择“代码覆盖”单选按钮,myfault.sys将覆盖NtReadFile的前几个字节,NtReadFile是一个常用的系统函数。NtReadFile是代表从任何文件句柄读取的线程调用的底层系统调用。
因此,当我们按下“Do bug”时,代码将被覆盖(因为这是允许的,因为我们运行的是一个默认系统,它的内核和引导驱动程序映像映射到一个标记为读/写的大页面)。当代码被重写时,它将很容易被检测到,因为其他东西将调用NtReadFile函数,该函数将运行到某些被重写的指令中,从而导致崩溃。如果你看一下停止代码,就没有指向驱动程序的指针,因为导致这次崩溃的驱动程序早已不复存在。如果我们查看崩溃转储文件,我们可以很容易地找到一个关键系统组件驱动程序,如Win32k.sys,现在这是一个误诊。我们可以使用最高级的调试命令,并保留一个无法分析的崩溃文件。同样,解决方案是使用驱动程序验证程序。在驱动程序验证程序打开的情况下按“Do bug”表示系统代码覆盖已打开。当它崩溃时,蓝屏立即指向myfault.sys,并且进一步的文本说明试图写入只读内存。崩溃转储文件以前指向与崩溃无关的驱动程序,现在指向正确的驱动程

 

粗略地说,当Windows崩溃转储文件从调试器的分析引擎中不正确时,目标是将这些文件转换为可分析的文件。驱动验证器是帮助实现这一点的工具,以及提高系统性能的工具。

简介

不久前,我必须建立一个符号服务器,带有源索引,那时,关于这个主题的信息不多,所以我很难让所有的东西按照我们想要的方式工作。不幸的是,仍然只有这些相同的信息,但是自从微软将源代码发布到.NET框架并自动将其符号服务器添加到Visual Studio 2010以来,似乎有更多的人意识到了这一点。大多数使用.NET框架的人现在都会意识到,他们可以通过使用Microsoft的符号服务器从Microsoft获取符号文件和源代码来调试框架,但是有多少人真正了解什么是符号服务器,或者Visual Studio如何将源代码获取到.NET框架?答案出人意料地简单,与一些人的看法相反,他们认为这项技术已经存在了似乎是永远的,而且可以供你使用。因此,本文将向您介绍符号服务器的奇迹,也许更重要的是,源索引。

什么是符号服务器?

符号服务器基本上只是一个使用文件系统的非常简单的数据库,用于存储符号文件的不同版本。WinDbg和Visual Studio都可以通过SymSrv DLL使用这些数据库,SymSrv DLL随Windows调试工具包提供,用于为正在调试的应用程序加载匹配的符号。实际上,它不是一个数据库,而是一组结构方便的文件夹和文件,因此,您可以自由地复制和粘贴您的数据库到任何您喜欢的位置,但symbol服务器仍会记录事务,并保留已添加或从数据库中删除的所有内容的记录,因此您不能只是手动添加据我所知的档案。关于symbol服务器的一个重要注意事项是它不支持同时执行多个事务,没有锁定机制来阻止其他人在事务进行时更新数据库,因此您可能需要小心,一次只有一个人在更新数据库。
使用符号服务器意味着每个人都可以轻松访问最新版本的符号,如果需要调试较旧版本的应用程序或库,则无需担心自己会挖不出符号文件,它们将自动加载。
符号服务器本身是非常方便的,因为它允许您在调试时查看调用堆栈和其他有用的信息。但是举例来说,您正在调试一个客户机通过加载一个密钥库而经历的崩溃,并且您机器上的源代码不再匹配用于构建该应用程序特定版本的源代码,您可能很难准确地找到导致崩溃的原因;这是源索引出现的地方。

什么是源索引?

源索引是将命令嵌入到符号文件中的行为,当运行时,将从源代码管理系统中提取源代码的正确版本,或者从您可能已经准备好的其他备份中获取源代码。调试器可以在需要打开文件时运行这些命令,以便获得正确的源文件。因此,当您加载客户端在愤怒的电子邮件中发送给您的小型转储文件时,您可以在Visual Studio中加载它,您将看到用于生成该生成文件的确切源代码,以及(希望)指向有问题的代码行的大箭头所遇到的错误。

如何设置符号服务器

symbol服务器的先决条件是您有一些网络位置来存储数据库,即使您是唯一的用户,它也可以只是硬盘上的一个文件夹。下一步是告诉调试器符号服务器的位置,以便它在调试时检查符号文件。
在最新版本的Visual Studio中,您需要转到“工具”->“选项”->“调试”->“符号”,然后将路径添加到符号服务器。如果符号服务器位于某个网络上,则还应为Visual Studio指定一个本地缓存,以便在下次需要这些符号时将这些符号复制到该网络,而无需从该网络下载这些符号。

 

 

在旧版本的Visual Studio中,仅仅指定符号服务器的路径是不够的,还需要告诉它它是符号服务器。您可以将SRV*放在符号服务器路径之前。
SRV*只是symsrv*symsrv.dll*的简写,所以如果您看到完整版本,它的意思完全相同。SRV*语法有几个变体:

SRV*LocalCache*SymbolServerPath
SRV*LocalCache*NetworkCache*SymbolServerPath 

因此,可以为每个符号服务器指定不同的缓存位置。如果你是从某个非现场位置获取你的符号,你也可以指定一个网络缓存,这样当其他用户需要符号文件时,他们只需要直接从你自己的网络下载,而不是从地球的另一边下载。

如果使用WinDbg,则要添加符号服务器,需要转到“文件”->“符号文件路径”,然后使用上面的SRV*语法添加符号服务器。

 

 

您还可以为符号服务器设置一个环境变量,以便Visual Studio和WinDbg(以及任何其他兼容的调试器)都知道您的服务器,而不必在每个应用程序中显式地设置它们。需要创建的环境变量是符号路径,可以在中作为用户或系统变量创建。它对每台服务器使用SRV*语法,如果需要指定多台服务器,则需要用分号分隔每台服务器。

_NT_SYMBOL_PATH= SRV*c:\symbols*\\symbolserver;
SRV*c:\symbols*http://www.someotherplace.co.uk/symbols 

此时,您应该让调试器在服务器中查找符号,此时该符号可能为空。要向服务器添加符号,需要使用Windows调试工具提供的SymStore.exe。向服务器添加一组符号的基本命令是:

symstore add /f "c:\MyProject\Output\*.*" /s "\\MySymbolServer\Symbols" /t "MyProject"
/v "Build 1234" /c "Example Transaction"

关于每个命令的注释:

  • add告诉symstore我们正在添加文件。
  • /f对于我们要添加的文件(在本例中是一个或多个文件)的路径,如果您像我在这里所做的那样指定一个路径,则它将搜索要添加到服务器的任何兼容文件—这些文件包括Visual Studio生成的调试pdb文件以及二进制文件本身。请记住,如果要从小型转储进行调试,则可能还必须将二进制文件添加到符号服务器。我不知道如何指定要从中添加的多个路径,因此如果您只需要.PDB和.DLL文件(但不需要可执行文件),则必须对每个文件运行单独的命令,或者将要备份的所有文件移动到同一文件夹。
  • /s要将文件添加到的符号服务器的路径。如果只是一个空文件夹,那么它将添加必要的文件和文件夹,将空文件夹转换为符号服务器。
  • /t事务的名称,这是一个必需的参数,通常您只需将项目名称放在此处,或任何其他标识字符串。
  • /v要添加的文件的版本号。它不是必需的,只是为了您的方便,所以如果您需要手动找到一组特定的符号,您可以。
  • /c事务的注释,同样地,它不是必需的,只是用于日志文件和您的利益。

Symstore还有一些其他参数,允许您以几种不同的方式设置symbol服务器。我不在这里介绍它们,因为这里有一个很好的MSDN页面来解释它们,您可以从文章底部的链接中找到它们。

如何索引符号文件

在将符号文件添加到服务器之前,可以在其中嵌入命令,以便从版本控制系统或其他任何相关的位置提取当前源代码。Windows调试工具中包含一些脚本,这些脚本将把不同版本控制系统中的源代码索引到PDB文件中。我将给出一个如何使用脚本的快速示例,然后我将检查实际发生的情况,以便如果您有需要,可以编写自己的脚本。
要使用源索引脚本,首先需要安装Perl,因为这些实际上是Perl脚本。一旦您构建了项目,并希望用附加信息为PDB文件编制索引,调试器将需要从版本控制系统中提取源代码,您需要转到Windows安装调试工具中的srcsrv文件夹,并找到版本控制的相关脚本,例如,您正在使用Subversion,那么您需要运行svnindex.cmd。有两个参数需要传递给脚本,以便脚本可以索引文件,它们是指向项目工作目录的源路径的分号分隔列表,以及PDB文件所在文件夹的分号分隔列表。所以你的命令是:

svnindex.cmd /source="c:\SharedModules;C:\MyVeryImportantProject"
/symbols="c:\SharedModules\Release;c:\MyVeryImportantProject\Release"

然后,脚本将插入PDB文件中列出的每个文件的命令,以从SVN中提取这些命令。在SVN的情况下,如果需要使用特定的用户名和密码,还可以传入/user=“MyUserName”/pass=“MyPassword”注意,这些参数是特定于SVN脚本的,其他脚本可能不总是接受用户名和密码,并且可能有自己的特定设置。与控制台中的大多数内容一样,您可以通过传递-?作为论据。
每个索引脚本还支持从两个环境变量加载一个变量以及一个名为SrcSrv.ini的配置文件。运行脚本时,它们将使用大多数本地设置,因此命令行参数将覆盖srcsrv.ini,后者将覆盖环境变量。还可以通过添加/ini=“Path to ini file”指定运行脚本时要使用的特定配置文件。

脚本里做了什么

基本上,索引脚本的目的是生成这样的数据块:

SRCSRV: ini ------------------------------------------------
VERSION=1
INDEXVERSION=2
VERCTRL=Test
DATETIME=Mon, 04 October 2010
SRCSRV: variables ------------------------------------------
SRCSRVTRG=%targ%\%var4%\%var2%\%fnfile%(%var1%)
SRCSRVCMD=cmd /c copy "%var1%" "%SRCSRVTRG%"
SRCSRV: source files ---------------------------------------
D:\Documents\SKProjects\AlphaForms\AlphaForms\LayeredWindow.cs*11*_*AlphaForms\AlphaForms
*svn://192.168.1.5/AlphaForms/trunk/AlphaForms/LayeredWindow.cs
SRCSRV: end ------------------------------------------------

上面有一些关于如何构建命令的信息,下面列出了每个文件的该命令的参数。因此,如果该命令是一个简单的副本,那么您可以将该命令设置为:

copy "%var1%" "%srcsrvtrg%

var1引用变量列表中的第一项,在本例中为D:\ Documents\SKProjects\AlphaForms\AlphaForms\LayeredWindow.cs。srcsrvtrg是数据块头中指定文件复制位置的命名变量之一。在调用命令之前,调试器将检查文件是否存在,因此,如果您在此之前打开了该文件的版本,则不需要重新运行一个可能非常慢的命令。srcsrvtrg由其他几个命名变量组成。targ是文件将放入的本地缓存目录,fnfile实际上是一个函数,它从以下括号中指定的路径(在本例中是var1,文件的路径)获取文件名。
您可能已经注意到,两个模符号之间的所有内容都被视为变量,并将被它们表示的实际数据替换(如果可能的话),这种替换值的操作也是递归的,就像srcsrvtrg变量将被它表示的字符串替换一样,然后填写变量targ和var1。
事实上,将这些数据插入PDB文件是完全无用的,因为它只会将您系统中已有的文件复制到其他地方。一个更现实的例子是:

SRCSRV: ini ------------------------------------------------
VERSION=1
INDEXVERSION=2
VERCTRL=Subversion
DATETIME=Mon, 04 October 2010
SRCSRV: variables ------------------------------------------
SRCSRVTRG=%targ%\%var4%\%var2%\%fnfile%(%var1%)
SRCSRVCMD=cmd /c "svn cat "%var5%@%var2% --non-interactive > "%SRCSRVTRG%"
SRCSRV: source files ---------------------------------------
D:\Documents\SKProjects\AlphaForms\AlphaForms\LayeredWindow.cs*11*_*AlphaForms\AlphaForms
*svn://192.168.1.5/AlphaForms/trunk/AlphaForms/LayeredWindow.cs
D:\Documents\SKProjects\AlphaForms\AlphaForms\AlphaForm_WndProc.cs*10*_
*AlphaForms\AlphaForms*svn://192.168.1.5/AlphaForms/trunk/AlphaForms/AlphaForm_WndProc.cs
D:\Documents\SKProjects\AlphaForms\AlphaForms\AlphaForm.cs*10*_
*AlphaForms\AlphaForms*svn://192.168.1.5/AlphaForms/trunk/AlphaForms/AlphaForm.cs
SRCSRV: end ------------------------------------------------

然后,实际的命令将由main部分中指定的变量生成,因此对于第一个文件,它将如下所示:

cmd /c "svn cat " svn://192.168.1.5/AlphaForms/trunk/AlphaForms/LayeredWindow.cs@11
--non-interactive > "C:\Documents…"

然后将执行此命令,并希望将所需的文件放在目标目录中,然后调试器将尝试打开它。
如果您确实想编写自己的脚本或程序来索引PDB文件,那么您只需要生成一个类似的数据块,然后使用pdbstr.exe将其插入PDB文件。将自己的源代码编制索引的基本步骤将是这样的:

  1. 收集工作目录中的文件列表
  2. 获取提取命令的每个文件的参数列表
  3. 使用srctool获取PDB中引用的文件列表
  4. 将数据块的头写入某个临时文件
  5. 对于PDB中的每个文件,将参数添加到临时文件
  6. 使用pdbstr将数据插入PDB

要获取PDB中引用的文件列表,请使用:

srctool.exe "path to pdb file" –r 

它将把pdb中的每个文件打印到一个新的行上。要将数据块添加到PDB文件,需要使用:

pdbstr –w –p:"path to pdb file" –s:srcsrv –i:"path to temp file" 

-w开关指定您正在写入文件,使用-r会将数据流(如果存在)打印到控制台。-s给出了我们要写入的数据流的名称,在本例中是srcsrv。实际上,您可以使用任何喜欢的流名称将任何需要的内容插入到PDB文件中,但是Visual Studio将在srcsrv流中查找数据。-我给出了将插入PDB文件的输入文件的路径,在您的情况下,该文件就是您将数据写入的文件。