2023年1月

本文概述了从生成进程的内存转储、在一台计算机上运行某个版本的CLR到将转储加载到在另一台计算机上运行的WinDbg所涉及的步骤。在WinDbg中,我们希望加载SOS调试器扩展来查询转储。具体来说,即使WinDbg机器没有安装运行转储进程的机器所安装的CLR版本,SOS也应该加载。

步骤1:生成进程转储

要生成示例转储,请导航到Windows任务管理器,右键单击w3wp.exe(我们知道它正在运行.NET代码),然后选择创建转储文件。对于我们希望基于触发器(如CPU或内存利用率超过某个阈值)生成转储的情况,或者当进程抛出某种类型的异常时,ADPlus、DebugDiag和ProcDump提供了更好的转储捕获方法。
在准备将转储复制到WinDbg机器时,我们必须确定w3wp进程中运行的CLR版本及其位。如果对CLR版本有疑问,我们始终可以检查IIS应用程序池设置,在本例中显示的是.NET Framework v2.0.50727。至于位,Task Manager显示w3wp,没有*32后缀,表示64位进程。另一种方法是,我们可以使用Sysinternals流程资源管理器,查找加载到流程中的dll,这些dll来自C:\ Windows\Microsoft.NET子文件夹之一。
在C:\Windows\Microsoft.NET\Framework64中,每个x64版本的.NET都显示为一个子文件夹(v2.0.50727、v4.0.30319等)。文件夹名称版本控制方案缺少的是修补程序版本。查看v2.0.50727文件夹中某个dll的属性,我们可以看到w3wp进程中运行的CLR的确切版本实际上是2.0.50727.4253。
有了内存转储和有关在转储进程中运行托管代码的CLR版本和位的知识,我们就可以将转储和辅助文件复制到WinDbg计算机。

步骤2:将转储和.NET CLR dll复制到其他计算机

要使WinDbg能够加载和检查转储,w3wp中运行的CLR的辅助文件必须可用。这些文件与上面确定的修补程序版本和位匹配,可能已经存在于WinDbg计算机上。但是,我们不是假设,而是将所需的两个文件从w3wp机器的.NET Framework文件夹以及转储文件复制到WinDbg机器:

  • mscordacwks.dll:用于工作站的Microsoft公共对象运行时数据访问组件(COR是CLR的早期名称)公开了API,WinDbg可以通过这些API访问转储进程的内存,从而访问CLR数据结构。组件实际上是从与正在执行进程内的CLR相同的源代码编译的。在死后调试期间,它充当CLR的替身,在转储中及时冻结。在运行的进程中,查询CLR数据结构不仅意味着读取内存结构,还意味着执行本机代码来解释这些结构。在调试的进程之外运行的数据访问组件也有同样的用途。
  • sos.dll:Strike之子的缩写,此库包含WinDbg.NET扩展命令。通过使用mscordacwks.dll(并通过本机检查转储),这些SOS命令查询和解释CLR数据结构,并以可消化的格式呈现这些结构。如果没有mscordacwks.dll和sos.dll,我们将看到CLR数据结构在内存中的分层。

由于CLR内部以及SOS命令可能会随新的运行时版本而更改,因此每个运行时都会附带mscordacwks.dll和SOS.dll的特定版本。因此,从.NET框架文件夹中收集mscordacwks.dll和sos.dll,并与w3wp.dmp一起将这些文件复制到WinDbg计算机上的C:\ debug\w3wp-sp2007文件夹中。

步骤3:首次尝试将w3wp.dmp加载到WinDbg中

打开WinDbg(X64)并转到“文件”菜单,打开崩溃转储。。。并找到C:\ debug\w3wp-sp2007\w3wp.dmp。作为响应,WinDbg打印以下输出:

接下来,我们发出命令,设置公共Microsoft符号服务器的符号路径,并为加载到w3wp的模块重新加载符号。然后,基于加载到w3wp中的mscorwks.dll的文件夹位置(版本),我们从同一路径加载SOS。.loadby命令实际上是general.load命令的快捷方式,接受要加载的DLL的路径。引入loadby是因为.NET dll往往有一个很长的路径,因此快捷方式根据加载的mscorwks.dll的位置解析路径。在CLR 4.0及更高版本上,命令为.loadby sos CLR,因为CLR现在位于CLR.dll中:

