VC驿站

 找回密码
 加入驿站

QQ登录

只需一步,快速开始

搜索
查看: 3871|回复: 6

[原创] Windows函数栈环境追踪

[复制链接]
用户已被注册 发表于 2016-8-15 21:10:20 | 显示全部楼层 |阅读模式
最近学到一个API函数CaptureStackBackTrace,这个函数可以追踪到程序调用函数的信息,但是只能获取到对应函数所在的地址,我想应该有方法可以得到函数名称等信息,我在网上找了些资料,终于把这个问题解决了,现在分享一下。为了达到这个目的,需要使用SymInitialize、StackWalk、SymGetSymFromAddr、SymGetLineFromAddr、SymCleanup这几个API函数。
基本上所有高级语言都有专门为函数准备的堆栈,用来存储函数中定义的变量,在C/C++中在调用函数之前会保存当前函数的相关环境,在调用函数时首先进行参数压栈,然后call指令将当前eip的值压入堆栈中,然后调用函数,函数首先会将自身堆栈的栈底地址保存在ebp中,然后抬高esp并初始化本身的堆栈,通过多次调用最终在堆栈段形成这样的布局 :

这里对函数的原理做简单的介绍,有兴趣的可以看我之前在论坛上发的关于C函数原理的帖子。
VC++编译器在编译时对函数名称与地址都有详细的记录,为了调试方便在Debug版本中,都有一个符号常量表,将符号常量与它对应的地址形成映射,在搜索时首先根据这些堆栈环境找到对应地址,然后根据地址在符号常量表中,找到具体调用的信息,这些API具体怎么实现的我不太清楚,我也没有找到对应的详细说明,所以说它们的原理我只能通过多改改源程序根据输出结构来进行猜测:每个线程都有一个独立的线程栈,用来保存函数中的信息,上面的图中可以看到API在遍历堆栈时可以找到对应的esp、eip、ebp等信息,eip中保存的是call语句的后一条语句,进一步得到call后面跟的地址值,根据这个值应该就可以在系统维护的符号常量表中找到对应的函数名称,以及在哪调用的它,然后根据ebp的值可以找到下一个函数的栈底地址和栈顶地址,一步一步向下查找。
SymInitialize:这个函数主要用作初始化相关环境。
SymCleanup:清楚这个初始化的相关环境,在调用SymInitialize之后需要调用SymCleanup,进行释放资源的操作
StackWalk:程序的功能主要由这个函数实现,函数会从初始化时的堆栈顶开始向下查找下一个堆栈的信息,原型如下:
  1. BOOL WINAPI StackWalk(
  2.   __in          DWORD MachineType, //机器类型现在一般是intel的x86系列,这个时候填入IMAGE_FILE_MACHINE_I386
  3.   __in          HANDLE hProcess, //追踪的进程句柄
  4.   __in          HANDLE hThread, //追踪的线程句柄
  5.   __in_out      LPSTACKFRAME StackFrame, //记录的追踪到的堆栈信息
  6.   __in_out      PVOID ContextRecord, //记录当前的线程环境
  7.   __in          PREAD_PROCESS_MEMORY_ROUTINE ReadMemoryRoutine,
  8.   __in          PFUNCTION_TABLE_ACCESS_ROUTINE FunctionTableAccessRoutine,
  9.   __in          PGET_MODULE_BASE_ROUTINE GetModuleBaseRoutine,
  10.   __in          PTRANSLATE_ADDRESS_ROUTINE TranslateAddress //后面的四个参数都是回掉函数,有系统自行调用,而且这些函数都是定义好的,只需要填入相应的函数名称
  11. );
复制代码

