[Win32]一个调试器的实现(九)符号模型

在接下来的文章中会讲解如何在调试器中显示局部变量和全局变量的类型和值。实现这个功能一定要有调试符号的支持,因为调试符号记录了每个变量的名称,类型,地址,长度等信息。这不是一件轻松的事情,因为你首先要对符号模型有一定的了解。所以本文的主要目的就是介绍DbgHelp中的符号模型。

 

符号模型

这里所说的“符号模型”指的是各种符号之间的逻辑关系,虽然微软定义了各种不同格式的符号文件,但是它们使用的符号模型都是相同的。正因为如此,使用DbgHelp可以读取各种格式的符号文件,而且DIA文档中关于符号模型的那部分内容也适用于DbgHelp,正好弥补了DbgHelp文档中缺少的内容。遗憾的是,这部分内容非常少,点到即止,不足以让人完整地学习符号模型,而且相关文档非常缺乏,或者可以说几乎没有,给使用调试符号的人带来了极大的不便。

 

在开发MiniDebugger的过程中,我自己也遇到了上述的窘境,在完全不了解符号模型的情况下,靠着那一点点可怜的文档,用代码来探索这个模型的原理。过程虽然艰辛,但最终还是对该模型有了一些浅显的了解。不敢说我对该模型的了解完全正确,但我仍然希望和大家分享一下这些知识,希望会对大家有所帮助。

 

微软的符号模型是语言无关的,它支持使用C/C++COM.Net等编写的程序,所以它是一个通用的模型。对于C/C++程序来说,该符号模型有很多额外的特征是用不到的,调试C/C++程序的调试器只会使用它的一个子集。

 

符号文件中的大部分信息都是通过符号(Symbol)来表示的,函数,变量,类型,复合类型的字段,函数参数等等都是符号。符号有两个最基本的属性:符号类型(SymTag)和符号IDSymIndexId)。符号ID是每个符号的唯一标识符,它由符号处理器来管理,同一个符号的ID每次运行都有可能不同,甚至在同一次运行中,同一个符号通过不同方法获取到的ID都有可能不同,所以我们编写调试器的时候不应该依赖符号ID的值,它只是符号的标识符。符号类型指明了符号属于上述的哪种类型,它的取值可以是SymTagEnum枚举中的任意一个。SymTagEnum定义在cvconst.h头文件中,这个头文件属于DIA,在Visual Studio默认的包含文件夹中不存在该文件。如果嫌麻烦,可以自己定义该枚举。它的定义如下: 

 1 enum SymTagEnum {
 2     SymTagNull,
 3     SymTagExe,
 4     SymTagCompiland,
 5     SymTagCompilandDetails,
 6     SymTagCompilandEnv,
 7     SymTagFunction,         //函数
 8     SymTagBlock,
 9     SymTagData,             //变量,函数实参,复合结构字段,枚举值
10     SymTagAnnotation,
11     SymTagLabel,
12     SymTagPublicSymbol,
13     SymTagUDT,              //用户定义类型,例如struct,class和union
14     SymTagEnum,             //枚举类型
15     SymTagFunctionType,    //函数类型
16     SymTagPointerType,      //指针类型
17     SymTagArrayType,        //数组类型
18     SymTagBaseType,         //基本类型
19     SymTagTypedef,          //typedef类型
20     SymTagBaseClass,        //基类类型
21     SymTagFriend,           //友元类型
22     SymTagFunctionArgType,  //函数形参类型
23     SymTagFuncDebugStart, 
24     SymTagFuncDebugEnd,
25     SymTagUsingNamespace, 
26     SymTagVTableShape,
27     SymTagVTable,
28     SymTagCustom,
29     SymTagThunk,
30     SymTagCustomType,
31     SymTagManagedType,
32     SymTagDimension
33 };

C/C++中可以用代码写出来的,“看得见”的类型都作了注释,下文会提及这些类型。而那些编译器定义的,“看不见”的类型,或者与C/C++无关的类型,则几乎不会用到,因此本文不会提及这些类型。

 

