0 前言
作为Windows程序员,对PDB文件肯定不陌生,不管是平时的本机调试还是现网Crash的Dump文件分析,如有PDB文件的辅助,可以大大提高调试效率。
本文主要从PDB的文件格式,PDB的生成与存储以及PDB的使用几个方面介绍PDB的相关知识。
1 PDB简介
PDB(Program DataBase)是Vsiual Studio构建工程时生成的用于调试的符号文件。主要包含了如下信息:
- 全局变量
- 局部变量
- 函数名和它们的入口点地址
- 帧指针省略记录FPO(Frame Pointer Omission)
- 源码行号
下面是与PDB相关的几个VS工程配置选项:
-
指定调试信息格式,设置为/Zi或/ZI时会生成PDB文件:
Project -> Properites -> Configuration Properties -> C/C++ -> General -> Debug Information Format
-
指定编译器(Compiler)生成的PDB文件名,默认为VCx0.pdb,其中x代表当前VS的版本,例如VS2010默认PDB名为VC100.pdb:
Project -> Properites -> Configuration Properties -> C/C++ -> Output Files -> Program Database File Name
指定连接器(Linker)是否生成调试信息,YES为生成,NO为不生成。
Project -> Properites -> Configuration Properties -> Linker -> Debugging -> Generate Debug Info
-
指定连接器(Linker)生成的PDB文件名,默认为TargetName.pdb,其中TargetName为连接后生成的文件名。
Project -> Properites -> Configuration Properties -> Linker -> Debugging -> Generate Program Database File
1.1 PE文件格式
连接器在连接时将PDB文件名及路径信息存放到可执行文件的"CODEVIEW"调试目录中。该目录包含如下PDB相关信息:
偏移地址 | 数据类型 | 注释 |
---|---|---|
+0h | dword | "RSDS"签名 |
+4h | GUID | 16字节的GUID |
+14h | dword | "age" |
+18h | byte string | 以0结尾的UTF-8格式文件路径和文件名 |
下面我们就通过PEView来查看一个PE文件。
上图中对应的数据信息如下所示:
偏移地址 | 数据类型 | 注释 |
---|---|---|
+0h | dword | RSDS |
+4h | GUID | E6AC12193E049A47AD6BD95644534850 |
+14h | dword | 1 |
+18h | byte string | d:\Test\MyTest\Output\MyTest\MyTest.pdb |
1.2 PDB文件格式
PDB文件格式并未公开,但是Microsoft提供了API来读取PDB中的数据,可以参考CCI开源项目。
虽然官方未公开格式,但是The RSDS pdb format对PDB的文件格式做了较详细的介绍。
PDB的文件格式类似于磁盘的文件系统,每个磁盘会被划分成很多个大小一样的扇区,文件中的数据就存放在不同的扇区中,而且无需保证这些扇区在磁盘上是连续的。PDB文件用page进行划分,类似于扇区,stream就类似于文件,stream directory类似于文件目录。
下面我们就用winhex来看下PDB中到底存放了那些信息吧。
1.2.1 PDB头部结构
上图中PDB头部信息解析如下:
偏移地址 | 数据类型 | 数据 | 备注 |
---|---|---|---|
+0h | byte string | Microsoft C/C++ MSF 7.00 | PDB版本信息,不同版本字符串长度不一样 |
+1Ah | byte | EOF | 标志PDB版本信息字符串结束 |
+1Bh | byte string | DS | 签名 |
+1Eh | byte | null-terminator | 终结符 |
+20h | dword | 00000400h(1024) | page的大小(单位:字节) |
+24h | dword | 00000002h(2) | 未知 |
+28h | dword | 00000293h(659) | 整个文件有占用多少个page |
+2ch | dword | 00000AE0h(2784) | stream directory占用大小(单位:字节) |
+30h | dword | 00000000h(0) | 未知 |
+34h | dword | 00000291h(657) | stream directory指针的页地址(单位:页) |
1.2.2 PDB Stream Directory Pointers信息结构
根据上述信息可以计算出stream directory指针地址为A4400h,对应信息如下:
从图中可以看出存放了三个stream的页地址指针,分别为028Eh, 028Fh, 0290h,之所以是三个是因为stream directory占用了0AE0(2784)字节,需要三个page才能存放。
注意:这里的指针并不需要是连续的,只是这里刚好是连续的而已。
1.2.3 PDB Stream Directory信息结构
Stream Directory的数据结构如下所示:
偏移地址 | 数据类型 | 注释 |
---|---|---|
+0h | dword | stream的个数 |
+4h | dword | stream大小(单位:字节),忽略0和-1(FFFFFFFF) |
+?h | dword | stream的指针数组 |
以028Eh为例,乘以400h之后得到A3800h,该地址对应信息如下:
从上图可以看出该stream directory中存在002Fh(47)个stream,而接下来的47个dword分别表示每个stream的大小。因此page指针将从A38C0h处开始。
前3个stream信息解析如下:
Streams | size | page pointers |
---|---|---|
Stream1 | 0000001Ch | 00000004h |
Stream2 | 00000072h | 0000028Ch |
Stream3 | 00050FD0h | 0000028Dh,00000134h,... |
1.2.4 PDB Stream信息结构
不同的stream包含不同信息,这里主要介绍下存放PDB文件真实性相关的信息。一般Stream2包含了这些信息。通过计算可以得到Stream2在文件中的地址为A3000h,对应地址的信息如下:
红框中记录的就是前面通过PEView工具查看CODEVIEW中的GUID,在它前面的dword就是对应的age字段。
2 PDB的生成与存储
一般在本机编译工程的时候,PDB文件会在本机指定的目录生成,如果要到其它计算机上调试,需要同时拷贝二进制文件和PDB文件。
而对于发布版本,我们一般都会在编译机上编译,并且将生成的PDB文件保存到符号服务器,这样不管在哪台计算机上都可以通过配置符号服务器路径来拉取到对应的PDB文件。
那么PDB文件在符号服务器是如何存储的呢?调试工具又是如何定位到对应的PDB文件的呢?答案就是SysStroe。
下面我们来详细介绍下整个过程。
2.1 SysStore
SymStore(symstore.exe)是用于创建符号存储的工具。包含在Windows调试工具包中。
SymStore按照某种格式存储符号,使调试器可以通过时间戳、映像大小(对于.dbg或可执行文件)、签名和寿命(.pdb文件)来查找符号。 使用符号存储而不是常规的符号存储格式的好处是,所有符号都可以在同一个服务器上进行存储或引用,而调试器不需要知道具有哪些产品对应的符号。
注意,不同版本的.pdb符号文件(例如公有和私有符号)不能保存在相同的符号服务器中,因为他们具有相同的签名和寿命。
2.1.1 SymStore事务
- SymStore的每次调用都被记录为事务。事务分两种:添加和删除。
- 当创建符号存储时,会在服务器根目录下创建一个"000admin"目录。它为每个事务都包含一个文件以及作为日志记录的server.txt和history.txt。
- server.txt包含当前服务器上的所有事务的列表。
- history.txt包含所有历史事务,按时间排序。
- 每次添加或删除符号文件都会创建一个新的事务号,然后在000admin中创建一个以事务号为名字的文件。文件中包含所有在该事务中添加到符号存储服务器的文件或文件指针的列表。如果某个事务被删除,SymStore会读取它的事务文件来决定哪些文件和指针需要删除。
- add和del选项用来指定是否进行事务添加或删除。在添加操作中包含/p选项表示添加一个指针,忽略/p指定添加实际的符号文件。
- 也可以分两步来创建符号存储。
- 使用SymStore和/x选项来创建一个索引文件。
- 使用SymStore 和/y选项通过索引文件中的信息来创建真正的存储文件或指针。
- 这是一种很有用的技术。比如:
- 方便符号存储由于某些原因丢失后的重建,只要索引文件还存在。
- 当包含符号文件的计算机和创建符号存储的计算机之间网络连接速度很慢时。可以在符号文件所在机器上创建索引文件,并将该文件传输到第二台机器,在第二台机器上创建符号存储。
- SymStore参数的完整列表,请参阅SymStore命令行选项。
注意: SymStore不支持多个用户同时发起事务。建议指派一个用户作为符号存储的"管理员"并处理所有的add和del事务。
2.1.2 事务示例
- 在\sampledir\symsrv中添加Windows Server 2003 build 3790符号指针。
symstore add /r /p /f \\BuildServer\BuildShare\3790free\symbols\*.*
/s \\sampledir\symsrv /t "Windows Server 2003" /v "Build 3790 x86 free"
/c "Sample add"
symstore add /r /p /f \\BuildServer\BuildShare\3790Chk\symbols\*.*
/s \\sampledir\symsrv /t "Windows Server 2003" /v "Build 3790 x86 checked"
/c "Sample add"
- 将\largeapp\appserver\bins中的项目符号文件添加到\testdir\symsrv。
symstore add /r /f \\largeapp\appserver\bins\*.* /s \\testdir\symsrv /t "Large Application" /v "Build 432" /c "Sample add"
- 下面是使用索引文件的示例。
- 创建\largeapp\appserver\bins\上的符号文件的索引文件,并放在第三台计算机\hubserver\hubshare上。 使用/g选项来指定前缀"\largeapp\appserver"可能会在未来改变。
- 将\largeapp\appserver机器上的所有符号文件移动到\myarchive\appserver上,然后通过使用\hubserver\hubshare\myindex.txt索引文件来创建符号存储。
symstore add /r /p /g \\largeapp\appserver /f \\largeapp\appserver\bins\*.* /x \\hubserver\hubshare\myindex.txt
symstore add /y \\hubserver\hubshare\myindex.txt /g \\myarchive\appserver /s \\sampledir\symsrv /p /t "Large Application" /v "Build 432" /c "Sample Add from Index"
- 删除之前的事务中添加的文件,假设事物ID为0000000096。
symstore del /i 0000000096 /s \\sampledir\symsrv
2.1.3 压缩文件
- SymStore可以通过两种方法使用压缩文件:
- 使用SymStore和/p选项来保存符号文件的指针。SymStore完成之后,压缩指针指向的文件。
- 使用SymStore和/x选项来创建索引文件。SymStore完成之后,压缩索引文件中列出的文件。然后,使用SymStore和/y选项(也可以使用/p)来将这些文件或指针保存到符号存储。(在这个操作中无需解压文件。)
- 符号服务器会负责在适当的时候解压这些文件。
- 如果使用SymSrv作为符号服务器,必须使用随Microsoft Windows SDK发布的compress.exe 工具来进行压缩。压缩文件扩展名的最后一个字符必须是下划线(例如module1.pd_或module2.db_)。详细信息,请参阅SymSrv。
2.1.4 server.txt和history.txt 文件
添加事务之后,会在server.txt和history.txt中添加一些信息用于未来查询使用。下面是一次添加事务在server.txt和history.txt中产生的一行记录:
0000000096,add,ptr,10/09/99,00:08:32,Windows XP,x86 fre 1.156c-RTM-2,Added from \\mybuilds\symbols,
每行记录用逗号分隔。下面是每条记录中各个字段的含义。
字段 | 说明 |
---|---|
0000000096 | 事务ID号,由SymStore创建 |
add | 事务类型,这个字段可以是add或del。 |
ptr | 添加的是文件或指针。该字段可以是file或ptr。 |
10/09/99 | 事务发生的日期。 |
00:08:32 | 事务开始的时间。 |
Windows XP | 产品 |
x86 fre | 版本(可选)。 |
Added from | 注释(可选)。 |
Unused | (保留为以后使用)。 |
下面是事务文件0000000096中的一些示例行。每行都记录了添加到目录中的文件或指针的目录和位置。
canon800.dbg\35d9fd51b000,\\mybuilds\symbols\sp4\dll\canon800.dbg
canonlbp.dbg\35d9fd521c000,\\mybuilds\symbols\sp4\dll\canonlbp.dbg
certadm.dbg\352bf2f48000,\\mybuilds\symbols\sp4\dll\certadm.dbg
certcli.dbg\352bf2f1b000,\\mybuilds\symbols\sp4\dll\certcli.dbg
certcrpt.dbg\352bf04911000,\\mybuilds\symbols\sp4\dll\certcrpt.dbg
certenc.dbg\352bf2f7f000,\\mybuilds\symbols\sp4\dll\certenc.dbg
如果使用del事务来撤销以前的add事务,这些行会从server.txt中删除,并添加下面这行到history.txt中:
0000000105,del,0000000096
删除事务的字段说明如下。
字段 | 说明 |
---|---|
0000000105 | 事务ID号,SymStore创建。 |
del | 事务类型。该字段可以是add或del。 |
0000000096 | 被删除的事务。 |
2.1.5 符号存储格式
SymStore使用文件系统本身作为数据库。它创建一个大的目录树,目录名基于符号文件的时间戳、签名、寿命和其他数据。
例如一些不同的acpi.dbg添加到服务器之后,目录可能像下面一样:
Directory of \\mybuilds\symsrv\acpi.dbg
10/06/1999 05:46p <DIR> .
10/06/1999 05:46p <DIR> ..
10/04/1999 01:54p <DIR> 37cdb03962040
10/04/1999 01:49p <DIR> 37cdb04027740
10/04/1999 12:56p <DIR> 37e3eb1c62060
10/04/1999 12:51p <DIR> 37e3ebcc27760
10/04/1999 12:45p <DIR> 37ed151662060
10/04/1999 12:39p <DIR> 37ed15dd27760
10/04/1999 11:33a <DIR> 37f03ce962020
10/04/1999 11:21a <DIR> 37f03cf7277c0
10/06/1999 05:38p <DIR> 37fa7f00277e0
10/06/1999 05:46p <DIR> 37fa7f01620a0
本例中,acpi.dbg的查找路径可能像这样:\\mybuilds\symsrv\acpi.dbg\37cdb03962040
。
在查找目录中可能有三个文件:
- acpi.dbg,如果保存了该文件。
- file.ptr,如果保存的是指针,它包含实际的符号文件路径。
- refs.ptr,包含当前被添加到符号存储中的,具有该时间戳和映像大小的acpi.dbg当前位置的完整列表。
2.1.5.1 只包含file.ptr和refs.ptr的情况
\\mybuilds\symsrv\acpi.dbg\37cdb03962040
目录显示如下:
10/04/1999 01:54p 52 file.ptr
10/04/1999 01:54p 67 refs.ptr
file.ptr文件包含字符串"\\mybuilds\symbols\x86\2128.chk\symbols\sys\acpi.dbg"
。因为该目录中没有acpi.dbg 文件,所以调试器会试图从\\mybuilds\symbols\x86\2128.chk\symbols\sys\acpi.dbg
中去查找。
refs.ptr中的内容仅被SymStore使用,而不会被调试器使用。这个文件记录该目录中发生过的所有事务的记录。refs.ptr中的一个示例行可能像这样:
0000000026,ptr,\\mybuilds\symbols\x86\2128.chk\symbols\sys\acpi.dbg
这表明指向\\mybuilds\symbols\x86\2128.chk\symbols\sys\acpi.dbg
的指针由事务"0000000026"添加。
一些符号文件在各种产品、版本或特殊产品中保持不变。比如:msvcrt.pdb。列举\\mybuilds\symsrv\msvcrt.pdb
目录显示只有两个版本的msvcrt.pdb被添加到了符号服务器上:
Directory of \\mybuilds\symsrv\msvcrt.pdb
10/06/1999 05:37p <DIR> .
10/06/1999 05:37p <DIR> ..
10/04/1999 11:19a <DIR> 37a8f40e2
10/06/1999 05:37p <DIR> 37f2c2272
但是,列举\\mybuilds\symsrv\msvcrt.pdb\37a8f40e2
目录表明refs.ptr中有一些指针。
Directory of \\mybuilds\symsrv\msvcrt.pdb\37a8f40e2
10/05/1999 02:50p 54 file.ptr
10/05/1999 02:50p 2,039 refs.ptr
\\mybuilds\symsrv\msvcrt.pdb\37a8f40e2\refs.ptr
的内容如下:
0000000001,ptr,\\mybuilds\symbols\x86\2137\symbols\dll\msvcrt.pdb
0000000002,ptr,\\mybuilds\symbols\x86\2137.chk\symbols\dll\msvcrt.pdb
0000000003,ptr,\\mybuilds\symbols\x86\2138\symbols\dll\msvcrt.pdb
0000000004,ptr,\\mybuilds\symbols\x86\2138.chk\symbols\dll\msvcrt.pdb
0000000005,ptr,\\mybuilds\symbols\x86\2139\symbols\dll\msvcrt.pdb
0000000006,ptr,\\mybuilds\symbols\x86\2139.chk\symbols\dll\msvcrt.pdb
0000000007,ptr,\\mybuilds\symbols\x86\2140\symbols\dll\msvcrt.pdb
0000000008,ptr,\\mybuilds\symbols\x86\2140.chk\symbols\dll\msvcrt.pdb
0000000009,ptr,\\mybuilds\symbols\x86\2136\symbols\dll\msvcrt.pdb
0000000010,ptr,\\mybuilds\symbols\x86\2136.chk\symbols\dll\msvcrt.pdb
0000000011,ptr,\\mybuilds\symbols\x86\2135\symbols\dll\msvcrt.pdb
0000000012,ptr,\\mybuilds\symbols\x86\2135.chk\symbols\dll\msvcrt.pdb
0000000013,ptr,\\mybuilds\symbols\x86\2134\symbols\dll\msvcrt.pdb
0000000014,ptr,\\mybuilds\symbols\x86\2134.chk\symbols\dll\msvcrt.pdb
0000000015,ptr,\\mybuilds\symbols\x86\2133\symbols\dll\msvcrt.pdb
0000000016,ptr,\\mybuilds\symbols\x86\2133.chk\symbols\dll\msvcrt.pdb
0000000017,ptr,\\mybuilds\symbols\x86\2132\symbols\dll\msvcrt.pdb
0000000018,ptr,\\mybuilds\symbols\x86\2132.chk\symbols\dll\msvcrt.pdb
0000000019,ptr,\\mybuilds\symbols\x86\2131\symbols\dll\msvcrt.pdb
0000000020,ptr,\\mybuilds\symbols\x86\2131.chk\symbols\dll\msvcrt.pdb
0000000021,ptr,\\mybuilds\symbols\x86\2130\symbols\dll\msvcrt.pdb
0000000022,ptr,\\mybuilds\symbols\x86\2130.chk\symbols\dll\msvcrt.pdb
0000000023,ptr,\\mybuilds\symbols\x86\2129\symbols\dll\msvcrt.pdb
0000000024,ptr,\\mybuilds\symbols\x86\2129.chk\symbols\dll\msvcrt.pdb
0000000025,ptr,\\mybuilds\symbols\x86\2128\symbols\dll\msvcrt.pdb
0000000026,ptr,\\mybuilds\symbols\x86\2128.chk\symbols\dll\msvcrt.pdb
0000000027,ptr,\\mybuilds\symbols\x86\2141\symbols\dll\msvcrt.pdb
0000000028,ptr,\\mybuilds\symbols\x86\2141.chk\symbols\dll\msvcrt.pdb
0000000029,ptr,\\mybuilds\symbols\x86\2142\symbols\dll\msvcrt.pdb
0000000030,ptr,\\mybuilds\symbols\x86\2142.chk\symbols\dll\msvcrt.pdb
这表明多次版本构建都用到了相同的msvcrt.pdb并保存到\mybuilds\symsrv。
2.1.5.2 同时包含文件和附加指针的情况
下面是同时包含文件和附加指针的目录示例:
Directory of E:\symsrv\dbghelp.dbg\38039ff439000
10/12/1999 01:54p 141,232 dbghelp.dbg
10/13/1999 04:57p 49 file.ptr
10/13/1999 04:57p 306 refs.ptr
refs.ptr包含的内容如下:
0000000043,file,e:\binaries\symbols\retail\dll\dbghelp.dbg
0000000044,file,f:\binaries\symbols\retail\dll\dbghelp.dbg
0000000045,file,g:\binaries\symbols\retail\dll\dbghelp.dbg
0000000046,ptr,\\sampledir\bin\symbols\retail\dll\dbghelp.dbg
0000000047,ptr,\\sampledir2\bin\symbols\retail\dll\dbghelp.dbg
可以看出,事务43、44和45向服务器添加了同样的文件,事务46和47添加了指针。如果事务43、44和45被删除,dbghelp.dbg 文件也会从目录中删除掉。这时,目录包含的内容如下:
Directory of e:\symsrv\dbghelp.dbg\38039ff439000
10/13/1999 05:01p 49 file.ptr
10/13/1999 05:01p 130 refs.ptr
新的file.ptr包含:"\\sampledir2\bin\symbols\retail\dll\dbghelp.dbg"
新的refs.ptr包含:
0000000046,ptr,\\sampledir\bin\symbols\retail\dll\dbghelp.dbg
0000000047,ptr,\\sampledir2\bin\symbols\retail\dll\dbghelp.dbg
只要refs.ptr的最后一项为指针,就会存在file.ptr文件,并且包含相关文件的路径。当refs.ptr最后一项为文件时,目录中不会存在file.ptr。因此,任何对refs.ptr中最后一项的删除操作都可能使得file.ptr被创建、删除或修改。
3 PDB的使用
一般用PDB调试主要是为了能够进行源码调试,因此在使用SysStroe存储之前还需要将PDB与源码关联起来,具体操作请参阅Source Indexing。
3.1 VS中如何配置
可以通过以下VS工程属性来配置符号路径。
Tools -> Options -> Debugging -> Symbols
3.2 Windbg中如何配置
可以通过以下方式设置符号路径。
-
File -> Symbol File Path
,快捷键为Ctrl+S
。 - 通过.sympath命令添加符号路径。比如将
C:\Symbols
目录添加到符号路径列表。
.sympath+ C:\Symbols
3.3 PDB的加载顺序
PDB的加载顺序如下:
- 首先是二进制文件所在的目录;
- 其次是二进制文件中硬编码的构建目录,例如obj\debug*.pdb;
- 再次是根据symbol server的设置,在本地的symbol server的cache中查找;
- 最后到远程的symbol server中查找。