2023年1月

如果您厌倦了启动调试器、加载转储文件、设置sympath、加载扩展名等,这里有一个很好的方法,可以在.dmp文件的上下文菜单上获取“调试此转储文件”,并自动加载所有您喜欢的命令。
首先创建一个包含以下内容的.reg文件(在修改注册表时一定要非常小心)

Windows Registry Editor Version 5.00[HKEY_CLASSES_ROOT\.dmp]

@
="Debugger.Dump"[HKEY_CLASSES_ROOT\Debugger.Dump]

[HKEY_CLASSES_ROOT
\Debugger.Dump\DefaultIcon]

@
="c:\\debuggers\\cdb.exe"[HKEY_CLASSES_ROOT\Debugger.Dump\Shell]

[HKEY_CLASSES_ROOT
\Debugger.Dump\Shell\Debug_Without_Remote]

@
="Debug This Dump"[HKEY_CLASSES_ROOT\Debugger.Dump\Shell\Debug_Without_Remote\Command]

@
="\"C:\\debuggers\\windbg\" -z \"%1\" -c \"$<c:\\debuggers\\commands.txt\""[HKEY_CLASSES_ROOT\Debugger.Dump\Shell\Debug_With_Remote70]

@
="Debug this Dump With Remote:70"[HKEY_CLASSES_ROOT\Debugger.Dump\Shell\Debug_With_Remote70\Command]

@
="\"C:\\debuggers\\windbg\" -server tcp:port=70 -z \"%1\" -c \"$<c:\\debuggers\\commands.txt\""

符号可以包含有关全局变量、局部变量、函数名、参数、结构和源行号的信息。符号有三种类型:导出符号、pdb符号(公共符号)和专用pdb符号(专用符号)。
导出符号是dll本身的一部分。例如,ntdll.dll和kernel32.dll将其函数的很大一部分公开为导出符号,因此可以将它们称为API,但是在进程中发现的大多数dll都有一组非常小的导出符号。通常导出符号不包含函数的参数信息,并且由于很少有函数以这种方式公开,因此当只有导出符号时,您不能真正依赖堆栈的有效性。
公共符号包含一些基本符号,如函数名和全局变量,但同样,并非所有函数名都在公共符号中公开。dll的开发人员选择公开什么作为公共符号,因此他/她可以隐藏任何他们认为会泄露太多有关实现信息的内容。私人符号包含第一段中列出的几乎所有内容

调试时,通过将dll/exe链接到符号文件的GUID,符号与相应的dll或exe匹配。这意味着,如果符号搜索路径中有多个ntdll.pdb,调试器将知道哪个对应于特定版本的ntdll.dll。搜索路径由.sympath指定,除sympath中列出的内容外,调试器还将查找加载dll的目录以及环境变量_NT_SYMBOL_PATH中给定的路径中的任何内容。

如果符号是错误的的时候会发生什么?

让我们看看这个带有mscorsvr.dll公共符号的堆栈:

 54  Id: 62c.1590 Suspend: 1 Teb: 7ffa2000 Unfrozen
ChildEBP RetAddr  Args to Child
1212ef44 7c59a030 00000090 00000000 1212ef64 ntdll!NtWaitForSingleObject+0xb [i386\usrstubs.asm @ 2004]
1212ef6c 7c57b3db 00000090 00009c40 00000000 kernel32!WaitForSingleObjectEx+0x71 [D:\nt\private\windows\base\client\synch.c @ 1309]
1212ef7c 791b578b 00000090 00009c40 00000000 kernel32!WaitForSingleObject+0xf [D:\nt\private\windows\base\client\synch.c @ 1217]
1212efa0 791dbe6e 00000000 00000000 00000000 mscorsvr!ThreadpoolMgr::WorkerThreadStart+0x3a
1212ffb4 7c57b388 0d406838 00000002 00000000 mscorsvr!ThreadpoolMgr::intermediateThreadProc+0x44
1212ffec 00000000 791dbe2d 0d406838 00000000 kernel32!BaseThreadStart+0x52 [D:\nt\private\windows\base\client\support.c @ 460]

