windows平台调用函数堆栈的追踪方法
在windows平台,有一个简单的方法来追踪调用函数的堆栈,就是利用函数CaptureStackBackTrace,但是这个函数不能得到具体调用函数的名称,只能得到地址,当然我们可以通过反汇编的方式通过地址得到函数的名称,以及具体调用的反汇编代码,但是对于有的时候我们需要直接得到函数的名称,这个时候据不能使用这个方法,对于这种需求我们可以使用函数:SymInitialize、StackWalk、SymGetSymFromAddr、SymGetLineFromAddr、SymCleanup。
原理
基本上所有高级语言都有专门为函数准备的堆栈,用来存储函数中定义的变量,在C/C++中在调用函数之前会保存当前函数的相关环境,在调用函数时首先进行参数压栈,然后call指令将当前eip的值压入堆栈中,然后调用函数,函数首先会将自身堆栈的栈底地址保存在ebp中,然后抬高esp并初始化本身的堆栈,通过多次调用最终在堆栈段形成这样的布局
这里对函数的原理做简单的介绍,有兴趣的可以看我的另一篇关于C函数原理讲解的博客,点击这里跳转
VC++编译器在编译时对函数名称与地址都有详细的记录,编译出来的程序都有一个符号常量表,将符号常量与它对应的地址形成映射,在搜索时首先根据这些堆栈环境找到对应地址,然后根据地址在符号常量表中,找到具体调用的信息,这是一个很复杂的工程,需要对编译原理和汇编有很强的基础,幸运的是,如今这些工作不需要程序员自己去做,windows帮助我们分配了一组API,在编写程序时只需要调用API即可
函数说明
SymInitialize:这个函数主要用作初始化相关环境。
SymCleanup:清楚这个初始化的相关环境,在调用SymInitialize之后需要调用SymCleanup,进行释放资源的操作
StackWalk:程序的功能主要由这个函数实现,函数会从初始化时的堆栈顶开始向下查找下一个堆栈的信息,原型如下:
BOOL WINAPI StackWalk(
__in DWORD MachineType, //机器类型现在一般是intel的x86系列,这个时候填入IMAGE_FILE_MACHINE_I386
__in HANDLE hProcess, //追踪的进程句柄
__in HANDLE hThread, //追踪的线程句柄
__in_out LPSTACKFRAME StackFrame, //记录的追踪到的堆栈信息
__in_out PVOID ContextRecord, //记录当前的线程环境
__in PREAD_PROCESS_MEMORY_ROUTINE ReadMemoryRoutine,
__in PFUNCTION_TABLE_ACCESS_ROUTINE FunctionTableAccessRoutine,
__in PGET_MODULE_BASE_ROUTINE GetModuleBaseRoutine,
__in PTRANSLATE_ADDRESS_ROUTINE TranslateAddress //后面的四个参数都是回掉函数,有系统自行调用,而且这些函数都是定义好的,只需要填入相应的函数名称
);
需要注意的一点是,在首次调用该函数时需要对StackFrame中的AddrPC、AddrFrame、AddrStack这三个成员进行初始化,填入相关值,以便函数从此处线程堆栈的栈顶进行搜索,否则调用函数将失败,具体如何填写请看MSDN。
SymGetSymFromAddr:根据获取到的函数地址得到函数名称、堆栈大小等信息,这个函数的原型如下:
BOOL WINAPI SymGetSymFromAddr(
__in HANDLE hProcess, //进程句柄
__in DWORD Address, //函数地址
__out PDWORD Displacement, //返回该符号常量的位移或者填入NULL,不获取此值
__out PIMAGEHLP_SYMBOL Symbol//返回堆栈信息
);
SymGetLineFromAddr:根据得到的地址值,获取调用函数的相关信息。主要记录是在哪个文件,哪行调用了该函数,下面是函数原型:
BOOL WINAPI SymGetLineFromAddr(
__in HANDLE hProcess,
__in DWORD dwAddr,
__out PDWORD pdwDisplacement,
__out PIMAGEHLP_LINE Line
);
它参数的含义与SymGetSymFromAddr,相同。
通过上面对函数的说明,我们可以知道,为了追踪函数调用的详细信息,大致步骤如下:
1. 首先调用函数SymInitialize进行相关的初始化工作。
2. 填充结构体StackFrame的相关信息,确定从何处开始追踪。
3. 循环调用StackWalk函数,从指定位置,向下一直追踪到最后。
4. 每次将获取的地址分别传入SymGetSymFromAddr、SymGetLineFromAddr,得到函数的详细信息
5. 调用SymCleanup,结束追踪
但是需要注意的一点是,函数StackWalk会顺着线程堆栈进行查找,如果在调用之前,某个函数已经返回了,它的堆栈被回收,那么函数StackWalk自然不会追踪到该函数的调用。
具体实现
void InitTrack()
{
g_hHandle = GetCurrentProcess();
SymInitialize(g_hHandle, NULL, TRUE);
}
void StackTrack()
{
g_hThread = GetCurrentThread();
STACKFRAME sf = { 0 };
sf.AddrPC.Offset = g_context.Eip;
sf.AddrPC.Mode = AddrModeFlat;
sf.AddrFrame.Offset = g_context.Ebp;
sf.AddrFrame.Mode = AddrModeFlat;
sf.AddrStack.Offset = g_context.Esp;
sf.AddrStack.Mode = AddrModeFlat;
typedef struct tag_SYMBOL_INFO
{
IMAGEHLP_SYMBOL symInfo;
TCHAR szBuffer[MAX_PATH];
} SYMBOL_INFO, *LPSYMBOL_INFO;
DWORD dwDisplament = 0;
SYMBOL_INFO stack_info = { 0 };
PIMAGEHLP_SYMBOL pSym = (PIMAGEHLP_SYMBOL)&stack_info;
pSym->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL);
pSym->MaxNameLength = sizeof(SYMBOL_INFO) - offsetof(SYMBOL_INFO, symInfo.Name);
IMAGEHLP_LINE ImageLine = { 0 };
ImageLine.SizeOfStruct = sizeof(IMAGEHLP_LINE);
while (StackWalk(IMAGE_FILE_MACHINE_I386, g_hHandle, g_hThread, &sf, &g_context, NULL, SymFunctionTableAccess, SymGetModuleBase, NULL))
{
SymGetSymFromAddr(g_hHandle, sf.AddrPC.Offset, &dwDisplament, pSym);
SymGetLineFromAddr(g_hHandle, sf.AddrPC.Offset, &dwDisplament, &ImageLine);
printf("当前调用函数 : %08x+%s(FILE[%s]LINE[%d])\n", pSym->Address, pSym->Name, ImageLine.FileName, ImageLine.LineNumber);
}
}
void UninitTrack()
{
SymCleanup(g_hHandle);
}
测试程序如下:
void func1()
{
OPEN_STACK_TRACK;
}
void func2()
{
func1();
}
void func3()
{
func2();
}
void func4()
{
printf("hello\n");
}
int _tmain(int argc, TCHAR* argv[])
{
func4();
func3();
func3();
return 0;
}
OPEN_STACK_TRACK是一个宏,它的定义如下:
#define OPEN_STACK_TRACK\
HANDLE hThread = GetCurrentThread();\
GetThreadContext(hThread, &g_context);\
__asm{call $ + 5}\
__asm{pop eax}\
__asm{mov g_context.Eip, eax}\
__asm{mov g_context.Ebp, ebp}\
__asm{mov g_context.Esp, esp}\
InitTrack();\
StackTrack();\
UninitTrack();
这个程序需要注意以下几点:
1. 如果想要追踪所有调用的函数,需要将这个宏放置到最后调用的位置,当然前提是此时之前被调函数的堆栈仍然存在。当然可以在调用前简单的计算,找出在哪个位置是所有函数都没有调用完成的,不过这样可能就与程序的初衷相悖,毕竟程序本身就是为了获取堆栈的调用信息。。。。
2. IMAGEHLP_SYMBOL的结构体中关于Name的成员,只有一个字节,而函数SymGetSymFromAddr在填入值时是没有关心这个实际大小,它只是简单的填充,这就造成了缓冲区溢出的情况,为了避免我们需要在Name后面额外给一定大小的缓冲区,用来接收数据,这也就是我们定义这个结构体SYMBOL_INFO的原因。另外IMAGEHLP_SYMBOL中的MaxNameLength成员是指Name的最大长度,需要根据给定的缓冲区,进行计算。
3. 从测试程序来看,在进行追踪时func4已经调用完成,而我们在获取线程的运行时环境g_context时函数GetThreadContext,也在堆栈中,最终得到的结果中必然包含GetThreadContext的调用信息,如果想去掉这个信息,只需要修改获得信息的值,既然函数StackWalk是根据堆栈进行追踪,那么只需要修改对应堆栈的信息即可,需要修改eip 、ebp、esp的值,关于esp ebp的值很好修改,可以在对应函数中esp ebp这些寄存器的值,而eip的值就不那么好获取,本生利用mov指令得到eip的值它也是指令,会改变eip的值,从而造成获取到的eip的值不准确,所以我们利用call指令,先保存当前eip的值到堆栈,然后再从堆栈中取出。call指令的实质是 push eip和jmp addr指令的组合,并不一定非要调用函数。call指令的大小为5个字节,所以call $ + 5表示先保存eip在跳转到它的下一跳指令处。这样就可以有效的避免检测到GetThreadContext中的相关函数调用。
windows平台调用函数堆栈的追踪方法的更多相关文章
- 关于追踪qemu 源码函数路径的一个方法
这阵子一直在研究qemu 磁盘io路径的源码,发现直接看代码是意见非常低效率的事情,qemu是一个比较庞大的家伙(源码部分大概154MB,完全由C语言来完成),整个结构也都非常地复杂,所以从代码上研究 ...
- Windows API 函数列表 附帮助手册
所有Windows API函数列表,为了方便查询,也为了大家查找,所以整理一下贡献出来了. 帮助手册:700多个Windows API的函数手册 免费下载 API之网络函数 API之消息函数 API之 ...
- C#调用Windows API函数截图
界面如下: 下面放了一个PictureBox 首先是声明函数: //这里是调用 Windows API函数来进行截图 //首先导入库文件 [System.Runtime.InteropServices ...
- C++向main函数传递参数的方法(实例已上传至github)
通常情况下,我们定义的main函数都只有空形参列表: int main(){...} 然而,有时我们确实需要给mian传递实参,一种常见的情况是用户设置一组选项来确定函数所要执行的操作.例如,假定ma ...
- WINDOWS API 函数(超长,值得学习)
一.隐藏和显示光标 函数: int ShowCursor ( BOOL bShow ); 参数 bshow,为布尔型,bShow的值为False时隐藏光标,为True时显示光标:该函数的返回值为整型 ...
- windows socket函数详解
windows socket函数详解 近期一直用第三方库写网络编程,反倒是遗忘了网络编程最底层的知识.因而产生了整理Winsock函数库的想法.以下知识点均来源于MSDN,本人只做翻译工作.虽然很多前 ...
- 函数的属性和方法之call、apply 及bind
一.前言 ECMAScript中的函数是对象,因此函数也有属性和方法.每个函数都包含两个属性:length和prototype.每个函数也包含两个非继承来的方法:apply()和call(),还有一些 ...
- Windows CE Notification API的使用方法
1 引言 以Windows CE 为操作系统的掌上电脑(如PocketPC或HPC),除具备PC的功能外,还具备很强的自身控制能力.Windows CE API超越微软其他操作系统的 API ...
- php禁用函数设置及查看方法详解
这篇文章主要介绍了php禁用函数设置及查看方法,结合实例形式分析了php禁用函数的方法及使用php探针查看禁用函数信息的相关实现技巧,需要的朋友可以参考下 本文实例讲述了php禁用函数设置及查看方法. ...
随机推荐
- 阿里云部署Docker(4)----容器的使用
通过上一节的学习,我们知道怎样执行docker容器,我们执行了一个普通的,一个后台的,我们还学习了几个指令: docker ps - Lists containers. docker logs - S ...
- A. Arrays(Codeforces Round #317 水题)
A. Arrays time limit per test 2 seconds memory limit per test 256 megabytes input standard input out ...
- ABP 框架启程
准备动手写一套电商的系统,辗转收集了不少相关的开源项目,最后决定使用ABP作为起点. 在园子里好多人都在推广ABP.有个园友做了一个集合贴,方便大家使用 ABP集合贴 建议大家优先看 HK Zhan ...
- 分布式系统的消息&服务模式简单总结
分布式系统的消息&服务模式简单总结 在一个分布式系统中,有各种消息的处理,有各种服务模式,有同步异步,有高并发问题甚至应对高并发问题的Actor编程模型,本文尝试对这些问题做一个简单思考和总结 ...
- 《Java NIO (中文版)》【PDF】下载
<Java NIO (中文版)>[PDF]下载链接: https://u253469.pipipan.com/fs/253469-230062530 NIO (中文版)>[PDF]& ...
- iOS框架搭建(MVC,自定义TabBar)--微博搭建为例
项目搭建 1.新建一个微博的项目,去掉屏幕旋转 2.设置屏幕方向-->只有竖向 3.使用代码构建UI,不使用storyboard 4.配置图标AppIcon和LaunchImage 将微博资料的 ...
- 【USACO】玉米实验(单调队列)
Description 约翰决定培育新的玉米品种以提高奶牛的产奶效率.约翰所有的土地被分成 N ×N 块,其中第 r行第 c 列的玉米质量为 Ar,c.他打算找一块质量比较均匀的土地开始自己的实验.经 ...
- 程序员的自我救赎---11.4:FileSystem文件服务
<前言> (一) Winner2.0 框架基础分析 (二)PLSQL报表系统 (三)SSO单点登录 (四) 短信中心与消息中心 (五)钱包系统 (六)GPU支付中心 (七)权限系统 (八) ...
- free 命令详解
作用:显示当前系统未使用的和已使用的内存数目,还可以显示被内核使用的内存缓冲区 选项: -b 以byte为单位显示内存使用情况 -k 以kb 为单位显示内存使用情况 -m 以mb 为单位显示内存使用情 ...
- 阅读MDN文档之StylingBoxes(五)
目录 BoxModelRecap Box properties Overflow Background clip Background origin Outline Advanced box prop ...