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禁用函数设置及查看方法. ...
随机推荐
- [NOIP复习]第三章:动态规划
一.背包问题 最基础的一类动规问题.相似之处在于给n个物品或无穷多物品或不同种类的物品,每种物品仅仅有一个或若干个,给一个背包装入这些物品,要求在不超出背包容量的范围内,使得获得的价值或占用体积尽可能 ...
- 单行json_ajax
html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3 ...
- Objective-C 和 Swift 混编项目的小 Tips(一)
本文主要闲聊一些 Objective-C 和 Swift 混编项目带来的一些潜规则,希望能帮到对此感到疑惑的朋友.下面我们开始进入主题: 命名 官方 Guide 上只是简单叙述(Using Swift ...
- bootstrap插件summernote 的使用
一.有时Java工程中会用到summernote 编辑器,下面简单说一下引用 1.将summernote 相应的文件放到工程中(webapp下面) 2.建一个jsp文件,在文件中引入相应的js.css ...
- springboot(十三):springboot小技巧
一些springboot小技巧.小知识点 初始化数据 我们在做测试的时候经常需要初始化导入一些数据,如何来处理呢?会有两种选择,一种是使用Jpa,另外一种是Spring JDBC.两种方式各有区别下面 ...
- 《极客与团队》【PDF】下载
<极客与团队>[PDF]下载链接: https://u253469.ctfile.com/fs/253469-231196337 内容简介 软件开发是一项团队运动,人的因素对结果的影响完全 ...
- 解决WebService/WCF调用时报错"服务器提交了协议冲突. Section=ResponseStatusLine"问题
今天更新了一个网站,新增了一个页面,调用WebService,在测试环境好好的,部署到正式环境后就莫名报错: 服务器提交了协议冲突. Section=ResponseStatusLine 网上查了好多 ...
- linux下增加磁盘改变指定文件路径分区挂载点和迁移数据
Centos7 系统上原有目录/data 挂载根目录下,空间有点小,我们需要把/data目录挂载到另一个磁盘,同时把数据迁移. 1.查看分区情况 fdisk -l 2.查看路径对应分区情况 df -l ...
- bzoj 3999: [TJOI2015]旅游
Description 为了提高智商,ZJY准备去往一个新世界去旅游.这个世界的城市布局像一棵树.每两座城市之间只有一条路径可 以互达.每座城市都有一种宝石,有一定的价格.ZJY为了赚取最高利益,她会 ...
- bzoj 4569: [Scoi2016]萌萌哒
Description 一个长度为n的大数,用S1S2S3...Sn表示,其中Si表示数的第i位,S1是数的最高位,告诉你一些限制条件,每个条 件表示为四个数,l1,r1,l2,r2,即两个长度相同的 ...