代码和数据符号

代码和数据是程序的基本组成部分,SymTagFunctionSymTagData分别表示这两种类型的符号。代码一定在函数中,所以SymTagFunction表示函数符号;而数据有可能是变量,常量,实参,复合类型的字段,枚举类型的值等,SymTagData统一表示这些符号。代码和数据符号的信息都可以通过SYMBOL_INFO结构体来表示,其定义如下:

 1 typedef struct _SYMBOL_INFO {
 2     ULONG SizeOfStruct;
 3     ULONG TypeIndex;
 4     ULONG64 Reserved[2];
 5     ULONG Index;
 6     ULONG Size;
 7     ULONG64 ModBase;
 8     ULONG Flags;
 9     ULONG64 Value;
10     ULONG64 Address;
11     ULONG Register;
12     ULONG Scope;
13     ULONG Tag;
14     ULONG NameLen;
15     ULONG MaxNameLen;
16     TCHAR Name[1];
17 } SYMBOL_INFO,  *PSYMBOL_INFO;

下面解释每一个字段的含义:

 

SizeOfStructSYMBOL_INFO结构体的长度,在使用该结构体调用函数之前必须将这个字段设置为sizeof(SYMBOL_INFO)

 

TypeIndex:符号所属类型的ID,指明了符号的类型。

 

Reserved:保留字段。

 

Index:符号的ID

 

Size:符号的大小。对于函数来说,Size字段是函数所有指令的字节大小;而对于SymTagData类型的符号来说,这个字段的值通常都是0,不能通过它获取变量的大小,而应该通过变量的类型符号来获取。下文会提及这部分内容。

 

ModBase:符号所在模块的基地址。

 

Flags:这个字段的值是几个位标志的组合,描述了符号的一些性质。最常用的一个位标志是SYMFLAG_REGREL,表示符号的地址是寄存器相关的,此时Address字段保存的不是符号的虚拟地址,而是一个偏移量,需要将这个偏移量和某个寄存器的值相加才得到虚拟地址。局部变量和实参符号的Flags字段都有这个标志,它们使用EBP寄存器的值作为基址。其它位标识的含义请参考MSDN文档。

 

Value:符号的值。只有当符号是常量时,这个字段才有效。C/C++程序中含有const修饰符的变量并不视为常量,所以这些符号的Value字段是无效的。枚举类型的值是常量,通过Value字段可以获取每个枚举值的整型值。

 

Address:符号的地址。对于函数和全局变量来说,这个字段的值就是它们的虚拟地址;对于局部变量和参数来说,这个字段的值是一个偏移量,需要将其和EBP寄存器的值相加才得到虚拟地址。

 

Register:如果Flags字段包含SYMFLAG_REGREL,那么Register字段保存的是寄存器的标识符。通常不需要关注这个字段,因为兼容IA32CPU总是使用EBP寄存器来访问局部变量和实参。

 

Scope:这个字段由DIA使用,不必关注这个字段。

 

Tag:符号的类型。它的值是SymTagEnum枚举中的一个。

 

NameLen:符号名称的长度,即Name字段中的字符个数,不包括字符串尾部的0

 

MaxNameLen:指定Name字段的长度。Name字段定义为TCHAR[1],为了获取符号的名称,需要分配足够大的缓冲区,其头部是SYMBOL_INFO结构,剩下的部分用来保存符号名称,MaxNameLen就是用来指定缓冲区剩下部分的长度的。例如: 

1 BYTE buffer[sizeof(SYMBOL_INFO) + sizeof(TCHAR) * 128];
2 
3 PSYMBOL_INFO pSymInfo = (PSYMBOL_INFO)buffer;
4 
5 pSymInfo->MaxNameLen = 128;

 

Name:符号的名称。

 

可以通过SymFromNameSymFromAddr函数来获取某个符号信息,它们都通过SYMBOL_INFO结构体来返回信息。这两个函数的使用方法在前面的文章中已经提到过了。

 

类型符号