0:000>.symfix0:000>.reload
................................................................
................................................................
................................................................
................................................................
................................................................
................................................................
.
Loading unloaded module list
................
0:000> .loadby sos mscorwks

在处理二进制数据时,我们经常使用dt命令将字节分组到有意义的字段中,例如。

0:000> dt ntdll!_PEB @$peb+0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   +0x003 BitField         : 0x8 ''
   +0x003ImageUsesLargePages : 0y0+0x003IsProtectedProcess : 0y0+0x003IsLegacyProcess  : 0y0+0x003IsImageDynamicallyRelocated : 0y1+0x003SkipPatchingUser32Forwarders : 0y0
...

每个开发人员都知道单元测试提高了代码的质量。我们还从静态代码分析中获益,并在我们的构建管道中使用SonarQube等工具。然而,我仍然发现许多开发人员并不知道检查代码有效性的一种更古老的方法:断言。在这篇文章中,我将向您介绍使用断言的好处,以及.NET应用程序的一些配置技巧。我们还将学习.NET和Windows如何支持它们。

什么是断言,什么时候使用它们

断言声明某个谓词(真-假表达式)在程序中的特定时间必须为真。当断言的计算结果为false时,会发生断言失败,这通常会导致程序崩溃。我们通常在调试版本中使用断言,并在调试器或某些特殊日志中处理断言异常(稍后我们将重点讨论配置)。在.NET中,有两种使用断言的方法:Debug.Assert or Trace.Assert.第一个方法的定义如下:

[System.Diagnostics.Conditional("DEBUG")]public static void Assert(bool condition, stringmessage) {
TraceInternal.Assert(condition, message);
}

某些项目设置也对调试产生影响。 这些设置确定诸如调试器查看的目录、用于启动程序的命令和命令参数以及为程序创建的调试信息的类型等内容。 可以在**“属性页”**对话框中更改这些设置。

本文介绍可用选项,并介绍如何进行设置。

如何:指定调试器设置

在 Visual Studio 中可为调试器行为指定各种设置,包括如何显示变量,是否显示某些警告,如何设置断点以及中断如何影响正在运行的程序。 在“选项”对话框中可以指定调试器设置。设置步骤如下:

  1. 在“工具”菜单上,单击“选项”。

  2. 在“选项”对话框中打开“调试”文件夹。

  3. 在“调试”文件夹中选择所需选项的类别。

    最常用的选项位于“常规”类别中。

  4. 选择或清除所需选项。 按 F1 可获得有关选项的帮助。

如何:设置调试和发布配置

Visual Studio项目中,程序的发行版本和调试版本使用不同的配置。 顾名思义,生成调试版本的目的是用于调试,而生成发行版本的目的是用于版本的最终分发。如果在 Visual Studio 中创建程序,Visual Studio 会自动创建这些配置并设置适当的默认选项和其他设置。 在默认设置下:

  • 程序的“调试”配置用全部符号调试信息编译,不进行优化。 优化会使调试复杂化,因为源代码和生成的指令之间的关系更加复杂。

  • 程序的“发布”配置被完全优化,不包含任何符号调试信息。 根据使用的编译器选项,可在 程序数据库文件 (C++)中生成调试信息。 如果以后还必须调试发行版本,创建 PDB 文件就非常有用。

可使用“标准”工具栏或配置管理器在发行版本和调试版本之间进行切换。安装 Visual Studio 时,会要求您选择主编程语言的一组开发设置。 如果选择“Visual Basic 开发设置”,则选择“调试”或“发布”配置的工具不会出现在工具栏中。 如果您从“调试”菜单中选择“开始”,Visual Studio 会自动选择“调试”配置;如果您使用“生成”菜单,则会自动选择“发布”配置。 

快速切换到“调试”或“发布”配置

在“标准”工具栏上,选择“解决方案配置”列表框中的“调试”“发布”。 在 Visual Basic 速成版 或 Visual C# 速成版 中没有此工具栏。

 

 

