当前位置:Gxlcms > 数据库问题 > .pdb 文件的内部结构

.pdb 文件的内部结构

时间:2021-07-01 10:21:17 帮助过:10人阅读

struct _CV_NB10 // PDB reference { CV_HEADER Header; DWORD dSignature; // seconds since 01-01-1970 DWORD dAge; // 1++ BYTE abPdbName[]; // zero-terminated } CV_NB10, *PCV_NB10, **PPCV_NB10; #define CV_NB10_ sizeof(CV_NB10)

终弄清这些复合文件的典型特点花费了我不少时间。复合文件是将一个小型文件系统 打包到一个单一文件中。“文件系统”这一修饰词可很好的解释上面得到的观察结果: 

  • 一个文件系统会将磁盘细化为大小固定的扇区,一组扇区又构成一个文件(此文件 件大小可变)。由扇区构成的文件可位于磁盘的任何位置上,并不要求必须是连续 的。文件/扇区的对应关系定义在文件目录中。
  •  一个复合文件将一个原始磁盘文件细化为大小固定的页,一组页构成一个流 (stream),并且流的大小可变。由页构成的复合文件可位于原始文件中的任何位 置,这些页并不必须是连续的。流和页的对应关系定义在流目录中。

很显然,文件系统中的格式和复合文件格式差不多是一一对应的,只需简单的将“扇区” 替换为“页”,将“文件”替换为“流(Stream)”。对照文件系统可以很好的解释为什么 PDB 文件是按大小固定的块组织起来的,同时还解释了为什么这些块并不一定都是连续的。不过, 一页中几乎都是二进制 1 的块又代表什么呢?实际上,这种类型的数据在文件系统中是很常 见的。为了跟踪磁盘上已用和还未使用的扇区,很多文件系统都维护了一个二进制位的分配 数组,数组中的每个二进制位对应文件系统中的一个扇区(或一簇扇区)。如果一个扇区未 使用,其对应的二进制位就将被设置为 1。当文件系统为文件分配空间时,它就会扫描这个 分配位数组,以找出未使用的扇区。在将扇区加入到文件中后,文件系统就将对应得分配位 设为 0。复合文件的页和流也采用了相似的处理方式。一长串的二进制 1 代表还未使用的页, 二进制 0 表示对应的页已分配给某个流。
 
现在唯一的问题就是为什么有些数据块会在 PDB 文件中反复出现。同样的事情也出现 在磁盘的扇区上。当文件系统中的一个文件被多次重写时,每个写操作可能会使用不同的扇 区来存放数据。因此,磁盘上某些空扇区中可能会包含旧数据的副本。这在文件系统中不算 是什么问题。如果扇区在分配数组中标识为未使用,那么该扇区上有什么数据就无所谓了。 这样的扇区很快就会在另一个文件中被使用,其原有内容将被新的数据覆盖掉。对应文件系 统的这一特性,我们再来看复合文件,这意味着我们观察到的那些重复的页应该是修改留下 的副本。可以安全地忽略它们;我们唯一需要关心的就是那些在流目录(stream directory) 中被引用到的页。
 
现在已经介绍完了PDB文件的基本结构,接下来我们将检查构成PDB文件的那些基本的 数据块。列表1-23给出了PDB头部的布局。在PDB_HEADER的开始位置有一个文件字符串 给出了当前PDB的版本标识。该标识字符串以EOF字符(ASCII码为0x1A)结束。在其后还 有一个附加的数字:0x0000474A,如果将该数字解释为字符串的话,则为:”JG\0\0”。或许 这代表PDB格式的初设计者吧。嵌入的EOF字符有一个很好的作用:如果普通用户在控制 台窗口中使用type ntoskrnl.pdb,那么将不会显示其后的数据,显示出来的信息只是:Microsoft C/C + + program database 2.00。Windows 2000所有的符号文件都是PDB 2.00 版。显然,曾经存在过PDB 1.00格式,而且其结构似乎与现在的有很大不同。

#define PDB_SIGNATURE_200 \         "Microsoft C/C++ program database 2.00\r\n\x1AJG\0" 
 