类型符号就像是代码和数据符号的元数据,描述了它们的各种属性。在SymTagEnum枚举中,SymTagUDTSymTagEnumSymTagFunctionTypeSymTagPointerTypeSymTagArrayTypeSymTagBaseTypeSymTagTypedefSymTagBaseClassSymTagFriendSymTagFunctionArgType都属于类型符号。代码和数据符号通过类型符号的ID来引用它们所属的类型,SYMBOL_INFO中的TypeIndex就是类型符号的ID。各种类型符号有各自的属性,在DbgHelp中,可以通过SymGetTypeInfo函数来获取类型符号的各种属性。该函数的声明如下:

1 BOOL WINAPI SymGetTypeInfo(
2     HANDLE hProcess,
3     DWORD64 ModBase,
4     ULONG TypeId,
5     IMAGEHLP_SYMBOL_TYPE_INFO GetType,
6     PVOID pInfo
7 );

 

第一个参数是符号处理器的标识符。第二个参数ModBase是类型符号所在模块的基地址,只有该模块具有该类型时才可以获取到它的属性。第三个参数TypeId是类型符号的ID。至于参数GetType是一个IMAGEHLP_SYMBOL_TYPE_INFO枚举,表示要获取哪种属性,同一个枚举值对于不同的类型可能有不同的意义,在下文讲解每种类型符号时会对此进行说明。这里先给出IMAGEHLP_SYMBOL_TYPE_INFO枚举的定义:

 1 typedef enum _IMAGEHLP_SYMBOL_TYPE_INFO {
 2     TI_GET_SYMTAG,
 3     TI_GET_SYMNAME,
 4     TI_GET_LENGTH,
 5     TI_GET_TYPE,
 6     TI_GET_TYPEID,
 7     TI_GET_BASETYPE,
 8     TI_GET_ARRAYINDEXTYPEID,
 9     TI_FINDCHILDREN,
10     TI_GET_DATAKIND,
11     TI_GET_ADDRESSOFFSET,
12     TI_GET_OFFSET,
13     TI_GET_VALUE,
14     TI_GET_COUNT,
15     TI_GET_CHILDRENCOUNT,
16     TI_GET_BITPOSITION,
17     TI_GET_VIRTUALBASECLASS,
18     TI_GET_VIRTUALTABLESHAPEID,
19     TI_GET_VIRTUALBASEPOINTEROFFSET,
20     TI_GET_CLASSPARENTID,
21     TI_GET_NESTED,
22     TI_GET_SYMINDEX,
23     TI_GET_LEXICALPARENT,
24     TI_GET_ADDRESS,
25     TI_GET_THISADJUST,
26     TI_GET_UDTKIND,
27     TI_IS_EQUIV_TO,
28     TI_GET_CALLING_CONVENTION,
29     TI_IS_CLOSE_EQUIV_TO,
30     TI_GTIEX_REQS_VALID,
31     TI_GET_VIRTUALBASEOFFSET,
32     TI_GET_VIRTUALBASEDISPINDEX,
33     TI_GET_IS_REFERENCE,
34     TI_GET_INDIRECTVIRTUALBASECLASS,
35 } IMAGEHLP_SYMBOL_TYPE_INFO;

 

最后一个参数pInfo是一个输出参数,函数的结果通过该参数来返回。GetType参数的值不同,pInfo的实际类型也不同,下文会对此进行说明。

 

虽然文档中没有说明,但是SymGetTypeInfo也可以用来获取代码和数据符号的属性,这反映了所有符号都使用相同或相似的格式来保存。

 

下面针对每个类型符号进行详细说明。(我不会罗列每个类型符号的所有属性,只会对最基本且重要的属性进行说明)

 

SymTagBaseType