切换到“调试”或“发布”配置

  1. 在“解决方案资源管理器”中选择项目。

  2. 在“视图”菜单上,单击“属性页”。

  3. 单击“生成”“调试”选项卡(在 Visual C# 或 Visual F# 项目中)、“编译”“调试”选项卡(在 Visual Basic 项目中),或者“配置属性”(在 Visual C++ 项目中)。

  4. 在“配置”下拉列表中单击“调试”“发布”。

C++ 调试配置的项目设置

可在“属性页”对话框中更改 C 或 C++ 调试配置的项目设置,这在上节中进行了探讨。 下表显示“属性页”对话框中与调试器有关的设置的位置。在“要启动的调试器”列表框中指定要使用的调试器。 您的选择将影响属性的可见性。每当您保存解决方案时,每个调试属性设置均自动写入并保存到解决方案的“每用户”文件 (.vcxproj.user)。

配置属性”文件夹(“调试”类别)

 

 

设置

说明

要启动的调试器

指定要运行的调试器,有以下选择:


  • 本地 Windows 调试器

  • 远程 Windows 调试器

  • Web 服务调试器

  • MPI 群集调试器

  • Web 浏览器调试器

“命令”(本地 Windows 调试器)

指定在本地计算机上用于启动要调试程序的命令。

“远程命令”(远程 Windows 调试器)

“应用程序命令”(MPI 群集调试器)

远程计算机上的 .exe 的路径。 可以像在远程计算机上一样输入路径。

“命令参数”(本地 Windows 调试器和远程 Windows 调试器)

“应用程序参数”(MPI 群集调试器)

  • 为前面指定的命令指定参数。


可以在此框中使用下列重定向运算符:


< file

从文件中读取 stdin。

> file

将 stdout 写入文件。

>> file

将 stdout 追加到文件。

2> file

将 stderr 写入文件。

2>> file

将 stderr 追加到文件。

2> &1

将 stderr (2) 输出发送到与 stdout (1) 相同的位置。

1> &2

将 stdout (1) 输出发送到与 stderr (2) 相同的位置。


大多数情况下,这些运算符仅适用于控制台应用程序。

工作目录

指定要调试的程序的工作目录(相对于 EXE 所在的项目目录)。 如果保留此设置为空白,则工作目录就是项目目录。 对于远程调试,项目目录将位于远程服务器上。

“附加”(本地 Windows 调试器和远程 Windows 调试器)

指定要启动应用程序还是附加到应用程序。 默认设置为“否”。

“远程服务器名称”(远程 Windows 调试器和 MPI 群集调试器)

指定您要在上面调试应用程序的计算机(不是您的计算机)的名称,或指定 Msvsmon 服务器名称。 也可以从“进程”对话框中选择此计算机名称(请参见选择远程计算机)。 如果在此处指定计算机名称,则还必须在“连接”中指定连接类型。

RemoteMachine 生成宏被设置为此属性的值;有关更多信息,请参见用于生成命令和属性的宏。

“连接”(远程 Windows 调试器和 MPI 群集调试器)

允许您在远程调试的标准与非身份验证连接类型之间切换。 在“远程服务器名称”框中指定远程计算机的名称。 连接类型包括:


  • 带 Windows 身份验证的远程访问

  • 不带身份验证的远程访问(仅限本机)


注意 不带身份验证的远程调试可能会使远程计算机容易受到安全攻击。 Windows 身份验证模式更安全。

有关更多信息,请参见安装远程调试。

HTTP URL(Web 服务调试器和 Web 浏览器调试器)

指定您要调试的项目所在的 URL。

调试器类型

指定要使用的调试器类型:“仅限本机”“仅限托管”“混合”“自动”(默认)或“脚本”


  • “仅限本机”适用于非托管 C++ 代码。

  • “仅限托管”适用于在公共语言运行时下运行的代码(托管代码)。

  • “混合”对托管代码和非托管代码都调用调试器。

  • “自动”将根据编译器和 EXE 信息确定调试器类型。

  • “脚本”调用脚本调试器。

“环境”(本地 Windows 调试器)

为您要调试的程序指定环境变量。 使用标准的环境变量语法(例如,PATH="%SystemRoot%\ … …")。 根据“合并环境”设置的不同,这些变量重写系统环境或与系统环境合并。 当在设置列单击时,出现“编辑…”字样 单击该链接编辑环境变量。

“合并环境”(本地 Windows 调试器)

确定在“环境”框中指定的变量是否与操作系统定义的环境合并。 默认设置为“是”。

“SQL 调试”(除 MPI 群集调试器外的所有调试器)

“C/C++”文件夹(“常规”类别)

设置

说明

调试信息格式 (/Z7、/Zd、/Zi、/ZI)

指定要为项目创建的调试信息类型。

默认选项 (/ZI) 以“编辑并继续”的兼容格式创建程序数据库 (PDB)。 有关更多信息,请参见 /Z7、/Zd、/Zi、/ZI(调试信息格式)。

有关更多信息,请参见 PDB 文件和 DBG 文件。

“C/C++”文件夹(“优化”类别)

 

 

设置

说明

优化

指定编译器是否应优化其生成的代码。 优化过程将更改执行的代码。 优化的代码不再与源代码匹配。 因此,调试将变得非常困难。

默认选项(“禁用 (/0d)”)取消优化。 您可以在开发时取消优化,并在创建代码的产品版本时再启用优化。

“链接器”文件夹(“调试”类别)

 

设置

说明

生成调试信息 (/DEBUG)

通知链接器收集调试信息,这些信息具有 /Z7、/Zd、/Zi 或 /ZI 指定的格式。

生成程序数据库文件 (/PDB:name)

在该框中指定 PDB 文件的名称。 必须为“调试信息格式”选择 /ZI 或 /Zi。

去除私有符号 (/PDBSTRIPPED:filename)

如果不希望在 PDB 文件中包含私有符号,则在该框中指定 PDB 文件的名称。 当使用任何生成 PDB 文件的编译器或链接器选项(如 /DEBUG、/Z7 和 /Zd)生成程序图像时,此选项创建第二个程序数据库 (PDB) 文件。 或/Zi。 这第二个 PDB 文件省略您不希望交付给用户的符号。 有关更多信息,请参见 /PDBSTRIPPED(去除私有符号)。

生成映射文件 (/MAP)

通知链接器在链接过程中生成映射文件。 默认设置为“否”。 有关更多信息,请参见 /MAP(生成映射文件)。

映射文件名(/MAP:name)

如果选择“生成映射文件”,则可在该框中指定映射文件。 有关更多信息,请参见 /MAP(生成映射文件)。

映射导出 (/MAPINFO:EXPORTS)

在映射文件中包含导出函数。 默认设置为“否”。 有关更多信息,请参见 /MAPINFO(包含映射文件中的信息)。

“可调试程序集”(/ASSEMBLYDEBUG)

为链接器 /ASSEMBLYDEBUG 选项指定设置。 可能值如下:


  • “未产生 Debuggable 特性”

  • “运行时跟踪和禁用优化 (/ASSEMBLYDEBUG)”。 这是默认设置。

  • “无运行时跟踪和启用优化 (/ASSEMBLYDEBUG:DISABLE)”

  • “<从父级或项目默认设置继承>”

  • 有关更多信息,请参见 /ASSEMBLYDEBUG(添加 DebuggableAttribute)。

通过使用 Microsoft.VisualStudio.VCProjectEngine.VCDebugSettings 接口,可以在“配置属性”文件夹(“调试”类别)中以编程方式更改这些设置。

程序数据库文件

程序数据库 (PDB) 文件保存着调试和项目状态信息,使用这些信息可以对程序的调试配置进行增量链接。 当以 /ZI 或 /Zi(用于 C/C++)生成时,将创建一个 PDB 文件。在 Visual C++ 中,/Fd 选项用于命名由编译器创建的 PDB 文件。 当使用向导在 Visual Studio 中创建项目时,/Fd 选项被设置为创建一个名为 project.PDB 的 PDB。

如果使用生成文件创建 C/C++ 应用程序,并指定 /ZI/Zi 而不指定 /Fd 时,则最终将生成两个 PDB 文件:

  • VC80.PDB   (更笼统地说就是 VCx0.PDB,其中 x 表示 Visual C++ 的版本。)该文件存储各个 OBJ 文件的所有调试信息并与项目生成文件驻留在同一个目录中。

  • project.PDB   该文件存储 .exe 文件的所有调试信息。 对于 C/C++,它驻留在 \debug 子目录中。

每当创建 OBJ 文件时,C/C++ 编译器都将调试信息合并到 VCx0.PDB 中。 插入的信息包括类型信息,但不包括函数定义等符号信息。 因此,即使每个源文件都包含公共头文件(如 <windows.h>),这些头文件中的 typedef 也只存储一次,而不是在每个 OBJ 文件中都存在。

链接器将创建 project.PDB,它包含项目的 EXE 文件的调试信息。 project.PDB 文件包含完整的调试信息(包括函数原型),而不仅仅是在 VCx0.PDB 中找到的类型信息。 这两个 PDB 文件都允许增量更新。 链接器还在其创建的 .exe 或 .dll 文件中嵌入 .pdb 文件的路径。

Visual Studio 调试器使用 EXE 或 DLL 文件中的 PDB 路径查找 project.PDB 文件。 如果调试器在该位置无法找到 PDB 文件或者如果路径无效(例如,如果项目被移动到了另一台计算机上),调试器将搜索包含 EXE 的路径,即在“选项”对话框(“调试”文件夹,“符号”节点)中指定的符号路径。 如果调试器无法找到 .PDB 文件,则显示“查找符号”对话框,这将允许您搜索符号或向搜索路径添加其他位置。

微软以不断发明新的专有文件格式而闻名,而.pdb文件的程序数据库格式就是其中之一。许多软件开发人员都在拼命寻找关于PDB内部的更多信息。好消息:搜索结束了!本文将PDB格式记录到最后一位,并提供一个Win32实用程序,该实用程序将复合PDB文件拆分为单独的文件。
软件开发和调试工具的编写人员经常面临着显示有关Windows 2000系统模块的符号信息的任务。例如,每个好的反汇编程序都不应该只显示原始的数字和地址,而应该尝试将它们解析为有意义的名称。否则,用户很难弄清楚反汇编代码的实际用途。要采取的第一步(也是最简单的一步)是使用隐藏在被检查模块的可移植可执行文件(PE)文件中的符号信息,以及它通过动态链接引用的所有模块。然而,这些信息并不足以完全理解反汇编代码,因为这些信息只涉及模块之间的接触点。要掌握内部(非导出)函数的语义,首先了解函数的名称通常非常有帮助。还必须知道它调用的从属函数的名称和它访问的全局变量。幸运的是,微软在Windows2000操作系统中以符号文件的形式发布了这些重要信息,这些文件的扩展名是.dbg和.pdb。

.dbg and .pdb Symbol Files

在安装Windows 2000、微软Visual C/C++和平台SDK之后,您仍然会丢失它们,必须在单独的步骤中设置的符号文件。请注意,符号文件必须始终与操作系统的“已更正的服务磁盘”(CSD)级别匹配。也就是说,在每次安装Service Pack和热修复程序之后,也必须始终更新符号文件。通常,符号安装程序会附带新的操作系统文件。安装程序名为symbolsx.exe,符号文件位于关联的存档文件symbols.cab中。首次运行安装程序后,您会发现包含Windows 2000操作系统的硬盘丢失了400MB到500MB的可用空间。默认情况下,符号文件安装在名为Symbols的目录树中,该目录树包含在Windows 2000系统根目录中(例如,C:\WINNT\Symbols)。

对于每个模块文件扩展名,将创建一个单独的子目录,并且每个系统模块都有两个文件扩展名为.dbg和.pdb的符号文件。如果您以前使用过WindowsNT4.0,您可能会想为什么现在每个Windows2000需要两个符号文件,而不是一个.dbg文件。原因是微软已经将所谓的“公共符号”移动到一个叫做程序数据库(Program Database,PDB)的单独文件中。基本上,PDB文件是由几个独立的流组成的复合文件。您可以将PDB复合文件视为单个文件中的简单平面文件系统,其中的流与文件系统承载的文件相对应。其中一个流包含一系列可变长度的记录,这些记录描述了在关联模块中定义的符号。

在上一个示例中,假定Windows 2000安装在目录C:\ WINNT中,符号根目录为C:\ WINNT\Symbols,则Windows 2000内核模块ntoskrnl.exe的符号文件将安装为C:\ WINNT\Symbols\exe\ntoskrnl.dbg和C:\ WINNT\Symbols\exe\ntoskrnl.pdb。同样,ntdll.dll符号文件的路径将是C:\ WINNT\Symbols\dll\ntdll.dbg和C:\ WINNT\Symbols\dll\ntdll.pdb。这些文件的主要目的之一是允许调试器或反汇编程序查找可归属于模块中给定二进制地址的最近符号名。例如,如果反汇编程序发现要显示的下一个汇编语言指令是call72A05A2Eh,那么最好为用户提供与函数入口点0x72A05A2E相关联的实名,例如call_pMemAlloc@4。

PDB文件布局全局

在本文中,我将介绍更一般的任务,即解析PDB文件并将其分解为各个部分,而不考虑其各种流的内容。这里提供的示例应用程序是一个简单的Win32控制台模式程序,它期望PDB文件的路径,并将其组成部分存储在单独的文件中。根据命令行中指定的选项开关,可以提取以下信息:

  • PDB文件头,包含有关文件的一般信息
  • 分配位表,指定文件的哪些部分正在使用
  • 根流,包含有关所有数据流的特定信息
  • 根流中列出的所有数据流

PDB文件最基本的结构属性是将其细分为大小相等的页面。最常用的页面大小是1KB(1024字节),但我的研究表明2K和4K页面也是合法的。(可以通过检查Microsoft的dbghelp.dll如何处理PDB文件来验证这一点。)PDB流是包含一致信息的文件页序列。流最基本的特性是它的页面可以按任意顺序位于文件中的任何位置。
当一个流被读或写时,流目录负责告诉应用程序哪些页面需要按哪个顺序访问。此目录本身存储在名为根流的流中。此外,嵌入的分配位表会跟踪已使用和未使用的页。由于流的重新排列,一旦PDB文件中出现“洞”,此表就必不可少。如果流被重写到PDB文件的末尾,释放它之前占用的页,则分配位表反映在使用新页时前页是空闲的。该方案是从简单的操作系统(如MS-DOS)及其胖文件系统借鉴而来的,其中有一个类似的表指定将哪些磁盘扇区分配给文件。
下图显示了1-KB、2-KB和4-KB模式下PDB文件的典型基本布局。应该使用哪个页面大小取决于要存储在流中的数据。如果页面大小增加,分配位表和根流就会变小。另一方面,较大的页面大小会导致更多的页面悬置;也就是说,如果流大小不是页面大小的确切倍数,则会浪费更多的文件字节。同样的问题也出现在文件系统中,在文件系统中,必须正确选择磁盘扇区大小,以避免过度的扇区挂起。大多数PDB文件,如Windows 2000符号文件和微软Visual C/C++ 6所生成的调试信息,都采用1-KB方案,如下图左侧所示。

 

 在1-KB页面模式下,PDB文件中最多可以存储64MB,这只是将分配位的数量乘以页面大小的结果。稍后我将展示,PDB页码存储为基于零的16位数量。因此,分配位表永远不会包含超过0[ts]10000位(8KB)。
PDB头总是占据第一个文件页,后面跟着一个或多个包含分配位的页。头的结构定义如清单1所示。前44个字节组成ID字符串PDB_SIGNATURE_200,指定文件类型和版本。撰写本文时,最新版本为2,这是Windows 2000符号文件和Visual C/C++调试信息所使用的版本。dPageSize成员指示应用于文件中所有页的页大小,wStartPage是分配位之后的第一个数据页的从零开始的页码。分配位表的大小始终可以通过从wStartPage中减去1(对于头页)并将结果乘以dPageSize来计算。wFilePages成员指定PDB文件中存储的页数,并且应该始终与文件大小(字节)除以页面大小相匹配。

Listing 1PDB File Header

#define PDB_SIGNATURE_200 
    "Microsoft C/C++ program database 2.00\r\n\x1AJG\0"

#define PDB_SIGNATURE_   44 //size of signature (bytes)

//-----------------------------------------------------------------
typedefstruct_PDB_HEADER
{
BYTE abSignature [PDB_SIGNATURE_];
//PDB_SIGNATURE_200 DWORD dPageSize; //0x0400, 0x0800, 0x1000 WORD wStartPage; //0x0009, 0x0005, 0x0002 WORD wFilePages; //file size / dPageSize PDB_STREAM RootStream; //stream directory WORD awRootPages []; //pages containing PDB_ROOT }
PDB_HEADER,
*PPDB_HEADER, **PPPDB_HEADER;