第一章 Windows 2000对调试技术的支持
翻译:Kendiv ( fcczj@263.net )
更新:Tuesday, May 03, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
CodeView子节(CodeView Subsections)
CodeView是微软自己的调试信息格式。随着微软C/C++编译器和链接器的发展它也经历了很多变化。有些CodeView版本之间的区别非常明显。不过,所有版本的CodeView在它们的开始位置都有一个32位的签名来唯一标识其采用的数据格式。Windows NT 4.0符号文件使用NB09格式,该格式由CodeView 4.10引入。Windows 2000的符号文件包含的则是NB10格式的CodeView数据,这一格式只是引入了独立.pdb文件,这一点我在前面已经提及过。
NB09版的CodeView中的数据被进一步细化为一个目录及其下属的目录项。就像Matt Pietrek在MSJ所发表的文章中所指出的那样, 对于.dbg文件,大多数基本的CodeView结构在SDK的一组示例性头文件中都有定义。如果你安装了SDK示例,你会在\Program Files\Microsoft Platform SDK\Samples\SdkTools\Image\Include目录下发现一组非常有趣的文件。你解析CodeView所需的文件名为:cvexefmt.h和cvinfo.h。很不幸的是,这些文件已经很长时间没有更新了,这些文件的日期还停留在09-07-1994。这些文件最引人注意的是定义在cvexefmt.h中结构体的名字,这些名字都以OMF开始,OMF表示Object Module Format。OMF是16位DOS和Windows的.obj、.lib文件使用的一种标准文件格式。从微软的32位开发工具开始,这一格式由通用对象文件格式(Common Object File Format,COFF)代替了。
尽管今天看来原始的OMF格式已经过时了,但它仍被公认为是一种灵活的文件格式。它的一个目标就是尽可能的减少对内存和磁盘空间的使用量。另一个非常重要的属性是:即使应用程序并不完全了解此种格式的所有部分,也可以成功的解析这个格式的文件。基本的OMF数据结构是一种带有标识的记录,开始处的标识字节给出了记录中所包含的数据的类型。这种设计使得OMF读取器可以在记录之间灵活的移动,以选出它们感兴趣的记录。微软的CodeView格式就采用了这种设计方案,cvexetmt.h中CodeView结构名称的OMF前缀可以说明这一点。尽管CodeView记录和原始的OMF记录一样仅包含了很少的数据,但它仍保留了此种格式的基本特性:不需要理解记录中的所有内容,就可以读取此种格式。
列表1-19给出了多个基本的CodeView结构,它们都来自w2k_img.h。其中的一些定义和cvexefmt.h和cvinfo.h中的定义类似,但它们可以满足w2k_img.dll的所有需求。在所有CodeView数据中都出现了CV_HEADER结构,但格式的版本除外。签名是一个32位的格式版本ID,它类似于CV_SIGNATURE_NB09或者CV_SIGNATURE_NB10。lOffset成员给出了CodeView目录相对于表头地址的偏移量。在Windows NT 4.0的NB09格式的符号文件中,这一偏移量似乎总是8,这表示紧随表头之后的就是CodeView目录结构。Windows 2000符号文件中这一偏移量则为0。在稍后我们将详细讨论这一格式。
#define CV_SIGNATURE_NB 'BN'
#define CV_SIGNATURE_NB09 '90BN'
#define CV_SIGNATURE_NB10 '01BN'
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
typedef union _CV_SIGNATURE
{
WORD wMagic; // 'BN'
DWORD dVersion; // 'xxBN'
BYTE abText [4]; // "NBxx"
}
CV_SIGNATURE, *PCV_SIGNATURE, **PPCV_SIGNATURE;
#define CV_SIGNATURE_ sizeof (CV_SIGNATURE)
// -----------------------------------------------------------------
typedef struct _CV_HEADER
{
CV_SIGNATURE Signature;
LONG lOffset;
}
CV_HEADER, *PCV_HEADER, **PPCV_HEADER;
#define CV_HEADER_ sizeof (CV_HEADER)
// -----------------------------------------------------------------
typedef struct _CV_DIRECTORY
{
WORD wSize; // in bytes, including this member
WORD wEntrySize; // in bytes
DWORD dEntries;
LONG lOffset;
DWORD dFlags;
}
CV_DIRECTORY, *PCV_DIRECTORY, **PPCV_DIRECTORY;
#define CV_DIRECTORY_ sizeof (CV_DIRECTORY)
// -----------------------------------------------------------------
#define sstModule 0x0120 // CV_MODULE
#define sstGlobalPub 0x012A // CV_PUBSYM
#define sstSegMap 0x012D // SV_SEGMAP
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
typedef struct _CV_ENTRY
{
WORD wSubSectionType; // sst*
WORD wModuleIndex; // -1 if not applicable
LONG lSubSectionOffset; // relative to CV_HEADER
DWORD dSubSectionSize; // in bytes, not including padding
}
CV_ENTRY, *PCV_ENTRY, **PPCV_ENTRY;
#define CV_ENTRY_ sizeof (CV_ENTRY)
// -----------------------------------------------------------------
typedef struct _CV_NB09 // CodeView 4.10
{
CV_HEADER Header;
CV_DIRECTORY Directory;
CV_ENTRY Entries [];
}
CV_NB09, *PCV_NB09, **PPCV_NB09;
#define CV_NB09_ sizeof (CV_NB09)
// -----------------------------------------------------------------
typedef 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)
列表1-19. CodeView的数据结构
NB09版的CodeView目录由一个CV_DIRECTORY结构和紧随其后的一个CV_ENTRY数组构成。列表1-19中的CV_NB09结构反映了这种结构特点。CV_NB09结构包含CodeView表头、目录和一个Entry数组。CV_DIRECTORY结构的dEntries成员给出了Entries[]数组的大小。数组中的每个CV_ENTRY都指向CodeView的一个子节,该子节的类型由CV_ENTRY结构的wSubSectionType成员给出。Cvexefmt.h中定义了至少21种子节类型。不过,Windows NT 4.0仅使用其中的3个:sstModule(0x0120)、sstGlobalPub(0x012A)和sstSegMap(0x012D)。通常你会在符号文件发现多个sstModule类型的子节,而sstGlobalPub和sstSegMap类型的子节仅有一个(在一个符号文件中)。就像它们的名字所暗示的,sstGlobalPub表示在该类型的子节中,我们可以找到对应模块的公开的全局符号信息。
列表1-20中给出的imgCvEntry()函数可以方便的按类型查找CodeView目录项。该函数的pc09参数指向一个CV_NB09结构,这意味着,该参数将指向.dbg文件中签名为NB09的CodeView数据块。dType参数用于指定CodeView子节的类型ID(形如sst*),dIndex参数用于表示在多个类型相同的子节中选择哪个子节的实例。因此,仅当dType为sstModule时,dIndex才能被设置为一个非0值。
PCV_ENTRY WINAPI imgCvEntry (PCV_NB09 pc09,
DWORD dType,
DWORD dIndex)
{
DWORD i, j;
PCV_ENTRY pce = NULL;
if ((pc09 != NULL) &&
(pc09->Header.Signature.dVersion == CV_SIGNATURE_NB09))
{
for (i = j = 0; i < pc09->Directory.dEntries; i++)
{
if ((pc09->Entries [i].wSubSectionType == dType) &&
(j++ == dIndex))
{
pce = pc09->Entries + i;
break;
}
}
}
return pce;
}
// -----------------------------------------------------------------
PCV_PUBSYM WINAPI imgCvSymbols (PCV_NB09 pc09,
PDWORD pdCount,
PDWORD pdSize)
{
PCV_ENTRY pce;
PCV_PUBSYM pcp1;
DWORD i;
DWORD dCount = 0;
DWORD dSize = 0;
PCV_PUBSYM pcp = NULL;
if ((pce = imgCvEntry (pc09, sstGlobalPub, 0)) != NULL)
{
pcp = CV_PUBSYM_DATA ((PBYTE) pc09
+ pce->lSubSectionOffset);
dSize = pce->dSubSectionSize;
for (i = 0; dSize - i >= CV_PUBSYM_;
i += CV_PUBSYM_SIZE (pcp1))
{
pcp1 = (PCV_PUBSYM) ((PBYTE) pcp + i);
if (dSize - i < CV_PUBSYM_SIZE (pcp1)) break;
if (pcp1->Header.wRecordType == CV_PUB32) dCount++;
}
}
if (pdCount != NULL) *pdCount = dCount;
if (pdSize != NULL) *pdSize = dSize;
return pcp;
}
列表1-20. imgCvEntry()和imgCvSymbols()函数
CodeView符号
列表1-20底部的imgCvSymbols()函数将返回一个指向首个CodeView符号记录的指针。sstGlobalPub子节包含一个长度确定的CV_SYMHASH表头,紧随其后的是一组长度可变的CV_PUBSYM记录。列表1-21给出了这两个结构的定义。首先,imgCvSymbols()调用imgCvEntry()来找出CV_ENTRY,该结构的wSubSectionType成员将被设置为sstGlobalPub。如果imgCvEntry()返回一个CV_ENTRY结构,则将使用列表1-4底部的CV_PUBSYM_DATA()宏来跳过前导的CV_SYMHASH结构。最后,imgCvSymbols()通过遍历CV_PUBSYM记录列表来统计符号的个数,并使用CV_PUBSYM_SIZE()宏(参见列表1-21)计算每个记录的大小。
CV_PUBSYM列表和OMF对象文件的内容有些相似。前面已经提到过,一个OMF数据流由长度可变的记录组成,每个记录的开始位置的第一个字节为标志,紧随其后的一个WORD存放的是该记录的大小。CV_PUBSYM记录与之类似。CV_PUBSYM记录的开始位置是一个OMF_HEADER结构,该结构由wRecordSize和wRecordType成员构成。可看出这OMF非常相似,不同之处是标志字节之后的存放长度的WORD被扩展为16位。CV_PUBSYM结构的最后一部分是符号名,该符号名采用的是PASCAL格式,这一格式是OMF记录常用的格式。PASCAL格式的字符串的第一个字节用来记录该字符串的长度,其后的8位用来存储字符。和C风格的字符串不同,它并不以0表示结束。在符号名结束之后,CV_PUBSYM还将占有其后的16个位,这样做是为了到达32位边界。这16位由OMF_HEADER结构中的wRecordSize成员使用。要特别注意的是,wRecordSize给出的CV_PUBSYM结构的大小,并不包括wRecordSize自己占用的空间。这也是列表1-21中的CV_PUBSYM_SIZE()宏会在wRecordSize之上再加上sizeof(WORD),以获取整个记录体的大小。
typedef struct _CV_SYMHASH
{
WORD wSymbolHashIndex;
WORD wAddressHashIndex;
DWORD dSymbolInfoSize;
DWORD dSymbolHashSize;
DWORD dAddressHashSize;
}
CV_SYMHASH, *PCV_SYMHASH, **PPCV_SYMHASH;
#define CV_SYMHASH_ sizeof (CV_SYMHASH)
// -----------------------------------------------------------------
typedef struct _OMF_HEADER
{
WORD wRecordSize; // in bytes, not including this member
WORD wRecordType;
}
OMF_HEADER, *POMF_HEADER, **PPOMF_HEADER;
#define OMF_HEADER_ sizeof (OMF_HEADER)
// -----------------------------------------------------------------
typedef struct _OMF_NAME
{
BYTE bLength; // in bytes, not including this member
BYTE abName [];
}
OMF_NAME, *POMF_NAME, **PPOMF_NAME;
#define OMF_NAME_ sizeof (OMF_NAME)
#define S_PUB32 0x0203
#define S_ALIGN 0x0402
#define CV_PUB32 S_PUB32
// -----------------------------------------------------------------
typedef struct _CV_PUBSYM
{
OMF_HEADER Header;
DWORD dOffset;
WORD wSegment; // 1-based section index
WORD wTypeIndex; // 0
OMF_NAME Name; // zero-padded to next DWORD
}
CV_PUBSYM, *PCV_PUBSYM, **PPCV_PUBSYM;
#define CV_PUBSYM_ sizeof (CV_PUBSYM)
#define CV_PUBSYM_DATA(_p) ((PCV_PUBSYM) ((PBYTE) (_p) + CV_SYMHASH_))
#define CV_PUBSYM_SIZE(_p) ((DWORD) (_p)->Header.wRecordSize + sizeof (WORD))
#define CV_PUBSYM_NEXT(_p) ((PCV_PUBSYM) ((PBYTE) (_p) + CV_PUBSYM_SIZE (_p)))
列表1-21. CV_SYMHASH和CV_PUBSYM结构
如果你要扫描CV_PUBSYM流,一般情况下,你会遇到两种类型的记录:S_PUB32(0x0203)或者S_ALIGN(0x0402)。最后一种可以安全的忽略它,因为它仅仅是为了满足32位平台的字节对其方式。S_PUB32记录中则包含实际的符号信息。除符号名之外,记录中的wSegment和dOffset成员也非常有用。wSegment给出了PE文件中包含该符号的Section的索引。使用该索引的负数可以作为IMAGE_SECTION_HEADER数组(此数组位于.dbg文件的开始位置)的索引。dOffset是符号名相对于PE Section的偏移量。这里的符号名指的是与函数的入口点或者全局变量的基地址相关的符号。通常,将dOffset与对应IMAGE_SECTION_HEADER结构的虚拟地址相加即可得到该符号相对于模块基地址的偏移量。不过,如果.dbg文件还包含IMAGE_DEBUG_TYPE_OMAP_TO_SRC和IMAGE_DEBUG_TYPE_OMAP_FROM_SRC子节,那么dOffset必须经过一个附加的转换层。OMAP表的使用方式将在介绍完PDB文件的格式后再来讨论。
符号在CodeView的sstGlobalPub子节中的顺序有些随机。我还不清楚它的深层含义。不过,我可以肯定地说,符号并不是按照section的序号、偏移量或名字的顺序来存放的。不要依赖对符号存放顺序的假设,你必须自己完成符号记录的排序。w2k_img.dll示例库提供了三个默认的符号顺序:按照地址、按照名字(大小写敏感)和按照名字(忽略大小写)。