表示基本类型。基本类型有两个重要的属性,分别是长度和基本类型ID。长度即该类型在内存中占用多少个字节,而基本类型ID则指明是哪种基本类型,例如charintdouble等。DbgHelp通过BaseTypeEnum枚举来表示基本类型ID,它的定义如下:

 1 enum BaseTypeEnum {
 2    btNoType = 0,
 3    btVoid = 1,
 4    btChar = 2,
 5    btWChar = 3,
 6    btInt = 6,
 7    btUInt = 7,
 8    btFloat = 8,
 9    btBCD = 9,
10    btBool = 10,
11    btLong = 13,
12    btULong = 14,
13    btCurrency = 25,
14    btDate = 26,
15    btVariant = 27,
16    btComplex = 28,
17    btBit = 29,
18    btBSTR = 30,
19    btHresult = 31
20 };

 上文说过,微软的符号模型是通用的,不针对某种语言,所以BaseTypeEnum中的值与C/C++中的基本类型有些出入,不能将它们直接等同起来。为了辨别一个C/C++的基本类型,需要同时根据长度和基本类型ID的值来进行判断,如下表所示:

BaseTypeEnum

Length

C/C++ base type

btBool

1

bool

btChar

1

char

btUInt

1

unsigned char

btWChar

2

wchar_t

btInt

2

short

btUInt

2

unsigned short

btInt

4

int

btUInt

4

unsigned int

btLong

4

long

btULong

4

unsigned long

btInt

8

long long

btUInt

8

unsigned long long

btFloat

4

float

btFloat

8

double

长度可以使用TI_GET_LENGTH来获取,而基本类型ID可以通过TI_GET_BASETYPE来获取,此时pInfo的类型都是DWORD*。最后要注意的一点是,基本类型没有名称,不能通过TI_GET_SYMNAME获取名称。

 

SymTagPointerType

表示指针类型。指针类型有两个重要属性:是否引用,以及类型ID。在C++中,引用本质上是通过指针来实现的,所以指针类型和引用类型都由SymTagPointerType来表示,并通过“是否引用”这个属性来区别。而指针指向的类型的ID,则可以通过“类型ID”这个属性来获取。是否引用可以通过TI_GET_IS_REFERENCE来获取,pInfo的类型是BOOL*;类型ID可以通过TI_GET_TYPEID来获取,pInfo的类型是DWORD*。另外,指针类型也有一个长度属性,通过TI_GET_LENGTH来获取,pInfo的类型是DWORD*。在32位和64位操作系统中,指针的长度总是4个字节和8个字节,因此这个属性不怎么重要。指针类型同样没有名称。

 

SymTagArrayType

表示数组类型。数组类型有以下的属性:元素类型ID,元素个数以及数组长度。元素类型ID指明了数组的元素是哪种类型;元素个数的含义不言而喻了;数组长度即整个数组所占用的字节数。以上三个属性分别通过TI_GET_TYPEIDTI_GET_COUNTTI_GET_LENGTH来获取,前两者使用的pInfo类型都是DWORD*,最后一个使用的pInfo类型是ULONG64*。数组类型也没有名称。

 

SymTagEnum

表示枚举类型。枚举类型有以下的属性:名称,是否嵌套,基本类型ID,长度,枚举值个数,以及枚举值集合。名称即定义枚举时使用的名称,如果使用的是匿名类型,例如:

1 enum {
2     aeOne,
3     aeTwo,
4     aeThree,
5 } anonymousEnum;

那么使用的是由编译器产生的名称。如果枚举定义在一个UDT里面,那么“是否嵌套”属性的值就为真,此时它的名称前面会加上UDT的名称,例如Class::NestedEnum。在定义枚举类型的时候可以指定枚举值使用何种基本类型,基本类型ID属性和长度属性就是用来确定这个基本类型的,它们的获取方法以及表示的含义与SymTagBaseType类型符号相同。枚举值个数的意义不言而喻,例如上面的匿名枚举就有三个枚举值。枚举类型里的每个枚举值同样都是符号,这些符号的类型是SymTagData,属于数据符号,不是类型符号,为了叙述方便,下文将这些符号称为“枚举值符号”。枚举值符号都是常量,都拥有名称以及值这两个属性。由于枚举类型的变量实际上保存的是枚举值的整型值,通过枚举值符号的值属性,就可以确定变量保存的是哪个枚举值。枚举类型的名称属性可以通过TI_GET_SYMNAME获取,此时pInfo的类型是WCHAR**,我们不需要分配缓冲区来接收名称,因为SymGetTypeInfo会完成这个任务,并把缓冲区的指针赋值给*pInfo,我们要做的就是在使用完名称之后用LocalFree释放这个缓冲区,例如:

 1 WCHAR* pBuffer;
 2 
 3 SymGetTypeInfo(
 4    GetDebuggeeHandle(),
 5    modBase,
 6    typeID,
 7    TI_GET_SYMNAME,
 8    &pBuffer);
 9 
