Windows程序调试系列文章——使用VC++生成调试信息
使用VC++生成调试信息
ZhangTao,zhangtao.it@gmail.com, 译自 “Generating debug information with Visual C++”,Oleg Starodumov
引子
当我们使用调试器来调试程序时,我们希望能够单步调试到源代码中,在代码中设置断点,观察变量的值(包括用户自定义的复杂类型的值)。但是可执行文件只含有原始的字节数据——机器指令和操作系统执行程序时所使用的头信息和表信息。操作系统加载并运行可执行文件后,它根据不同的需求使用不同片段的内存(栈、堆)存放数据,其中的存放的依然是原始的字节数据。那么,调试器如何知道当前CPU指令对应哪一行代码?如何知道堆栈中的地址对应哪一个函数的局部变量?答案是“调试信息”,调试信息是高级编程语言和运行程序的原始字节数据之间的桥梁。名词解释
位置(location): 在不同的情况有不同的含义。对于函数而言,是函数首字节的地址;对于全局和静态变量而言,是内存中变量的首字节;对局部变量和函数参数而言,通常是该变量的首字节相对于函数堆栈的预先定义的基址的偏移。另外,其他类型的位置也可能出现,如:寄存器、TLS slot(参见:http://www.blogcn.com/u2/38/94/silannyukun/blog/37069531.html)、元数据标记(metadata token, 参见http://naoku.net/blogs/framesniper/archive/2005/04/12/1910.aspx)。
FPO (frame pointer omission): 帧指针省略,FPO用来链接CodeView或PDB符号。它在编译器没有用EBP寄存器生成标准堆栈桢(a standard stack frame) 的地方帮助调试器查找函数的参数和本地变量。调试信息的类型
我们只讨论在Intel X86平台上的现有的由微软提供的调试器。
信息的类型 |
描述 |
公共函数和变量 |
用于描述在多个的编译单元(源代码文件)中可见的函数和变量,调试信息保存每个函数和变量的位置(location)和名称。 |
私有函数和变量 |
用于描述除公共函数和变量以外的所有函数和变量,包括静态函数、静态和局部变量、函数参数),调试信息保存每个函数和变量的位置、大小和名称。 |
源文件和代码行信息 |
用于将每一行代码映射到可执行文件的某个位置上。当然,某些代码行不能做映射,如注释行,这样的代码行在调试信息中不做体现。 |
类型信息 |
用于存储每一个函数和变量的类型信息。对于变量或函数参数,类型信息能够告诉调试器它是整型还是字符串类型,或是用户自定义的类型。对于函数,类型信息记载了参数的个数、调用转换和返回值的类型。 |
FPO信息 |
对于做了FPO优化的函数,调试信息保存了一些数据来帮助调试器确定函数堆栈帧的大小,甚至在帧指针无效时也能工作。 如果没有FPO信息,调试器无法正确显示被优化的程序的调用堆栈。 |
编辑和继续执行信息 |
用于帮助Visual Studio IDE在调试时实现编辑和继续执行的功能 |
调试信息格式
现在来探索调试信息是如何存储的。在过去的十年中,微软开发工具使用了几种不同的格式来包装调试信息。这里我们讨论COFF、CodeView和应用的最广泛的PDB(Program Database)格式。在讨论每种格式时,我们从下列几个特性着手:
- 哪些类型的调试信息可以通过该格式保存?
- 调试信息究竟保存在哪里(在可执行文件中,还是单独的一个文件)?
- 该格式是否有文档说明?
COFF
COFF是这里要涉及的所有格式中最古老的一种,它只能保存三种调试信息: 公共函数和变量,源文件和代码行信息,FPO信息。COFF总是保存在可执行文件中,不能够单独保存在其他文件中。该格式的文档说明参见:微软可移植可执行和通用对象文件格式规范.
CodeView
CodeView是较COFF更新的而且更复杂的一种格式,它可以存储除编辑和继续执行信息外的所有类型的调试信息。CodeView通常保存在可执行文件中,它也可从可执行文件中导出到一个单独的文件(.DGB文件)。CodeView文档不全,其文档可以在MSDN中的VC++5.0符号调试信息规范(Symbolic Debug Information Specification)中找到。
Program Database 程序数据库
这是三种中最新的一种调试信息格式,可以存储所有类型的调试信息(包括编辑和继续执行信息),也支持增量编译(其余两种格式不支持)。程序数据库信息保存在一个单独的.PDB文件中。遗憾的是,微软没有提供程序数据库格式的文档,只提供特殊的编程接口DbgHelp 和DIA来访问它。目前,程序数据库格式有两个版本,第一版(PDB2.0)为VC6.0所用,第二版(PDB 7.0)被Visual Studio.NET采用。PDB 7.0不能向上兼容,也就是说:VC6.0不能读取PDB 7.0格式。
三种格式对比如下:
格式 |
是否有文档 |
存储 |
公共函数和变量 |
私有函数和变量 |
源文件和代码行信息 |
类型信息 |
FPO 信息 |
编辑和继续执行信息 |
COFF |
有 |
可执行文件中 |
+ |
- |
+ |
- |
+ |
- |
CodeView |
部分 |
可执行文件中 或.DBG文件中 |
+ |
+ |
+ |
+ |
+ |
- |
Program Database |
无 |
.PDB文件中 |
+ |
+ |
+ |
+ |
+ |
+ |
生成调试信息
构造(build)过程
一个典型的可执行文件的构造过程包含两步:编译和链接。首先,编译器分析源文件,生成机器指令(保存在.obj对象文件中);然后链接器将所有可用的对象文件合并到最终的可执行文件。在对象文件之外,链接器也会用到库文件(库文件也是其他一些对象文件的汇集)。整个构造过程如下图:
如果我们想要为可执行文件生成调试信息,也得经历两步:首先,编译器为每一个源文件创建调试信息;然后,链接器合并由编译器创建得调试信息,如下图:
缺省状态下,编译器和链接器不会产生调试信息。因此我们必须通过编译和链接选项来要求编译器和链接器生成调试信息,我们也可以指定生成哪些类型得调试信息,使用什么调试信息格式,将调试信息保存在什么地方。
接下来,我讨论具体得编译器和链接器选项。
Visual C++ 6.0
编译器 Compiler
有下列选项:
/Zd 生成COFF格式的调试信息,保存在对象文件中
/Z7 生成CodeView格式的调试信息,保存在对象文件中
/Zi 生成程序数据库格式的调试信息,保存在.PDB文件中
/ZI 与 /Zi 基本一致, 唯一不同的是调试信息中包含编辑和继续执行信息
缺省时,/Zi 和 /ZI 选项生成的PDB文件名为VC60.PDB,也可以使用/Fd指定文件名。
选项 |
格式 |
存储文件 |
内容 |
/Zd |
COFF |
.OBJ |
|
/Z7 |
CodeView |
.OBJ |
|
/Zi |
Program Database |
.PDB |
|
/ZI |
Program Database |
.PDB |
|
链接器Linker
下列选项可用:
/debug 告诉链接器生成调试信息,如果该选项不使用,则其他所有选项都无效
/debugtype 指定调试信息格式,可能的用法包括:
/debugtype:coff COFF格式。注意:该选项下,调试信息中不包含源文件和代码行信息
/debugtype:cv CodeView或程序数据库格式。究竟是哪一种格式,由/pdb决定
/debugtype:both 同时使用COFF格式和CodeView/程序数据库格式
/pdb 决定是CodeView还是程序数据库格式。/pdb:none 表示CodeView格式,/pdb:filename(如/pdb:myexe.pdb)表示使用程序数据库格式,文件名为myexe.pdb。在/debugtype:coff 选项下,/pdb 选项无效。
/pdbtype 该选项只在一个或多个对象文件或库文件的调试信息也保存在一个单独的PDB文件中。/pdbtype:sept 选项可以使得调试信息各自保存在各自的PDB文件中,这样可以加快链接速度,不利的是调试信息分散,调试时需要多个PDB文件。相对的,/pdbtype:con 选项使得所有调试信息都保存在与可执行文件对应的最终的PDB文件中。
为便于理解各个选项的配对使用,请见下表:
/debugtype |
/pdb |
格式 |
存储 |
coff |
/pdb:none (无效) |
COFF |
在可执行文件中 |
coff |
/pdb:filename (无效) |
COFF |
在可执行文件中 |
cv |
/pdb:none |
CodeView |
在可执行文件中 |
cv |
/pdb:filename |
Program Database |
.PDB 文件 |
both |
/pdb:none |
COFF and CodeView |
在可执行文件中 |
both |
/pdb:filename |
COFF and Program Database |
COFF 信息在可执行文件中, 程序数据库信息在 .PDB 文件中 |
Visual C++.NET (2002 and 2003)
编译器 Compiler
下列选项可用:
/Z7 生成CodeView格式的调试信息,保存在对象文件中
/Zd, /Zi 和 /ZI都表示生成程序数据库格式的调试信息,保存在.PDB文件中. 不同之处是调试信息的内容(见下表)。
缺省时,/Zd,/Zi 和 /ZI 选项生成的PDB文件名为VC70.PDB或VC71.PDB,也可以使用/Fd指定文件名。
注意: VC++.NET 编译器不支COFF。
选项 |
格式 |
存储 |
内容 |
/Z7 |
CodeView |
.OBJ |
|
/Zd |
Program Database |
.PDB |
|
/Zi |
Program Database |
.PDB |
|
/ZI |
Program Database |
.PDB |
|
链接器Linker
下列选项可用:
/debug告诉链接器生成调试信息,如果该选项不使用,则其他所有选项都无效。调试信息的格式总是程序数据库格式,保存在PDB文件中。缺省的,链接器使用可执行文件名生成PDB文件名。PDB文件名可包含所有调试信息的变量内容。
/pdb 指定PDB文件名.
/pdbstripped 允许链接器生成附加的PDB文件,该文件的内容限定于:
- 公共函数和变量
- FPO信息
注意: COFF 和 CodeView 格式不被 VC++.NET链接器支持。
静态库的调试信息
由于没有连接过程,静态库的调试信息的生成比可执行文件要简单的多。不考虑编译器版本(VC6 或 VS.NET),我们可以使用(/Zd, /Z7, /Zi, /ZI)中一个选项通知编译器为静态库生成调试信息。
关键问题是将调试信息保存在什么地方。当使用/Z7或/Zd选项时,调试信息保存在.LIB文件中;当使用/Zi或/ZI选项时,调试信息保存在.PDB文件中(当然可以使用/Fd指定文件名)。
调试信息对可执行文件的大小的影响
调试信息对可执行文件的大小的影响,决定于存储调试信息的地方,也间接的决定于所使用的格式。
COFF和CodeView格式下,调试信息保存在可执行文件中,因此可执行文件的大小将显著增长(通常要增长一倍以上,甚至更大)。
程序数据库格式下,调试信息单独保存,对可执行文件的大小几乎没有影响。在这种情况下,可执行文件需要保存一个头信息方便调试器对调试信息进行定位,因此需要增长大约几百个字节。
要避免可执行文件的膨胀,我们需要在使用/debug 同时,将/opt:ref 选项改为opt:noref。这样做,有一个另外的结果就是关闭了链接器的大小优化。如果要恢复大小优化,需要改回/opt:ref。
.DBG 文件
使用一个小工具——Rebase——可以将CodeView格式的内容从可执行文件中导出,存入到DBG文件中。Rebase包含在Visual Studio中。除了用于导出DBG文件外,它还有其他的一些用途。如果用于导出DBG文件,其命令行格式为:
rebase –b BaseAddr –x SymbolDir [-p] ExeName
选项 |
描述 |
-b BaseAddr |
指定可执行文件的基地址,如果你不想更改基地址,就指定当前可执行文件所使用的地址 |
-x SymbolDir |
制定存放.DBG文件的目录, 使用“.”表示当前目录 |
-p |
如果该选项被使用,DBG文件只包含公共函数和变量和FPO信息 |
例如:下面的命令行从DLL中导出调试信息到当前目录下的DBG文件中: rebase –b 0x60000000 –x . MyDll.dll
调试器和调试信息的格式
通用的调试器支持的格式如下:
调试器 |
COFF |
CodeView |
Program Database (2.0) |
Program Database (7.0) |
Visual Studio.NET |
- |
+ |
+ |
+ |
Visual C++ 6.0 |
+ |
+ |
+ |
- |
WinDbg 6.3 |
+ |
+ |
+ |
WinDbg 6.3 部分支持CodeView格式,它只能读取下列信息:
- 公共函数和变量
- FPO信息
- 源文件和代码行信息
它可以单步进入源代码,看到调用堆栈,但无法观察变量的值(因此类型信息不被支持).
操作系统符号文件(symbols)
Windows操作系统所公开的调试系统格式如下:
操作系统 |
格式 |
Windows NT 4.0 |
CodeView (.DBG files) |
Windows 2000 |
CodeView (.DBG files) and Program Database (2.0) |
Program Database (2.0) |
|
Windows XP SP2 |
Program Database (7.0) |
Windows 2003 Server |
Program Database (2.0) |