2023年1月

虽然我希望.NET PDB文件与本地PDB文件处理方式相同,但我们在这件事上没有任何选择,因为事情就是这样。我相信微软的调试器团队多年来听到过很多类似帕特里克的评论。也许我们会在未来的Visual Studio版本中看到所有问题都得到解决。


帕特里克非常幸运能够通过VPN远程调试到客户机器中。我相信你们大多数读者都会喜欢这样的场景!对于我们大多数人来说,只要让用户/管理员打开日志并将输出发送给我们,就和登月旅行一样困难。


本机二进制PDB文件非常敏感。正如我在关于PDB文件的原始文章中指出的,本机PDB文件包含以下数据(如果它们没有被剥离):

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

如果你把这些文件交给一个客户,你已经给了他们所有缺少源文件的东西。事实上,使用完整的本地PDB文件,使用公共DBGHELP符号API编写一个工具来编写一个重新创建头文件的工具并不难。如果你重视你的工作,你永远不想让你的本地PDB文件泄漏相关信息。

另一方面,.NET PDB文件只包含以下信息:

  • 源文件名及其行
  • 局部变量名

那么调试器如何知道所有关于.NET类型的信息呢?从二进制文件中的元数据来看,不需要在.NET PDB文件中复制元数据。.NET的优点是“自描述对象”,因此您需要更多有关二进制文件中类型的信息。元数据是如何使用反射加载以前从未见过的二进制文件并开始实例化其类型。正如您可以想象的,这意味着由于相同的元数据,将.NET二进制文件直接反编译回您选择的首选语言并不太难。选择.NET的易用性意味着你必须放弃一些东西,因为没有免费的午餐。


因为.NET PDB文件不包含任何敏感的内容,我说给他们自由!在Patrick的例子中,我将把.NET PDB文件给客户,这样他就可以对本机和.NET进行远程调试。所有源文件都加载在运行Visual Studio UI的本地计算机上,因此您不会放弃这些文件。


先发制人的评论攻击:是的,如果你做了一些疯狂的蠢事,比如在你的构建路径中嵌入域名和登录密码,这将显示在你给客户的.NET PDB文件中。我说的是正常的情况<big smile!>


实际上,我建议您为.NET PDB文件创建一个安装程序,以便可以将它们安装到与二进制文件相同的目录中。调试器(本地或远程)总是首先在加载二进制文件的目录中查找匹配的.NET PDB文件。要求您的客户手动将每个.NET PDB文件复制到适当的目录是一个灾难的秘诀。安装程序是好事,尽管Windows安装程序API有点麻烦。


让我将创建.NET PDB文件安装程序的“推荐”改为“强烈推荐”。您是否注意到开发机器上未处理的异常与测试或客户机器上的异常之间有一个有趣的区别?当您查看开发机器上未处理的异常时,您会看到调用堆栈以及一些非常有用的信息:调用堆栈中每个项的源和行。在开发计算机上,本地生成PDB文件与二进制文件位于同一目录中,异常类中的.NET StackTrace字段会自动读取它们以获取源和行信息。当你有一个写300行方法的同事时,准确的行在调用堆栈中有很大的帮助。


通过.NET PDB文件安装程序,您可以让复制未处理异常的客户安装符号,这样当您将异常转储到日志时,您就得到了源代码和行代码。这就是我所说的快速调试!


请记住,抛出的每个异常都使用StackTrace类,因此在计算调用堆栈时,由于符号查找,性能会受到一些影响。正如您可以猜到的,有许多变量会影响性能,所以我不能给您一个确切的数字,但事实上,您将在堆栈中拥有确切的源代码和代码行,这意味着您将更快地调试问题,这最终是性能的最终改进。