#define PDB_SIGNATURE_TEXT 40 
 
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 
typedef struct _PDB_SIGNATURE     
{
BYTE abSignature [PDB_SIGNATURE_TEXT+4]; // PDB_SIGNATURE_nnn
} PDB_SIGNATURE, *PPDB_SIGNATURE, **PPPDB_SIGNATURE;
#define PDB_SIGNATURE_ sizeof (PDB_SIGNATURE) // ----------------------------------------------------------------- #define PDB_STREAM_FREE -1 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - typedef struct _PDB_STREAM
{
DWORD dStreamSize; // in bytes, -1 = free stream
PWORD pwStreamPages;
// array of page numbers
} PDB_STREAM, *PPDB_STREAM, **PPPDB_STREAM;
#define PDB_STREAM_ sizeof (PDB_STREAM) // ----------------------------------------------------------------- #define PDB_STREAM_MASK 0x0000FFFF
#define PDB_STREAM_MAX (PDB_STREAM_MASK+1) #define PDB_STREAM_DIRECTORY 0
#define PDB_STREAM_PDB 1
#define PDB_STREAM_TPI 2
#define PDB_STREAM_DBI 3
#define PDB_STREAM_PUBSYM 7 typedef struct _PDB_ROOT
{
WORD wCount; // < PDB_STREAM_MAX
WORD wReserved;
// 0
PDB_STREAM aStreams [];
// stream #0 reserved for stream table
} PDB_ROOT, *PPDB_ROOT, **PPPDB_ROOT;
#define PDB_ROOT_ sizeof (PDB_ROOT) #define PDB_PAGES(_r) \
((PWORD) ((PBYTE) (_r) \
+ PDB_ROOT_ \
+ ((DWORD) (_r)->wCount * PDB_STREAM_))) // ----------------------------------------------------------------- #define PDB_PAGE_SIZE_1K 0x0400 // bytes per page
#define PDB_PAGE_SIZE_2K 0x0800
#define PDB_PAGE_SIZE_4K 0x1000
#define PDB_PAGE_SHIFT_1K 10 // log2 (PDB_PAGE_SIZE_*)
#define PDB_PAGE_SHIFT_2K 11
#define PDB_PAGE_SHIFT_4K 12
#define PDB_PAGE_COUNT_1K 0xFFFF // page number < PDB_PAGE_COUNT_*
#define PDB_PAGE_COUNT_2K 0xFFFF
#define PDB_PAGE_COUNT_4K 0x7FFF
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - typedef struct _PDB_HEADER
{
PDB_SIGNATURE 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;
#define PDB_HEADER_ sizeof (PDB_HEADER)

在标识字符串之后偏移量为 0x2C 处有一个名为 dPageSize 的 DWORD 类型的值,它代 表的是复合文件中每个页所占的字节数。合法的值可以是:0x0400(1KB)、0x800(2KB) 和 0x1000(4KB)。wFilePages 成员记录了 PDB 文件使用的页的总数。将 wFilePages 与 dPageSize 相乘即可得到该 PDB 文件的大小。wStartPage 是一个从零开始的页码,它指向第 一个数据页。该页的字节偏移量可由该页的页码乘以每页的大小得到。通常的值为:页号为 9 的 1KB 页(字节偏移量为 0x2400),页号为 5 的 2KB 页(字节偏移量为 0x2800)或者页 号为 2 的 4KB 页(字节偏移量为 0x2000)。在 PDB_HEADER 和第一个数据页之间的空间 保留给分配位数组,并总是从第二个页开始。这意味着,如果页大小为 1 或 2KB,则 PDB 文件使用 0x10000(64K)个分配位,每位对应 0x2000 字节(8KB)的页,如果页大小为 4KB,则使用 0x8000(32K)个分配位,每位对应 0x1000 字节(4KB)的页。以此类推, 这意味着,在页大小为 1KB 的情况下,PDB 文件可容纳 64MB 数据,在页大小为 2KB 或 4KB 的情况下,PDB 文件可容纳 128MB 数据。
 
PDB_HEADER 后的 RootStream 和 wRootPages[]成员记录了 PDB 文件中流目录的位 置。就像前面提到的,PDB 文件是由一组长度可变的流构成的,这些流中才包含有实际的 数据。流的位置及其内容是由一个单一的流目录管理的。流目录自身也存储在一个流中。我 称这个特殊的流为“Root Stream”。Root Stream 中保存着流目录(该流目录可能位于 PDB 文件的任何位置)。PDB_HEADER 的 Rootstream 和 wRootPages[]成员提供了 Root Stream 的 位置和大小。PDB_STREAM 子结构的 dStreamSize 成员给出了流目录占用的页的数目,这 些页的首地址保存在 wRootPages[]数组中,这些页包含实际的数据。

现在让我们用一个小例子来说明这一点。示例 1-9 给出了 ntoskrnl.pdb 的 PDB_HEADER 的十六进制 Dump 的部分内容。这里引用到的值由下划线标识出来。显然,这个 PDB 文件 使用的页的大小为 0x400 字节(1KB),一共使用了 0x02D1(721)个页,这样该文件的大 小则为 0xB4400(十进制 738,304)。使用 dir 命令可验证这个大小是正确的。Root Stream 的 大小为 0x5B0 字节(1456 字节),由于每个页的大小为 0x400 字节(1KB),则意味着 wRootPages[]数组中包含两项,分别位于偏移量为 0x3C 和 0x3E 处。数组中的两项内容都是 页码,需要将此页码与页大小相乘才能得到对应的字节偏移量。此处,该字节偏移量为: 0xB2000 和 0xB2800。
 
上面后一行给出的计算结果是 ntoskrnl.pdb 文件的流目录所占用的两组文件页的首地 址,其范围分别为:0xB2000----0xB23FF 和 0xB2800----0xB29AF。示例 1-10 给出了这些范 围的部分内容。
技术图片

 

 技术图片

 

 流目录由两个部分构成:一个 PDB_ROOT 结构的文件头部分,该结构定义在列表 1-24 中,另一部分是由 16 位页码构成的数组。PDB_ROOT 结构中的 wCount 成员记录了保存在 PDB 文件中的流的数目。aStream[]数组包含多个 PDB_STREAM 结构(参见列表 1-23),每 个 PDB_STREAM 结构代表一个流,紧随 aStream[]数组之后在就是页码数组。在示例 1-10 中,流的个数为 8,对应的偏移量为 0xB2000,该位置已用下划线标识出。随后的 8 个 PDB_STREAM 结构分别给出了这 8 个流的大小:0x5B0、0x3A、0x38、0x402A9、0x0、0x4004、 0x19EB4 和 0x4DF3C。这些值也都以下划线标识出。在 1KB 页模式下,流的大小为:0x2、 0x1、0x1、0x101、0x0、0x11、0x68 和 0x138,这样可计算出这些流总共占用了 0x2B6 个 页。在 PDB_STREAM 数组之后,第一个以下划线标识出来的值是页码列表中的第一个页码。 这里每个页码占用 2 个字节,这里需要考虑的是,页目录被属于其他部分的一个页截断了, 故页目录随后的偏移量为应为:0xB2044+0x400+(0x2B6*2)=0xB29B0,示例 1-10 很好的展 示了这一点。

PDB 的流目录结构 
#define
PDB_STREAM_DIRECTORY 0
#define PDB_STREAM_PDB 1
#define PDB_STREAM_TPI 2
#define PDB_STREAM_DBI 3
#define PDB_STREAM_PUBSYM 7 // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - typedef struct _PDB_ROOT
{
WORD wCount; // < PDB_STREAM_MAX
WORD wReserved;
// 0
PDB_STREAM aStreams [];
// stream #0 reserved for stream table
} PDB_ROOT, *PPDB_ROOT, **PPPDB_ROOT;
#define PDB_ROOT_ sizeof (PDB_ROOT) #define PDB_PAGES(_r) \
((PWORD) ((PBYTE) (_r) \
+ PDB_ROOT_ \
+ ((DWORD) (_r)->wCount * PDB_STREAM_)))

要找到给定的流所对应的页码需要一定的技巧,因为页目录除了流的大小之外,没有提 供任何信息。如果你对 3 号流感兴趣,那么你必须计算流 1 和流 2 所占用的页的数目,以获 取 3 号流在页码数组中的起始索引。一旦定位了指定流的页码列表,读取流中的数据就很简 单了。只需要遍历页码列表,将列表中的每个页码和每页的大小相乘,就可获得此页码对应 页的文件偏移量,然后从该偏移量处开始读取页的内容,反复如此,直到到达流的结束处, 就可读取整个流的内容了。猛地一看,解析一个 PDB 文件似乎非常费劲。但从另一个角度 看却十分简单-----因为这要比解析一个.dbg 文件简单的多。PDB 格式的这种清晰的随机访问 机制,将读取一个流的任务简化为读取连续的大小固定的页。这种优雅的数据访问机制让我 很是吃惊。
 
当更新一个已存在的 PDB 文件时,PDB 格式的优势就非常明显了。将使用连续的结构 体的数据插入到一个文件中,意味着将移动大量的原有数据。PDB 文件从文件系统借鉴来 的随机访问架构允许以小的开销完成删除或插入数据的操作,就像文件系统中的文件可以 很容易的修改一样。当一个流在增大时,只需改动流目录或则收缩页的边界。这种非常重要 的特性大为提高了 PDB 文件更新的灵活性。微软在基本知识库中正式提供这样一片文章: “信息:PDB 和 DBG 文件-----它们是什么以及它们是如何工作的”: “ .PDB扩展了“ Program database ”架构。此种文件用来存放调式信息,这种格式随 Visual C++ 1.0 一起引入。在将来, .PDB 文件还将包含其它的项目状态信息。格式改变的一 个重要动机是为了允许程序调试版的增量链接,第一次改变随 Visual C++ 2.0 引入。” ( Microsoft Corporation 2000e )
 
 
现在 PDB 文件的内部结构已经很清晰了,下一个问题是如何识别这些流的具体内容。 在检查完 PDB 文件的各个方面后,我得出这样一个结论:每一种流都用于特定的目的。第 一个流似乎总是包含一个流目录,第二个流包含用于验证该 PDB 文件是否与其关联的.dbg 文件相匹配的信息。例如,该流中包含的 dSignature 和 dAge 成员应该和 NB10 CodeView 节 中的对应成员一致。

.pdb 文件的内部结构

标签:含义   DBName   结果   fine   文档   rmi   包含   array   项目   

人气教程排行