需要注意的一点是,在首次调用该函数时需要对StackFrame中的AddrPC、AddrFrame、AddrStack这三个成员进行初始化,填入相关值,以便函数从此处线程堆栈的栈顶进行搜索,否则调用函数将失败,具体如何填写请看MSDN。
SymGetSymFromAddr:根据获取到的函数地址得到函数名称、堆栈大小等信息,这个函数的原型如下:
BOOL WINAPI SymGetSymFromAddr:根据取得的地址值,获取调用函数的信息,主要是获取函数名称函数原型如下:
  1. BOOL WINAPI SymGetSymFromAddr(
  2.   __in          HANDLE hProcess, //进程句柄
  3.   __in          DWORD Address, //函数地址
  4.   __out         PDWORD Displacement, //返回该符号常量的位移或者填入NULL,不获取此值
  5.   __out         PIMAGEHLP_SYMBOL Symbol//返回堆栈信息
  6. );
复制代码

SymGetLineFromAddr:根据得到的地址值,获取调用函数的相关信息。主要记录是在哪个文件,哪行调用了该函数,下面是函数原型:
  1. BOOL WINAPI SymGetLineFromAddr(
  2.   __in          HANDLE hProcess,
  3.   __in          DWORD dwAddr,
  4.   __out         PDWORD pdwDisplacement,
  5.   __out         PIMAGEHLP_LINE Line
  6. );
复制代码

它参数的含义与SymGetSymFromAddr,相同。
通过上面对函数的说明,我们可以知道,为了追踪函数调用的详细信息,大致步骤如下:
1. 首先调用函数SymInitialize进行相关的初始化工作。
2. 填充结构体StackFrame的相关信息,确定从何处开始追踪。
3. 循环调用StackWalk函数,从指定位置,向下一直追踪到最后。
4. 每次将获取的地址分别传入SymGetSymFromAddr、SymGetLineFromAddr,得到函数的详细信息
5. 调用SymCleanup,结束追踪
但是需要注意的一点是,函数StackWalk会顺着线程堆栈进行查找,如果在调用之前,某个函数已经返回了,它的堆栈被回收,那么函数StackWalk自然不会追踪到该函数的调用。
下面是具体实现的代码:
  1. void InitTrack()
  2. {
  3.     g_hHandle = GetCurrentProcess();

  4.     SymInitialize(g_hHandle, NULL, TRUE);
  5. }

  6. void StackTrack()
  7. {
  8.     g_hThread = GetCurrentThread();
  9.     STACKFRAME sf = { 0 };

  10.     sf.AddrPC.Offset = g_context.Eip;
  11.     sf.AddrPC.Mode = AddrModeFlat;

  12.     sf.AddrFrame.Offset = g_context.Ebp;
  13.     sf.AddrFrame.Mode = AddrModeFlat;

  14.     sf.AddrStack.Offset = g_context.Esp;
  15.     sf.AddrStack.Mode = AddrModeFlat;

  16.     typedef struct tag_SYMBOL_INFO
  17.     {
  18.         IMAGEHLP_SYMBOL symInfo;
  19.         TCHAR szBuffer[MAX_PATH];
  20.     } SYMBOL_INFO, *LPSYMBOL_INFO;

  21.     DWORD dwDisplament = 0;
  22.     SYMBOL_INFO stack_info = { 0 };
  23.     PIMAGEHLP_SYMBOL pSym = (PIMAGEHLP_SYMBOL)&stack_info;
  24.     pSym->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL);
  25.     pSym->MaxNameLength = sizeof(SYMBOL_INFO) - offsetof(SYMBOL_INFO, symInfo.Name);
  26.     IMAGEHLP_LINE ImageLine = { 0 };
  27.     ImageLine.SizeOfStruct = sizeof(IMAGEHLP_LINE);

  28.     while (StackWalk(IMAGE_FILE_MACHINE_I386, g_hHandle, g_hThread, &sf, &g_context, NULL, SymFunctionTableAccess, SymGetModuleBase, NULL))
  29.     {
  30.         SymGetSymFromAddr(g_hHandle, sf.AddrPC.Offset, &dwDisplament, pSym);
  31.         SymGetLineFromAddr(g_hHandle, sf.AddrPC.Offset, &dwDisplament, &ImageLine);
  32.         printf("当前调用函数 : %08x+%s(FILE[%s]LINE[%d])\n", pSym->Address, pSym->Name, ImageLine.FileName, ImageLine.LineNumber);
  33.     }

  34. }

  35. void UninitTrack()
  36. {
  37.     SymCleanup(g_hHandle);
  38. }
