现在的位置: 首页 > 综合 > 正文

[Win32]一个调试器的实现(十)显示变量

2012年01月31日 ⁄ 综合 ⁄ 共 4857字 ⁄ 字号 评论关闭

上回介绍了微软的符号模型,有了这个基础知识,这回我们向MiniDebugger中添加两个新功能,分别是显示变量列表和以指定类型显示内存内容。显示变量列表用于列出当前函数内的局部变量或者全局变量;以指定类型显示内存内容用于读取指定地址处的内存内容,然后将这些二进制数据按照类型的格式解析成可读的内容并显示出来。如下面的截图所示:


 

使用lv命令显示局部变量时,每一列从左到右分别是:类型,名称,长度,地址,值。只有基本类型、枚举类型以及指针类型的变量才会显示它的值,对于数组类型和UDT类型的变量则不显示它的值。获取这些变量的值需要使用f命令,该命令遵守同样的显示规则。下面介绍这两个命令的实现方法。

 

枚举全局变量

DbgHelp提供了SymEnumSymbols函数来枚举符号,它既可以枚举全局的符号,也可以枚举当前作用域内的符号。我们首先来看一下如何使用它来枚举全局变量。该函数的声明如下:

1 BOOL WINAPI SymEnumSymbols(
2     HANDLE hProcess,
3     ULONG64 BaseOfDll,
4     PCTSTR Mask,
5     PSYM_ENUMERATESYMBOLS_CALLBACK EnumSymbolsCallback,
6     PVOID UserContext
7 );

 

hProcess参数是符号处理器的标识符。BaseOfDll参数指定模块的基地址,SymEnumSymbols函数会枚举该模块内的所有全局符号。为了获取当前模块的基地址,可以使用SymGetModuleBase64函数,该函数的声明如下:

1 DWORD64 WINAPI SymGetModuleBase64(
2     HANDLE hProcess,
3     DWORD64 dwAddr
4 );

第一个参数是符号处理器的标识符。第二个参数是当前EIP的值,可以使用GetThreadContext来获取。

 

Mask参数是一个字符串,只有名称与该字符串匹配的符号才会被枚举,在字符串中允许使用通配符*?

 

EnumSymbolsCallback参数是一个回调函数的指针,对于每个被枚举的符号都会调用该函数。该函数的声明如下:

1 BOOL CALLBACK SymEnumSymbolsProc(
2     PSYMBOL_INFO pSymInfo,
3     ULONG SymbolSize,
4     PVOID UserContext
5 );

第一个参数是指向SYMBOL_INFO的指针,有关符号的信息都在该结构体中。第二个参数是符号的长度,对于变量来说,SYMBOL_INFO结构体中的Size字段是无效的,这里的SymbolSize参数才是变量的长度。第三个参数UserContext其实就是SymEnumSymbols的最后一个参数,如果想要给SymEnumSymbolsProc传递额外的信息,可以通过这个参数来传递。SymEnumSymbolsProc必须返回TRUE,整个枚举过程才会成功,如果在某次回调SymEnumSymbolsProc的过程中返回FALSE,枚举过程就会中断,SymEnumSymbols也会返回FALSE

 

要注意的是,SymEnumSymbols会枚举所有符合条件的符号,包括函数,变量等。所以需要在回调函数中检查pSymInfo->Tag是否等于SymTagData

 

枚举局部变量

枚举局部变量也是使用SymEnumSymbols函数,不同的是,BaseOfDll参数要为0,而且要事先调用SymSetContext函数。该函数的作用是设置枚举符号时使用的作用域,它的声明如下:

1 BOOL WINAPI SymSetContext(
2     HANDLE hProcess,
3     PIMAGEHLP_STACK_FRAME StackFrame,
4     PIMAGEHLP_CONTEXT Context
5 );

第一个参数是符号处理器的标识符。第二个参数是这个函数的关键,它是一个指向IMAGEHLP_STACK_FRAME结构体的指针,这个结构体确定了一个作用域,它有多个字段,然而目前只有InstructionOffset字段会用到,将当前EIP的值赋给它即可,其它字段应设为0。第三个参数不会用到,设为NULL即可。

 

如果调用SymSetContext时指定的作用域与调用之前的作用域相同,函数会返回FALSE,而GetLastError()会返回ERROR_SUCCESS,此时并不意味着函数调用失败,所以在SymSetContext返回FALSE时还要进一步检查GetLastError()的返回值。

 

枚举局部变量时还有一个问题需要注意,在回调函数中,pSymInfo->Address的值并不是变量的虚拟地址,而是相对于某个寄存器的偏移地址,pSymInfo->Flags中的SYMFLAG_REGREL标志指明了这个情况。运行在Intel x86兼容架构下的调试版程序总是使用EBP寄存器的值作为基址。在汇编级别上,函数的调用过程大致如下所示:

1 push eax  ;在栈上压入数据,以传递参数
2 push ebx
3 call func  ;调用函数,在栈上压入函数的返回地址
4 
5 ; func函数的入口
6 push ebp  ;保存ebp的值
7 mov ebp, esp  ;将esp的值赋给ebp
8 sub esp, 8  ;为局部变量分配空间

执行完上面的汇编语句之后,线程栈的内容如下图所示(每个矩形代表4个字节):

 

在函数的执行过程中,ESP的值是不断变化的,而EBP的值一直不会改变(除非函数返回),所以使用EBP作为基址更方便。在SymEnumSymbolsProc函数中,需要检查pSymbolInfo->Flags字段是否含有SYMFLAG_REGREL标志,如果有,则将pSymbolInfo->AddressEBP相加,得到变量的虚拟地址。但是,在某些情况下有例外,当被调试进程刚刚执行了CALL语句时,进入了函数的作用域,此时SymEnumSymbols可以枚举到函数内的局部变量。但这时还未执行PUSH EBPMOV EBP, ESP指令,EBP仍然是上一个函数中的值,如果将EBPpSymbolInfo->Address相加,肯定得到错误的结果。为了避免这个问题,需要检查当前EIP是否指向函数的第一条指令,如果是,则不能使用EBP,而应该使用ESP-4作为基址。参考上图,在执行第一条指令之前,ESP-4就是执行了PUSH EBPMOV EBP, ESP之后EBP的值。另外,PUSH EBPMOV EBP, ESP总是在同一源代码语句中,所以源代码级别的调试器不用担心EIP指向MOV EBP, ESP指令的情况。下面的代码展示了如何获取变量的地址(在枚举全局变量和局部变量时都用到该函数):

 1 //获取符号的虚拟地址
 2 //如果符号是一个局部变量或者参数
 3 //pSymbol->Address是相对于EBP的偏移,
 4 //将两者相加就是符号的虚拟地址
 5 DWORD GetSymbolAddress(PSYMBOL_INFO pSymbolInfo) {
 6 
 7     if ((pSymbolInfo->Flags & SYMFLAG_REGREL) == 0) {
 8         return DWORD(pSymbolInfo->Address);
 9     }
10 
11     //如果当前EIP指向函数的第一条指令,则EBP的值仍然是属于
12     //上一个函数的,所以此时不能使用EBP,而应该使用ESP-4作
13     //为符号的基地址
14 
15     CONTEXT context;
16     GetDebuggeeContext(&context);
17 
18     //获取当前函数的开始地址
19     DWORD64 displacement;
20     SYMBOL_INFO symbolInfo = { 0 };
21     symbolInfo.SizeOfStruct = sizeof(SYMBOL_INFO);
22 
23     SymFromAddr(
24         GetDebuggeeHandle(),
25         context.Eip,
26         &displacement,
27         &symbolInfo);
28 
29     //如果是函数的第一条指令,则不能使用EBP
30     if (displacement == 0) {
31         return DWORD(context.Esp - 4 + pSymbolInfo->Address);
32     }
33 
34     return DWORD(context.Ebp + pSymbolInfo->Address);
35 }

 

变量的名称、地址、长度等属性可以直接在SymEnumSymbolsProc回调函数中获取,至于变量类型名称和值的获取则不是那么容易,下面分别讲解如何获取这两个属性。

 

获取变量类型名称

SYMBOL_INFO结构体的TypeIndex字段指明了变量的类型ID,通过这个ID就可以知道变量所属类型的所有信息。由于每种类型的属性都不相同,其处理方法也不同,所以要先获取类型的种类,然后根据它的种类进行不同的处理。获取类型种类的方法是调用SymGetTypeInfo,对第三个参数传入TI_GET_SYMTAGpInfo的类型是DWORD*。调用成功之后得到一个SymTagEnum值,该值可能是这些值中的一个:SymTagBaseTypeSymTagPointerTypeSymTagArrayTypeSymTagUDTSymTagEnumSymTagFunctionType。由上一篇文章介绍的符号模型可以知道,每种类型之间可能存在嵌套关系,即一种类型内可能包含一个或多个其它的类型,所以对类型的处理必然是一个递归的过程,如下面的代码所示:

 1 std::wstring GetTypeName(int typeID, DWORD modBase) {
 2 
 3     DWORD typeTag;
 4     SymGetTypeInfo(
 5         GetDebuggeeHandle(),
 6         modBase,
 7         typeID,
 8         TI_GET_SYMTAG,
 9         &typeTag);
10 
11     switch (typeTag) {
12         
13         case SymTagBaseType:
14             return GetBaseTypeName(typeID, modBase);
15 
16         case SymTagPointerType:
17             return GetPointerTypeName(typeID, modBase);
18 
19         case SymTagArrayType:
20             return GetArrayTypeName(typeID, modBase);
21 
22         case SymTagUDT:
23             return GetUDTTypeName(typeID, modBase);
24 
25         case SymTagEnum:

抱歉!评论已关闭.