在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平台调用函数堆栈的追踪方法的更多相关文章

  1. 关于追踪qemu 源码函数路径的一个方法

    这阵子一直在研究qemu 磁盘io路径的源码,发现直接看代码是意见非常低效率的事情,qemu是一个比较庞大的家伙(源码部分大概154MB,完全由C语言来完成),整个结构也都非常地复杂,所以从代码上研究 ...

  2. Windows API 函数列表 附帮助手册

    所有Windows API函数列表,为了方便查询,也为了大家查找,所以整理一下贡献出来了. 帮助手册:700多个Windows API的函数手册 免费下载 API之网络函数 API之消息函数 API之 ...

  3. C#调用Windows API函数截图

    界面如下: 下面放了一个PictureBox 首先是声明函数: //这里是调用 Windows API函数来进行截图 //首先导入库文件 [System.Runtime.InteropServices ...

  4. C++向main函数传递参数的方法(实例已上传至github)

    通常情况下,我们定义的main函数都只有空形参列表: int main(){...} 然而,有时我们确实需要给mian传递实参,一种常见的情况是用户设置一组选项来确定函数所要执行的操作.例如,假定ma ...

  5. WINDOWS API 函数(超长,值得学习)

    一.隐藏和显示光标 函数: int ShowCursor ( BOOL bShow );  参数 bshow,为布尔型,bShow的值为False时隐藏光标,为True时显示光标:该函数的返回值为整型 ...

  6. windows socket函数详解

    windows socket函数详解 近期一直用第三方库写网络编程,反倒是遗忘了网络编程最底层的知识.因而产生了整理Winsock函数库的想法.以下知识点均来源于MSDN,本人只做翻译工作.虽然很多前 ...

  7. 函数的属性和方法之call、apply 及bind

    一.前言 ECMAScript中的函数是对象,因此函数也有属性和方法.每个函数都包含两个属性:length和prototype.每个函数也包含两个非继承来的方法:apply()和call(),还有一些 ...

  8. Windows CE Notification API的使用方法

    1 引言      以Windows CE 为操作系统的掌上电脑(如PocketPC或HPC),除具备PC的功能外,还具备很强的自身控制能力.Windows CE API超越微软其他操作系统的 API ...

  9. php禁用函数设置及查看方法详解

    这篇文章主要介绍了php禁用函数设置及查看方法,结合实例形式分析了php禁用函数的方法及使用php探针查看禁用函数信息的相关实现技巧,需要的朋友可以参考下 本文实例讲述了php禁用函数设置及查看方法. ...

随机推荐

  1. PushMeBaby 使用

    github 下载地址 https://github.com/stefanhafeneger/PushMeBaby 1.执行假设报错,那么导入CoreServices.framawork 替换这句 # ...

  2. Android使用gradle不同配置多项目打包

    //build.gradle该配置文件里路径均是相对路径 apply plugin: 'com.android.application' android { def suffix = "su ...

  3. 【Android】定位与解决anr错误记录

    问题描写叙述 cocos2d-x游戏项目androidproject接入sdk.支付成功后,java代码回调lua方法.产生了anr. 怎样定位anr? watermark/2/text/aHR0cD ...

  4. 游戏AI(三)—行为树优化之基于事件的行为树

    上一篇我们讲到了关于行为树的内存优化,这一篇我们将讲述行为树的另一种优化方法--基于事件的行为树. 问题 在之前的行为树中,我们每帧都要从根节点开始遍历行为树,而目的仅仅是为了得到最近激活的节点,既然 ...

  5. springboot定时任务——整合Quartz

    http://blog.csdn.net/liuchuanhong1/article/details/60873295 http://blog.csdn.net/lyg_come_on/article ...

  6. 利用reverse索引优化like语句的方法详解

    在有一些情况下,开发同学经常使用like去实现一些业务需求,当使用like时,我们都知道使用like 前%(like '%111')这种情况是无法使用索引的,那么如何优化此类的SQL呢,下面是一个案例 ...

  7. mac上虚拟机安装旧版本的macosx 10.8

    前言 由于测试的需要,需要10.8的macosx,但又不想降级自己mac版本,所以还是装虚拟机,Parallels Desktop试验了安装不了osx,就换VMware Fusion,发现是可以的. ...

  8. xml文件解析(使用解析器)

    一.Xml解析,解析xml并封装到list中的javabean中 OM是用与平台和语言无关的方式表示XML文档的官方W3C标准.DOM是以层次结构组织的节点或信息片断的集合.这个层次结构允许开发人员在 ...

  9. C#又能出来装个B了。一步一步微信跳一跳自动外挂

    PS:语言只是载体.思维逻辑才是王道 前天看见了个python的脚本.于是装python.配置环境变量.装pip.折腾了一上午,最终装逼失败. 于是进入博客园,顶部有篇文章吸引了我 .NET开发一个微 ...

  10. 4.Nginx的URL重写应用

    Nginx的URL重写应用 nginx的URL重写模块是用得比较多的模块之一,所以我们需要好好地掌握运用.常用的URL重写模块命令有if,rewrite,set,break等. if命令 if用于判断 ...