你好,我是Manish Vasani,是VC++编译器后端团队的SDET。在本系列文章中,我将讨论调用堆栈以及如何使用 DIA软件开发包 用于实现构建调用堆栈的堆栈遍历客户机。如果您对了解调试器如何构建调用堆栈感兴趣,或者希望编写一个应用程序来转储正在执行的进程的调用堆栈,那么您可能需要继续阅读更多内容。
在本文中,我将从堆栈遍历的简要介绍开始,讨论实现它的diaapi,然后遍历基于DIA的堆栈遍历的代码示例。熟悉stack walking的人都知道,除了DIA SDK之外,还有两种常用的stack walking api: 堆栈漫游64 (发现于 DbgHelp公司 )以及 虚拟风 (可在kernel32.lib中找到)。我将在文章的最后讨论这两个api。
简介:
当操作系统执行一个进程时,它会调用程序的入口点(或启动函数),并为进程分配一组资源。其中一些资源(如堆栈内存和寄存器上下文)是按线程分配的。线程中的每个新函数调用都会被分配一部分堆栈内存,并具有一个新的寄存器上下文。堆栈帧或 激活记录 本质上是活动函数调用的运行时状态信息。与堆栈帧相关联的典型属性包括:
1) 与框架关联的函数
2) 与帧关联的模块(二进制)
3) 回信地址
4) 为函数的参数和局部变量分配的堆栈内存量
5) 用于引用传递给函数调用的参数的基指针(BP)
6) 用于引用函数中局部变量的基指针(BP)(大多数情况下与5相同)
7) 用于当前执行指令(IP)的程序计数器或指令指针
8) 堆栈指针(SP)
9) 其他寄存器上下文
执行线程中的一系列活动堆栈帧通常称为调用堆栈。调用堆栈帮助开发人员调试运行时程序错误,还指示特定执行点的程序状态。构建这个调用堆栈的过程称为堆栈遍历。堆栈遍历客户机(如调试器)使用堆栈帧属性来检索函数参数、局部变量等的值。
用于St的DIA API确认行走:
的主要功能 DIA软件开发包 提供对PDB文件中调试信息的访问。除此之外,diasdk还提供了一组接口 可用于实现独立于平台的堆栈遍历器。这些是:
1) 艾迪斯塔克沃克 :这是DIA stack walker的主要初始化接口。
2) IDiaEnumStackFrames公司 :这是堆栈帧枚举器接口,它公开主API以执行到下一帧的堆栈遍历。
3) IDiaStackFrame公司 :此接口表示堆栈帧并公开堆栈帧的属性。
4) IDiaStackWalkHelper :这是DIA stack walker的助手接口。为了理解它的主要用途,让我们简单地介绍一下stack walker需要的两种主要输入类型:
一。 运行时实体:这包括运行时方面,如线程的寄存器上下文、加载的图像文件的属性,如 加载的u图像 ,对应于给定虚拟地址的可执行二进制文件(或模块)等。
二。 编译时生成实体:这包括PDB记录,它为框架提供静态属性,例如为本地/参数分配的内存,与帧对应的函数是否包含SEH/C++ EH,执行到下一帧的解压缩程序等。
diastack walker使用其他DIA接口读取编译时生成的实体,即PDB记录。它将获取运行时实体委托给DIA客户机。主要原因是DIA没有维护进程的运行时状态信息。例如,进程地址空间中的给定虚拟地址可能属于进程中加载的任何一个模块(可执行二进制文件)。每个模块都有不同的pdb文件,因此有不同的DIA会话来读取pdb。DIA不知道进程的已加载模块,因此希望其客户端提供此映射。大多数diastack walker客户机(如调试器)已经需要获取和存储此运行时信息。因此,期望客户机通过这个helper接口的实现向DIA提供这些信息是合乎逻辑的。
代码示例演练:
我将首先给出基于DIA的stack walker示例的类设计,然后介绍它的执行流。
班级设计:
1) MyDiaStackWalker公司: 这是主要的DIA stack walker类。它维护初始化接口(IDiaStackWalker)和堆栈帧枚举器(IDiaEnumStackFrames)的句柄。它还包含一些用于其他DIA操作的助手方法s。
2) MyWalkHelper: 此类实现IDiaStackWalkHelper接口。我将在后面详细讨论每种方法。
3) 调试器 :这是一个带有简单调试引擎的调试器类。它在启用调试的情况下启动指定的可执行文件,并在可执行文件中的每个调试中断事件中转储调用堆栈(调试中断事件可以通过使用 __调试中断 ()源代码中固有的)。
4) 上下文管理器 :此类管理与当前debuggee线程的当前堆栈帧关联的寄存器上下文(debuggee是正在调试的进程)
5) 模块管理器: 此类维护进程中加载的模块(可执行二进制文件)及其属性的列表。
执行流程:
1) 初始化 :
应用程序中的第一步是初始化DIA stack walker。这是通过IDiaStackWalker接口完成的。IDiaStackWalker提供两个特定于平台的API来初始化DIA客户端: 获取枚举帧 (对于x86)和 获取枚举帧2 (适用于x64和ia64平台)。这些API中的每一个都有一个指向实现IDiaStackWalkHelper的类的指针( MyDiastack辅助程序 )并返回堆栈帧枚举器(IDiaEnumStackFrames)。你可以参考“方法” MyDiaStackWalker::初始化“ 在MyDiaStackWalker.cpp中查看初始化逻辑。
2) 在堆栈中行走:
初始化之后,我们在调试器下启动指定的可执行文件。在遇到调试中断事件时,我们初始化寄存器上下文并为调试对象加载模块。比我们称之为 MyDiaStackWalker::WalkCallStack“ 方法遍历调用堆栈并检索每个堆栈帧的属性,如模块名、函数名、指令指针(IP)等:
//方法遍历调用堆栈中的所有堆栈帧
无效 MyDiaStackWalker::WalkCallStack ()
{
wprintf(左) “nWalking调用堆栈…n” );
IDiaSymbol*pFuncSym=NULL;
DWORD计数= 0 ;
BSTR功能名称;
乌龙龙ip= 0 ;
做 {
//获取当前堆栈帧的IP
&不英国标准普尔; pContextManager->获取当前ip(&ip);
如果 (!ip地址){
打破 ;
}
//获取堆栈帧的模块
模块*pModule=pModuleManager->FindModuleByVA(ip);
//获取堆栈帧的函数符号
pMyDiaStackWalkHelper->symbolForVA(ip和pFuncSym);
//显示堆栈帧属性
如果 (pFuncSym==空){
//如果未加载模块的符号,则函数符号可以为空。
//只需转储堆栈帧的IP
wprintf(左) “堆栈帧%%d:”“%ws!%%”#“x()’n” ,++count,pModule->name,ip);
持续 ;
}
如果 (pFuncSym->getu name(&szFunctionName)==Su确定){
wprintf(左) “堆栈帧%%d:”“%ws!%%”“不” ,++count,pModule->name,szFunctionName);
SysFreeString(szFunctionName);
}
pFuncSym->Release();
pFuncSym=空;
//走到下一个堆栈帧
} 虽然 (WalkStackFrame()==Su确定);
}
“ “WalkCallStack” 方法调用到“ “堆叠框架” 遍历每个堆栈帧。 WalkStackFrame公司 使用堆栈帧枚举器API( IDiaEnumStackFrames::下一步 )进行堆栈漫游到下一帧。以下代码段显示了这一点:
//MyDiaStackWalker.cpp文件
//方法遍历到下一个堆栈帧
HRESULT公司 MyStackWalker::WalkStackFrame (IDiaStackFrame**ppStackFrame)
{
冗长的{
wprintf(左) “MyDiaStackWalker::WalkStackFramen” );
}
乌龙celtFetched= 0 ;
IDiaStackFrame*pStackFrame=NULL;