我们正在创建一个工作线程,然后坐下来等待工作。让我们看一看同一个堆栈,其中包含用于mscorsvr的导出符号:

0:054> kb
ChildEBP RetAddr  Args to Child
WARNING: Stack unwind information not available. Following frames may be wrong.
1212ef6c 7c57b3db 00000090 00009c40 00000000 ntdll!NtWaitForSingleObject+0xb
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for mscorsvr.dll -
1212efa0 791dbe6e 00000000 00000000 00000000 kernel32!WaitForSingleObject+0xf
1212ffb4 7c57b388 0d406838 00000002 00000000 mscorsvr!GetCompileInfo+0x8e99
1212ffec 00000000 791dbe2d 0d406838 00000000 kernel32!lstrcmpiW+0xb

调试器很好,告诉我们找不到mscorsvr的符号文件,但是它给了我们一个函数名(从导出符号中),所以看起来我们正在调用一个名为GetCompileInfo的函数,嗯…很奇怪,它为什么选择这个名称?
如果我们要列出符号(导出mscorsvr的符号),我们会得到一个如下所示的列:

0:054>x mscorsvr!*

791b0000 mscorsvr!Ordinal73
=791b0000 mscorsvr!Ordinal76=791b0000 mscorsvr!Ordinal77=791b0000 mscorsvr!Ordinal75=791b0000 mscorsvr!Ordinal78=791b0000 mscorsvr!Ordinal71=791b0000 mscorsvr!Ordinal79=791b0000 mscorsvr!Ordinal74=791b0000 mscorsvr!Ordinal72=791d2fd5 mscorsvr!GetCompileInfo= 791e0920 mscorsvr!GetAssemblyMDImport

这是一个我经常遇到的问题,我们经常会遇到这样的情况:我们必须重新捕获内存转储,因为内存转储是以“错误”的方式捕获的。
简而言之:如果在64位计算机上执行32位进程,则需要使用允许创建32位转储的工具捕获转储。

怎么知道进程是32位的?

如果您在64位计算机上,可以检查任务管理器以查看您的进程正在使用的体系结构。

带*32的进程是32位的,其余的是64位的,因此在上面的示例中,我们可以看到QQ.exe正在执行32位代码。

为什么用正确的工具捕捉它们很重要?

如果使用捕获64位转储的工具捕获转储,您仍将获得内存转储,但您将获得syswow64的内存转储,这意味着许多扩展(如sos和psscor2)将无法读取数据。有些事情可能仍然有效,但它是非常有限的,你可能会得到错误或错误的调用堆栈等。
当您试图读取32位进程的64位内存转储时,通常会发生以下情况:

1、你可能会看到这样的警告

WARNING: wkscli overlaps srvcli
  ..............WARNING: wship6 overlaps dnsapi
  .WARNING: IPHLPAPI overlaps dsrole
  ...WARNING: FWPUCLNT overlaps rasadhlp
  WARNING: FWPUCLNT overlaps dnsapi
  .....WARNING: compstat overlaps iisre

2、堆栈显示wow64cpu方法