10 std::wstring typeName(pBuffer);
11 
12 LocalFree(pBuffer);

是否嵌套可以通过TI_GET_NESTED获取,pInfo的类型是DWORD*,如果为真,*pInfo的值为,否则为0。枚举个数可以通过TI_GET_CHILDRENCOUNT获取,pInfo的类型为DWORD*。至于枚举值符号的获取则比较麻烦,首先需要分配一块足够大的缓冲区,然后通过TI_FINDCHILDREN来获取,pInfo的类型是TI_FINDCHILDREN_PARAMS*。下面是示例: 

 1 TI_FINDCHILDREN_PARAMS* pFindParams =
 2    (TI_FINDCHILDREN_PARAMS*)malloc(sizeof(TI_FINDCHILDREN_PARAMS) + childrenCount * sizeof(ULONG));
 3 
 4 pFindParams->Start = 0;
 5 pFindParams->Count = childrenCount;
 6 
 7 SymGetTypeInfo(
 8    GetDebuggeeHandle(),
 9    modBase,
10    typeID,
11    TI_FINDCHILDREN,
12    pFindParams);

childrenCount表示枚举值的个数。TI_FINDCHILDREN_PARAMS的定义如下:

1 typedef struct _TI_FINDCHILDREN_PARAMS {
2     ULONG Count;
3     ULONG Start;
4     ULONG ChildId[1];
5 } TI_FINDCHILDREN_PARAMS;

StartCount字段分别指示从第几个枚举值开始获取(以0为基数),和获取多少个枚举值,在调用SymGetTypeInfo之前需要初始化这两个字段。而ChildID是保存枚举值符号的ID的数组,它只定义为1个元素的长度,所以我们要自己分配缓冲区来容纳所有的ID。对于枚举值符号来说,虽然它不是类型符号,但也可以通过SymGetTypeInfo来获取属性。枚举值符号的名称同样通过TI_GET_SYMNAME来获取,方法跟上文所说的一致。枚举值的整型值通过TI_GET_VALUE来获取,pInfo的类型是VARIANT*VARIANT可以表示多种不同类型的值,可以通过不同的字段来获取这些值。下表列举了VARIANT的一些字段与C/C++基本类型之间的关系:

Field

C/C++ Base Type

boolVal

bool

cVal

char

bVal

unsigned char

uiVal

wchar_t

iVal

short

uiVal

unsigned short

intVal

int

uintVal

unsigned int

lVal

long

ulVal

unsigned long

llVal

long long

ullVal

unsigned long long

fltVal

float

dblVal

double

 

SymTagUDT

表示用户定义类型,例如structclassunionUDT具有以下的属性:名称,长度,是否嵌套,种类,成员个数,成员集合。UDT名称的注意事项和获取方法与枚举类型名称一模一样,这里就不再赘述了。长度属性表示UDT在内存中实际占用的字节数,这是对齐后的大小,通过TI_GET_LENGTH获取。是否嵌套指明该UDT是否定义在另一个UDT里面,通过TI_GET_NESTED获取。种类属性则指明了UDTstructclass还是union,通过TI_GET_UDTKIND获取,此时pInfo的值是DWORD*它的值使用下面的枚举:

1 enum UdtKind { 
2     UdtStruct,
3     UdtClass,
4     UdtUnion
5 };