复制代码

测试程序如下:
  1. void func1()
  2. {
  3.     OPEN_STACK_TRACK;
  4. }

  5. void func2()
  6. {
  7.     func1();
  8. }

  9. void func3()
  10. {
  11.     func2();

  12. }
  13. void func4()
  14. {
  15.     printf("hello\n");
  16. }

  17. int _tmain(int argc, TCHAR* argv[])
  18. {
  19.     func4();
  20.     func3();
  21.     func3();
  22.     return 0;
  23. }
复制代码

OPEN_STACK_TRACK是一个宏,它的定义如下:
  1. #define OPEN_STACK_TRACK\
  2. HANDLE hThread = GetCurrentThread();\
  3. GetThreadContext(hThread, &g_context);\
  4. __asm{call $ + 5}\
  5. __asm{pop eax}\
  6. __asm{mov g_context.Eip, eax}\
  7. __asm{mov g_context.Ebp, ebp}\
  8. __asm{mov g_context.Esp, esp}\
  9. InitTrack();\
  10. StackTrack();\
  11. 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中的相关函数调用。
把这些代码直接拷贝,然后定义几个以“g_”开头的全局变量,就能运行了,如果想要完整的工程,可以去我的github上下载(目前没有啥项目,只是上传了自己平时写的小例子,大牛勿喷
最后请允许我不要脸的把地址贴出来:https://github.com/aMonst/StackTrack.git

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?加入驿站

x

评分

参与人数 1威望 +2 驿站币 +2 激情 +2 收起 理由
Syc + 2 + 2 + 2 支持原创!

查看全部评分

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请编辑帖子并把分类改成【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【驿站币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

Syc 发表于 2016-8-16 10:13:29 | 显示全部楼层
很不错的分析,细致到位,图文结合,有技术含量,支持!

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请编辑帖子并把分类改成【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【驿站币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

回复 支持 反对

使用道具 举报

libocdf 发表于 2016-8-26 09:30:01 | 显示全部楼层
这些需要符号表文件吧 pdb之类的!!

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请编辑帖子并把分类改成【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【驿站币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

回复 支持 反对

使用道具 举报

996949808 发表于 2016-8-31 11:26:10 | 显示全部楼层
好高深,下次再看

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请编辑帖子并把分类改成【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【驿站币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

回复 支持 反对

使用道具 举报

runfog 发表于 2017-5-2 08:32:36 | 显示全部楼层
学到API函数CaptureStackBackTrace

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请编辑帖子并把分类改成【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【驿站币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

回复 支持 反对

使用道具 举报

chxi 发表于 2017-7-6 22:36:14 | 显示全部楼层
API函数使用高手,佩服

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请编辑帖子并把分类改成【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【驿站币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

回复 支持 反对

使用道具 举报

蚊子 发表于 2017-8-7 09:46:46 | 显示全部楼层
反正我是看不懂,小白一个

发帖求助前要善用论坛搜索功能,那里可能会有你要找的答案;

如果你在论坛求助问题,并且已经从坛友或者管理的回复中解决了问题,请编辑帖子并把分类改成【已解决】

如何回报帮助你解决问题的坛友,一个好办法就是给对方加【热心】【驿站币】,加分不会扣除自己的积分,做一个热心并受欢迎的人!

回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 加入驿站

本版积分规则

展开

QQ|小黑屋|手机版|VC驿站 ( 辽ICP备09019393号 )

返回顶部
x

VC驿站微信公众号cctry2009

GMT+8, 2017-12-14 04:33

Powered by Discuz!

© 2009-2017 cctry.com

快速回复 返回顶部 返回列表