0:000> kp 
   Child-SP          RetAddr           Call Site 
   00000000`000ce728 00000000`73a22bcd wow64cpu!CpupSyscallStub+0x9 
   00000000`000ce730 00000000`73a9d07e wow64cpu!Thunk0ArgReloadState+0x1a 
   00000000`000ce7f0 00000000`73a9c549 wow64!RunCpuSimulation+0xa 
   00000000`000ce840 00000000`76d684c8 wow64!Wow64LdrpInitialize+0x429 
   00000000`000ced90 00000000`76d67623 ntdll!LdrpInitializeProcess+0x17e2 
   00000000`000cf290 00000000`76d5308e ntdll! ?? ::FNODOBFM::`string'+0x2bea0 
   00000000`000cf300 00000000`00000000 ntdll!LdrInitializeThunk+0xe

3、所有地址都是64位地址,即使这是一个32位的进程。

4、在尝试运行sos命令时会出现如下错误。
注意:如果没有正确设置符号,也会出现这些错误,因此这不是出现这些错误的唯一原因。

0:000> !eeheap -gc 
Failed to load data access DLL, 0x80004005 
Verify that 1) you have a recent build of the debugger (6.2.14 or newer)            
2) the file mscordacwks.dll that matches your version of clr.dll is                
in the version directory            
3) or, if you are debugging a dump file, verify that the file                
mscordacwks_<arch>_<arch>_<version>.dll is on your symbol path.            
4) you are debugging on the same architecture as the dump file.                
For example, an IA64 dump file must be debugged on an IA64                 machine.

应该使用什么工具?

在此场景中捕获进程转储的最佳方法是使用32位版本的工具,如Debug Diag或adplus+cdb(用于windows的32位调试工具)。您可以在64位系统上安装这些工具的32位版本。在下一个版本的Debug Diag中,您将能够使用64位版本的Debug Diag捕获32位转储。
我之前提到过,可以使用Vista+上的任务管理器捕获内存转储,但如果在64位计算机上执行此操作,则会获得64位转储。但是,您可以使用位于C:\ Windows\SysWOW64\taskmgr.exe中的32位任务管理器来获取32位转储。如果要验证是否正在运行32位版本的任务管理器,可以检查任务管理器本身中是否列出了taskmgr.exe和*32。当然还有其他工具。

使用正确的“位”调试器加载转储文件是否重要?

是的,要读取32位转储,您应该使用32位windbg(在32位或64位操作系统上),而对于64位转储,您调试转储的计算机需要是运行64位windbg的64位计算机。

计算器

当你在调试,需要做一些从十六进制到十进制的简单转换,一些整数计算你不需要切换到calc.exe,你可以只使用windbg的表达式计算器。
假设你得到了一个十六进制的大小,比如说2e903000,您可以转换并执行如下计算:

0:000> ?2e903000
Evaluate expression: 781201408 = 2e903000

所以0x2e903000字节是781201408字节

获取MB数,你只要运行

0:000> ?2e903000/0n1024/0n1024
Evaluate expression: 745 = 000002e9

即除以0n1024两次(十进制数字以0n为前缀),得到745 MB。

聊天客户端

好吧,它不是一个真正的聊天客户端,但是,当您处于远程会话中时,即当有人用.server远程退出了调试会话,并且您想向他们指出一些东西时,您可以用*开始注释,调试器不会解析它,但是连接到远程的其他方会看到它。

0:000>***嘿,看看这个奇怪的东西。。。

与其他应用程序交互

如果需要对命令的输出进行一些处理,并且不想执行将结果复制到记事本、运行外部应用程序等过程,.shell命令是必须尝试的。如果您需要在调试器中对返回的结果进行一些处理,这将特别有用。

0:000> .shell -i - -ci "~* kb 2000" FIND /c "mscorwks!ThreadpoolMgr::WorkerThreadStart"
7
.shell: Process exited

 

大多数开发人员都意识到PDB文件有助于您进行调试,但仅此而已。如果你不知道PDB文件是怎么回事,不要觉得很糟糕,因为虽然有文档在那里,但它分散在周围,而且大部分是为编译器和调试器编写器准备的。虽然编写编译器和调试器非常酷和有趣,但这可能不是你的工作。
我想做的是把每个在微软操作系统上进行开发的人都必须知道的PDB文件放在一个地方。这些信息也适用于本机开发人员和托管开发人员,不过我将提到一个特定于托管开发人员的技巧。我将从讨论PDB文件存储和内容开始。由于调试器使用PDB文件,我将详细讨论调试器如何为二进制文件找到正确的PDB文件。最后,我将讨论调试器在调试时如何查找源文件,并向您展示一个与调试器如何查找源代码相关的常用技巧。
在我们开始之前,我需要定义两个重要的术语。在开发计算机上执行的生成是私有生成。在生成计算机上完成的生成是公共生成。这是一个重要的区别,因为调试在本地生成的二进制文件很容易,总是公共生成导致问题。
所有开发人员需要知道的最重要的事情是:PDB文件和源代码一样重要!很多公司没有人能找到在生产服务器上运行的构建的PDB文件。如果没有匹配的PDB文件,您的调试挑战几乎是不可能的。通过大量的努力,可以在没有正确的PDB文件的情况下找到问题,但是如果您首先拥有正确的PDB文件,它将为您节省大量的资金。

正如Visual Studio的开发经理约翰坎宁安(John Cunningham)在2008年的PDC会议上所说的,“热爱、保持和保护您的pdb”。至少,每个开发商店都必须设置一个符号服务器。您还可以在Windows调试工具帮助文件中阅读Symbol Server文档本身。请查看这些资源以了解有关详细信息的更多信息。简而言之,符号服务器存储所有公共构建的pdb和二进制文件。这样,无论哪个构建有人报告崩溃或问题,您都有与调试器可以访问的公共构建完全匹配的PDB文件。Visual Studio和WinDBG都知道如何访问符号服务器,如果二进制文件来自公共构建,调试器将自动获取匹配的PDB文件。
大多数人在将PDB文件放入Symbol服务器之前还需要做一个准备步骤。这一步是在您的公共PDB文件上运行源服务器工具,这被称为源索引。索引嵌入了版本控制命令,以拉动在特定公共构建中使用的确切源文件。因此,在调试公共生成时,您永远不必担心找到该生成的源文件。如果您是一个一人或两人的团队,则有时可以不使用源服务器步骤。
我听到过一些团队对设置符号服务器的抱怨,他们的软件太大太复杂。我不得不承认,当我听到别人说“我的团队功能失调”时,你的软件绝不可能比微软做的任何事情都更大、更复杂。它们将所有产品的每个版本的索引和存储到一个符号服务器中。这意味着从Windows到Office,再到SQL,再到游戏,所有东西都存储在一个中心位置。我的猜测是,在雷德蒙德的34号楼只不过是存储所有这些文件的SAN驱动器,而大楼里的每个人都支持这些SAN。能够在微软内部调试任何东西都是非常令人惊奇的,而且您不必担心符号或源代码(前提是您对源代码树拥有适当的权限)。

在关键基础设施讨论结束后,让我来看看PDB中的内容以及调试器如何找到它们。PDB文件的实际文件格式是一个严格保密的秘密,但是微软提供了api来为调试器返回数据。本机C++ PDB文件包含相当多的信息:

  • 公共、私有和静态函数地址
  • 全局变量名称和地址
  • 参数和局部变量的名称和偏移在堆栈上的位置
  • 由类、结构和数据定义组成的类型数据
  • 帧指针省略(FPO)数据,这是在x86上进行本机堆栈遍历的关键
  • 源文件名及其行

一个.NET PDB只包含两部分信息:源文件名及其行和本地变量名。所有其他信息都已在.NET元数据中,因此无需在PDB文件中重复相同的信息。
将模块加载到进程地址空间时,调试器使用两条信息来查找匹配的PDB文件。第一个显然是文件名。如果加载ZZZ.DLL,调试器将查找ZZZ.PDB。最重要的部分是调试器如何知道这是与此二进制文件完全匹配的PDB文件。这是通过嵌入在PDB文件和二进制文件中的GUID完成的。如果GUID不匹配,您肯定不会在源代码级别调试模块。
.NET编译器和本机的链接器将此GUID放入二进制和PDB中。既然编译的行为创建了这个GUID,那么请停下来考虑一下。如果您有昨天的版本,但没有保存PDB文件,您是否可以再次调试二进制文件?不!这就是为什么保存每次生成的PDB文件如此重要的原因。因为我知道你在想,所以我会继续回答你脑海中已经形成的问题:不,没有办法更改GUID。
但是,可以查看二进制文件中的GUID值。使用Visual Studio附带的命令行工具DUMPBIN,可以列出可移植可执行文件(PE)的所有部分。要运行DUMPBIN,请从程序菜单中打开Visual Studio 2008命令提示符,因为需要设置PATH环境变量才能找到DUMPBIN EXE。顺便说一下,如果你对DUMPBIN向你展示的信息感兴趣的话,我强烈推荐Matt Pietrek在2002年2月和2002年3月的MSDN杂志上关于PE文件的权威文章。

DUMPBIN有很多命令行选项,但是显示构建GUID的选项是/HEADERS。Pietrek文章将解释输出,但对我们来说重要的一点是调试目录输出:

Debug Directories
Time Type Size RVA Pointer
——– —— ——– ——– ——–
4A03CA66 cv 4A 000025C4 7C4 Format: RSDS,
  {4B46C704-B6DE-44B2-B8F5-A200A7E541B0}, 1,
C:junkstuffHelloWorldobjDebugHelloWorld.pdb

根据调试器如何确定正确匹配的PDB文件的知识,我想讨论调试器在哪里查找PDB文件。调试时,通过查看Visual Studio模块窗口的符号文件列,可以看到所有这些命令都是自己加载的。首先搜索的是加载二进制文件的目录。如果PDB文件不存在,调试器将查找嵌入PE文件中的调试目录中的硬编码生成目录。如果查看上面的输出,就会看到完整的路径C:JUNKSTUFFHELLOWORLDOBJDEBUGHELLOWORD.PDB。(用于生成.NET应用程序的MSBUILD任务实际上生成到OBJ<CONFIG>目录,并仅在成功生成时将输出复制到调试或发布目录。)如果PDB文件不在前两个位置,并且在计算机上为设置了符号服务器,则调试器将在符号服务器缓存目录中查找。最后,如果调试器在Symbol Server缓存目录中找不到PDB文件,它将在Symbol Server本身中查找。此搜索顺序就是为什么本地生成和公共生成部件从不冲突的原因。
调试器搜索PDB文件的方式对几乎所有要开发的应用程序都很好。PDB文件加载更有趣的地方是那些需要您将程序集放入全局程序集缓存(GAC)中的.NET应用程序。我特别关注的是你的SharePoint和你对web部件的残忍,但是还有其他的。对于本地计算机上的私有构建,使用起来很容易,因为调试器将在构建目录中找到PDB文件,如我上面所述。当您需要在另一台计算机上调试或测试私有构建时,就会出现问题。

在另一台机器上,我看到许多开发人员在使用GACUTIL将程序集放入GAC后所做的工作是打开一个命令窗口,在C:windowssembly中搜索程序集在磁盘上的物理位置。尽管将来可能会发生更改,但为任何CPU编译的程序集实际上都位于如下目录中:C:WindowsassemblyGAC_MSILExample1.0.0.0__682bc775ff82796a

示例是程序集的名称,1.0.0.0是版本号,682bc775ff82796a是公钥标记值。推导出实际目录后,可以将PDB文件复制到该目录,调试器将加载该目录。
如果你现在对像这样挖掘GAC感到有点不安,你应该,因为它是不受支持和脆弱的。有一个更好的方法,似乎几乎没人知道DEVPATH。其思想是,您可以在.NET中设置一些设置,它会将您指定的目录添加到GAC中,因此您只需将程序集和它的PDB文件放入该目录,这样调试就容易得多。仅在开发计算机上设置DEVPATH,因为存储在指定目录中的任何文件都不会像在真正的GAC中那样进行版本检查。
顺便说一下,如果你在任何一个互联网搜索引擎中搜索DEVPATH,其中一个最热门的条目是Suzanne Cook的一篇过时的博客文章,她说微软正在摆脱DEVPATH。这不再是事实。和其他博客一样,看看苏珊博客上的日期:2003年。这相当于互联网时代的1670年。
要使用DEVPATH,首先要创建一个目录,该目录对所有帐户都具有读访问权限,至少对开发帐户具有写访问权限。此目录可以位于计算机上的任何位置。第二步是设置一个系统范围的环境变量DEVPATH,其值是您创建的目录。有关DEVPATH的文档并没有说明这一点,但是在执行下一步之前,请先设置DEVPATH环境变量。

要告诉.NET运行时已设置DEVPATH,需要根据应用程序的需要将以下内容添加到APP.CONFIG、WEB.CONFIG或MACHINE.CONFIG中:

<configuration>
   <runtime>
      <developmentMode developerInstallation=”true”/>
   </runtime>
</configuration>

一旦打开开发模式,您就会知道在进程中缺少DEVPATH环境变量或者您设置的路径不存在,如果应用程序在启动时死机,错误消息表示完全非直观的:“注册表的无效值”。如果确实要在MACHINE.CONFIG中使用DEVPATH,请格外小心,因为计算机上的每个进程都会受到影响。在一台机器上导致所有.NET应用程序失败不会赢得办公室里的许多朋友。
每个开发人员需要知道的关于PDB文件的最后一项是源文件信息如何存储在PDB文件中。对于已在其上运行源索引工具的公共构建,存储是将源文件获取到所设置的源缓存中的版本控制命令。对于私有构建,存储的是编译器用来生成二进制文件的源文件的完整路径。换句话说,如果在C:FOO中使用源文件MYCODE.CPP,那么PDB文件中嵌入的是C:FOOMYCODE.CPP。这可能是你已经怀疑过的,但我只是想说清楚。
理想情况下,所有的公共构建都会立即自动被源代码索引并存储在符号服务器中,因此如果您甚至不必再考虑源代码在哪里的话。然而,一些团队在PDB文件中不做源索引,直到他们做了冒烟测试或其他祝福,看看该构建是否足够好让其他人使用。这是一个非常合理的方法,但是如果必须在生成的源代码被索引之前调试生成,最好将该源代码拉到与生成计算机使用的驱动器和目录结构完全相同的位置,否则在源代码级别调试时可能会遇到一些问题。虽然Visual Studio调试器和WinDBG都有设置源搜索目录的选项,但我发现很难找到正确的方法。
对于较小的项目,这没有问题,因为您的源代码总是有足够的空间。生活更困难的地方是在更大的项目上。如果你有30MB的源代码,而你的C:驱动器上只剩下20MB的磁盘空间,你该怎么办?有办法控制PDB文件中存储的路径不是很好吗?
虽然我们不能编辑PDB文件,但是有一个简单的技巧可以控制PDB文件中的路径:SUBST.EXE。subt所做的是将路径与驱动器号相关联。如果您将源代码下拉到C:DEV并执行“SUBST R:C:DEV”,那么如果您键入“DIR C:DEV”,R:drive现在将在其顶层显示相同的文件和目录。您还将在资源管理器中看到R:drive作为新驱动器。也可以通过将驱动器映射到资源管理器中的共享目录来实现驱动器到路径的影响我个人更喜欢subt方法,因为它不需要机器上的任何共享。虽然有些人认为可以通过<DRIVE>$共享,但有些组织禁用了该功能。
在生成计算机上要做的是设置一个启动项,该启动项执行特定的SUBST命令。当生成系统帐户登录时,它将提供新的驱动器号,这是您进行生成的位置。通过对PDB文件中嵌入的驱动器和根目录的完全控制,在测试机上设置源代码所需做的全部工作就是在任何需要的地方将其拉下来,并使用与生成机使用的驱动器号相同的驱动器号执行子命令。现在调试器中不再考虑源匹配了。