粗略察看一 下.pdb 文件,会发现在其起始位置存放的是这样一个字符串“Microsoft C/C++ program database 2.00”。可以看出 PDB 是 Program Database 的首字母缩写。在 MSDN 中或 Internet 上搜索一下有关 PDB 内部结构的信息,你会发现没有任何有用的信息,唯一例外的是,在 微软的基础知识文章中,微软申明此种格式是它有的(Microsoft Corporation, 2000d)。就连 Windows 的老大 Matt Pietrek 也承认:
“ PDB符 号 表 的 格 式 并 没 有 公 开 的 文 档 。( 就 连 我 也 不 知 道 其 确 切 的 格 式 , 唯 一 知 道 的 是,它会随着 Visual C++ 的 更 新 而 更 新 。)”( Pietrek 1997a )
或许,pdb 格式会随着 Visual C/C++一起更新,不过针对当前版本的 Windows 2000 我 可以确切的告诉你 PDB 符号文件的结构。这或许是首次公开的 PDB 格式文档。但首先,还 是让我们检查一下.dbg 和.pdb 文件是如何链接到一起的。
Windows 2000 的.dbg 文件的一个显著特性是:它们包含的数据很少,几乎可以忽略它 们的 CodeView子节。下面示例给出了 ntsokrnl.exe 的.dbg 文件所包含的整个 CodeView数据, 只有区区 32 字节。

Address | 00 01 02 03-04 05 06 07 : 08 09 0A 0B-0C 0D 0E 0F | 0123456789ABCDEF
---------|-------------------------:-------------------------|-----------------
00006590 | 4E 42 31 30-00 00 00 00 : 20 7D 23 38-54 00 00 00 | NB10.... }#8T...
000065A0 | 6E 74 6F 73-6B 72 6E 6C : 2E 70 64 62-00 00 00 00 | ntoskrnl.pdb...

通常,子节总是以一个 CV_HEADER 结构开始,该结构中包 含 CodeView 的版本标识。这一次,该版本标识是 NB10MSDN(Microsoft 2000a)没能告 诉我们有关这个特殊版本的更多信息: “ NB10 ,可执行文件的这一标识表示,其调试信息保存在独立的 PDB文件中。相应的格式还有NB09或NB11。”( MSDN Library—April 2000\Specifications\Technologies and Languages\Visual C++ 5.0 Symbolic Debug Information Specification\Debug Information Format )
我并不知道 NB11 格式的内部细节,不过 PDB 格式和前面讨论的 NB09 格式一样几乎 什么也没有。第一句话很明确的说明了为什么 NB10 数据块是如此的小。所有相关的信息都 被移到了独立的文件中了,因此这个 CodeView 子节的主要作用就是提供指向实际数据的链 接。如示例 1-8 所暗示的,在 ntoskrnl.pdb 文件中一定可以找到实际的符号信息。
CV_HEADER 结构是自解释的。其后的两个成员的偏移量分别为:0x8 和 0xC,它们的 名字分别为:dSignature 和 dAge,在.dbg 和.pdb 文件链接的过程中它们将扮演重要角色。 dSignature 是一个 32 位的 UNIX 风格的时间戳,它保存了调试信息构建的日期和时间(自 01-01-1970 以来逝去的秒数)。w2k_img.dll 提供了两个函数:imgTimeUnpack()和 imgTimePack()用来将 dSignature 和 Windows 风格的时间格式进行相互的转化。我还不是非 常清楚 dAge 成员的确切含义。目前知道的是:dAge 成员的初始值为 1,每次修改 PDB 数 据后其值就会增一。dSignature 和 dAge 共同构成一个 64 位的 ID,调试器可以使用它来验 证给定的 PDB 文件是否与它引用的.dbg 文件相匹配。PDB 文件在它的一个数据流中包含着 两个值的一个副本,因此调试器可以拒绝处理不相匹配的.dbg/.pdb 文件。
 
无论你何时遇到格式未知的数据结构,你应该做的第一件事就是使用十六进制 Dump 浏览器察看这些结构。本书附带的w2k_dump.exe可很好的完成这一工作。通过检查Windows 2000 PDB 文件,如 ntoskrnl.pdb 或 ntfs.pdb,你会发现这些文件拥有如下一些共同特性: 

  • 这些文件似乎都被划分为多个大小固定的块,一般情况下,每个块的大小为 0x400 字节。
  • 某些块包含一长串 1,但偶而会被一小段连续的 0 打断。
  • 文件中的信息并不必须是连续的。有时,数据会在块的边界处突然结束,但又会在 文件的其它地方继续开始。
  • 有些数据块会在文件中反复出现。

CodeView 的 NB10 子节 
typedef
struct _CV_NB10 //PDB reference{
CV_HEADER Header;
DWORD dSignature;
//seconds since 01-01-1970 DWORD dAge; //1++ BYTEabPdbName[];//zero-terminated} CV_NB10,*PCV_NB10, **PPCV_NB10;#define CV_NB10_ sizeof(CV_NB10)