这个枚举同样定义在cvconst.h中。UDT实际上是一个新的作用域,在UDT里面可以定义变量,函数和类型等,这些称为UDT的成员,成员的获取方法与枚举值的获取方法的完全一致,TI_FINDCHILDREN_PARAMS结构的ChildId数组保存的是每个成员符号的ID。每个UDT成员都是符号,例如成员方法属于代码符号,成员变量属于数据符号,而内部类型属于类型符号,实际上UDT及其成员是一个递归的定义。对于成员变量,有一个特殊的偏移量属性,指明了成员变量的地址相对于UDT地址的偏移,可以通过TI_GET_OFFSET来获取,pInfo的类型是DWORD*

 

SymTagBaseClass

如果一个类继承了另一个类,那么子类会有一个特殊的成员符号,类型为SymTagBaseClass,是一个类型符号。它的属性有:名称,类型ID以及偏移,分别通过TI_GET_SYMNAMETI_GET_TYPEIDTI_GET_OFFSET获取。名称属性是父类的名称,类型ID属性是父类的类型ID,而偏移属性是父类部分在子类对象中的偏移。子类继承了多少个父类,就有多少个SymTagBaseClass成员。

 

SymTagFunctionType

表示函数类型。要注意SymTagFunctionSymTagFunctionType的区别,SymTagFunction表示函数的本体,包括声明和定义;而SymTagFunctionType仅表示函数的部分声明,包括返回值和形参列表,不包括函数的名称。在函数符号的SYMBOL_INFO结构体中,TypeIndex字段就是SymTagFunctionType类型符号的ID。函数类型有以下的属性:返回值类型ID,形参个数,以及形参集合。返回值类型ID指明了返回值的类型,可以通过TI_GET_TYPEID获取。如果返回值是void那么类型IDSymTagBaseType类型符号的ID,因为BaseTypeEnum有一个btVoid的枚举值。形参个数通过TI_GET_CHILDRENCOUNT获取,每个形参通过TI_FINDCHILDREN获取,具体方法请参考枚举类型。

 

SymTagFunctionArgType

表示函数形参。SymTagFunctionType通过TI_FINDCHILDREN获取到的都是SymTagFunctionArgType类型的符号,这种符号只有一个类型ID属性,指明了形参的类型,可以通过TI_GET_TYPEID获取。由于函数形参的名称无关紧要,所以这种符号没有名称属性。

 

SymTagTypedef

表示使用typedef关键字定义的类型。这种符号有一个类型ID属性,指明真正的类型,可以通过TI_GET_TYPEID获取。

 

SymTagFriend

表示友元。我尝试了所有可能的方法,都无法获取到SymTagFriend类型的符号,所以对这种符号没有了解。

 

总结

通过上文的描述,相信大家对微软的符号模型已经有一定的了解,这里对此作一个总结。

 

在调试符号中,大部分事物都由符号来表示,符号主要有三种类型:代码,数据和类型。代码符号表示函数;数据符号表示变量,实参,枚举值以及UDT的数据成员;类型符号表示代码符号和数据符号的类型,描述了这些符号的各种属性。类型符号包括基本类型,指针类型,数组类型,枚举类型,UDT类型,函数类型,函数形参类型,基类类型,typedef类型以及友元类型。

 

类型符号的信息可以通过SymGetTypeInfo函数来获取,对GetType参数传入不同的值,就可以获取到类型的各种信息。函数通过pInfo来返回信息,pInfo的类型根据GetType的值而变化。不同的类型符号有不同的属性,有些GetType的值只对特定的类型符号有效。代码符号和数据符号的信息可以通过SymFromNameSymFromAddr获取,这两个函数通过SYMBOL_INFO结构体来返回。也可以对代码符号和数据符号使用SymGetTypeInfo来获取符号的属性。

 

微软的符号模型非常适合使用面向对象的思想去理解,下图是相关的类图:(图中没有友元类型符号)

 

示例代码

本文没有相应的示例代码。

posted on 2011-04-14 12:42  Zplutor  阅读(6052)  评论(4编辑  收藏  举报