天秤座的唐风

总会有一个人需要你的分享~!- 唐风 -

  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  13 随笔 :: 0 文章 :: 69 评论 :: 0 Trackbacks

前几天和柯柯交流一个小问题,说是如何在一个函数内得到调用该函数的函数地址。有点拗口,就是说如果有一个函数A(当然我们在这个问题中并不知道它是哪个函数)调用了B函数,现在希望用个什么办法得到A函数的地址。  

我首先联想到的是,一般调试器都能给出嵌套的函数调用关系。那么肯定是有什么办法解决这个问题。上网查了一通之后只找到一些debug用的API和一些开发环境提供的调整宏等等,感觉不是很适用。后来想想,函数调用都涉及到“函数调用栈”(call stack),也许这里可以得到些什么信息。隐约回想起以前汇编课里老师讲过的一些函数调用时要“压栈”、“要保存现场”等,但已经记得不太清楚了,于是就又上网找了些函数调用栈的知识,发现了一些有意思的信息(上网时看到ChinaUnix上的一篇,也是转的,原地址和作者不详,如果你知道请告诉我):

  1. 一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作。
  2. 几乎任何本地编译器都会在每个函数体之前插入类似如下指令:PUSH EBP; MOV EBP ESP;即,在程式执行到一个函数的真正函数体时,已有以下数据顺序入栈:参数,返回地址,EBP。 

这里我最关心的是:函数调用时,会在栈里压入返回地址,和EBP。

因为函数调用的返回地址,正是调用指令Call的下一个指令的地址,那么,有了返回地址,就可以得到Call指令的位置了。有Call指令的位置又能干什么呢?幸好汇编课里的知识还记得一点点:Call指令就是一个跳转指令,它可以让IP(instruction point[Thanks to RednaxelaFX])指向要跳转的指令的地址,从那里开始执行。对于函数调用来说,就是让IP指向被调用的函数的地址。Call指令的操作数其实和被调用函数的地址有非常重要的关系。有了Call指令的操作数,就可以计算出被调用函数的地址。

但仅仅有这个还不够,比如,A调用了B,那么在A函数中肯定有一个Call指令,但这个Call指令中的操作数是和B函数地址相关的,与A的函数地址直接关系不大(至少在没有其它信息的情况下,不能计算出A的地址)。而我们要得到的却是A函数的地址。所以,得向上再找一层,找到调用A函数的地方,那个地方的Call指令里的操作数才和A函数地址有关。也就是说,Z函数调用了A函数,A函数调用了B函数。现在要得到A函数的地址,我们得在Z函数里找Call指令的操作数。这时候EBP就派上用场了。本地编译器在每个函数体之前插入的指令(PUSH EBP; MOV EBP ESP)构造了一个巧妙的结构,使得我们可以顺着函数调用栈一层一层向上,找到所有调用关系。

如何向上查找呢?我们看看函数调用时栈、EBP的值的情况就知道了。

假设现在函数在正Z函数内执行,那么此时栈和EBP的值可能是像下图这样的:

我们先不管现在EBP指向的内存(0x000f)中的内容XXX是什么(要不然会是鸡生蛋生鸡的问题),总之目前在栈中的着色块中的内容是属于函数Z的参数,Z执行结束后应该返回的地址以及Z函数的局部变量值。

现在Z函数调用A函数,会先将传给A的参数压栈,然后将现在这个指令(就是"Call A"啦)的下一个指令的地址压入栈中,以便A函数完后返回到Z中继续执行。然后进入A函数的内存空间,首先就是调用PUSH EBP,也就是将Z的EPB的内容(地址0x000f)压入栈中,然后再MOV EBP ESP,让EBP有一个新的栈顶(此时栈顶中的内容不就是Z函数时EBP的内容么?),然后再将A函数的局部变量压入栈中,开始执行A函数的代码。这时,栈和EBP的情况就像如图所示了: 

哈,这样就很清楚了,原来现在的EBP中的内容,正是上一级函数的EBP中的内容。而每一个函数的EBP指向的位置,向栈顶可以得到该函数的局部变量,向栈底可以得到函数的返回地址和参数。于是我们就可以根据这个结构层层向上,找到任何一层我们想找的函数EBP,从而也就能得到相应的返回地址了。  

好,从B函数中得到Z函数对A函数调用点的返回地址的问题也就解决了。现在就是处理Call指令的问题了。

我在Visual Studio 2003的Debug版中进行反汇编调试,发现Call指令对应的机器指令都是5个byte,第一个byte(E8)是指令的器码,猜想后面4个byte应该就是它的转移的目标地址了。结果按这个地址去找,发现根本不对,想想汇编也忘得差不多了,于是又去找了教程看看,才记起原来Call的操作数并不是绝对地址,而是偏移地址(跳转目标地址-Call指令地址-sizeof(Call指令)),这样就好办了,我有返回地址,于是就有了向上5个byte就是Call的地址,再从这个地址中取出Call指令机器码的后四个字节,加上返回地址,就得到了目标地址。

原以为已经搞定了。不过还有一个小插曲,就是在VS的Debug版中,Call并不直接跳到一函数中去(不知道为什么),而是跳到一块代码区,这块区域内排布了很多的Jmp指令用于各种跳转(不知道为什么这么搞,也许是为调试的功能而设计的吧,谁知道?还请不吝赐教),不过没关系,也就是多走一点路而已,Jmp指令的操作数和Call指令的意义是一样的,最终Jmp是跳到函数代码块中去的。于是也就得到了想要的结果。

  

下面是代码:

 1#include "stdafx.h"
 2
 3#include <string>
 4
 5unsigned int GetCallerAddress(void)
 6{
 7    unsigned int _ebp;
 8    __asm mov _ebp, ebp
 9
10    for (int i=2; i != 0--i) {
11        _ebp = *(unsigned int *)(_ebp);
12    }

13    unsigned int* ipAddress = (unsigned int*)(*(unsigned int *)(_ebp + 4));
14
15    ipAddress = (unsigned int*)((unsigned char *)ipAddress - 5);
16    unsigned int callInstructAddress = (unsigned int)ipAddress;
17    ipAddress = (unsigned int*)((unsigned char *)ipAddress + 1);
18    int funcAddrOffset = *ipAddress;
19    unsigned int *jumAddr = (unsigned int*)(callInstructAddress + funcAddrOffset + 5); 
20    callInstructAddress = (unsigned int)jumAddr;
21    jumAddr = (unsigned int*)((unsigned char *)jumAddr + 1);
22    funcAddrOffset = *jumAddr;
23    
24    return funcAddrOffset + callInstructAddress + 5;    
25}

26
27void fun1();
28
29void fun2()
30{
31    fun1();
32}

33
34void fun3()
35{
36    fun1();
37}

38
39
40void fun1()
41{
42    unsigned int _ebp;
43    __asm mov _ebp, ebp // 取当前EBP
44    unsigned int _preEbp = *(unsigned int *)(_ebp);   //得到上层函数的EBP
45    unsigned int* ipAddress = (unsigned int*)(*(unsigned int *)(_preEbp + 4)); // 取得返回地址
46    ipAddress = (unsigned int*)((unsigned char *)ipAddress - 5); // 得到Call指令地址  
47    unsigned int callInstructAddress = (unsigned int)ipAddress;     // 保存Call指令地址  
48    ipAddress = (unsigned int*)((unsigned char *)ipAddress + 1); 
49    int funcAddrOffset = *ipAddress; // 得到Call指令操作数
50    unsigned int *jumAddr = (unsigned int*)(callInstructAddress + funcAddrOffset + 5); // 找到Jmp指令
51    callInstructAddress = (unsigned int)jumAddr; // 保存jmp指令地址
52    jumAddr = (unsigned int*)((unsigned char *)jumAddr + 1); 
53    funcAddrOffset = *jumAddr; // 得到jmp指令操作数
54    unsigned int addr = funcAddrOffset + callInstructAddress + 5//得到函数地址
55
56    // 或者:unsigned int addr = GetCallerAddress();
57    printf("fun1 said : Caller Addres is 0x%08x\n", addr);
58}

59
60int _tmain(int argc, _TCHAR* argv[])
61{
62    fun1();
63    fun2();
64    fun3();
65
66    return 0;
67}

68
69


PS:后经柯柯验证,只有VC6、2003、2008的Debug版里才有效。Release版中不行,具体原因未细查(没时间,毕竟不是"正务",呵呵)。以后再遇到时再细究吧。至少,现在对函数调用栈有了一些新的认识。很开心,呵呵呵。  

 

后记: 

  这两天翻看《Windows95编程大奥秘》(候捷译)中,作者在分析PE格式的时候提到了,Call指令并不直接将程序控制转到目标函数,而是转入一个Jmp的代码块中,由Jmp来最终将控制权交给函数。为什么这么做呢?作者给出的结论是这样做可以使得载入器的行为变得简单。因为Jmp的操作数是存放在idata区的一个“变量”,载入器只需要将被调用的DLL的地址一次写入这个“变量”中就可以了。如果不这么做,那么需要在每个Call指令中的位置对函数地址进行Fixup,这样会有更多的工作量。 

  OK,你不要笑话我说还看Win95的书哦。是的,我承认我不知道上面这段话中内容在现在的XP或是Vista或是2000中是否依然有效(因为我没有去验证过),但我看到了解决的方向。另外,这本书真的像候捷先生所说,“仍然极具技术价值”。我很认同! 

  鉴于RednaxelaFX的提示和本书给的信息,我下一步将偿试从PE文件来找这个问题的解决之道,并顺带学习一下PE格式。读完《Win95》后,也可能会写篇读后感,敬请留意,嘿嘿。

  

posted on 2009-06-09 20:38 唐风 阅读(4393) 评论(7)  编辑 收藏 引用 所属分类: 调试技术与逆向工程

评论

# re: 函数调用栈初探 2009-11-15 03:33 OwnWaterloo
调试器需要符号表。

  回复  更多评论
  

# re: 函数调用栈初探 2009-11-21 17:39 唐风
@OwnWaterloo
调试器没有符号表的情况下应该也能工作,但给出来的信息是“人类不可读”的。

一直在找一种方法,希望能实现这样的需求:
给出两个断点,当程序停在第二个断点的时候,希望得到在之期间被调用过的函数的列表(按时间顺序)。
当然,希望这个方法是非侵入性的。
栈信息显然是不够的,某一时刻的栈信息只有被断点函数的上层函数信息,但可以肯定的是每个函数调用都会有压栈出栈,能不能设置一个类似“勾子”的东西,在CPU进行压栈的时候记录下某些信息呢?

  回复  更多评论
  

# re: 函数调用栈初探 2009-11-21 17:55 OwnWaterloo
@唐风

但可以肯定的是每个函数调用都会有压栈出栈

问题就在于这个前提是没有保证的。
编译器处理尾调用时可以使用jmp而不用call。
这种情况在msvc和gcc上都存在。

编译器不一定会生成序言部分 —— push ebp mov ebp esp。
msvc肯定有这种情况,我见过。
gcc好像会严格生成这部分代码。



vs2005 team suit好像有这个功能。 在性能测试中。 但没这么灵活……
好像只能在程序跑完之后才能输出分析的结果,不能任意两个断点之间。
输出结果中记得是包含了调用树的。
可以玩玩看,也许有输出2个断点之间的调用树呢?

  回复  更多评论
  

# re: 函数调用栈初探 2009-11-21 17:56 OwnWaterloo
@唐风

能不能设置一个类似“勾子”的东西,在CPU进行压栈的时候记录下某些信息呢?

这个就不知道了……
查查vs的那个性能测试是怎么做到的? 也是非侵入的。
或者其他profile工具是怎么做的?

  回复  更多评论
  

# re: 函数调用栈初探 2009-11-22 12:54 唐风
>>问题就在于这个前提是没有保证的。编译器处理尾调用时可以使用jmp而不用call。这种情况在msvc和gcc上都存在。
哦哦,原来还有这种情况,呃,如果是在 debug 模式下,优化全关呢?嗯,找个时间我再也看看。

>>或者其他profile工具是怎么做的?
这倒是,以前没想到这个可以做参考呢,嗯,谢谢提醒。


真是独学而无友,则孤陋而寡闻。

PS:
一连两天看到你抢占沙发长篇回复,果然是神人啊……
什么时候我够格能和你“论战”一番而后双方受益,倒是可以“含笑九泉”了,:)

学习中……

  回复  更多评论
  

# re: 函数调用栈初探 2009-11-27 02:29 OwnWaterloo
你本来写的是【quote】吧? cnblogs确实很偏心,cppblog确实是穿小鞋的……

说正题……
这里的评论里可能有你感兴趣的内容:
http://www.cnblogs.com/JeffreyZhao/archive/2009/11/17/linker-loader-library-correction-about-call-stack.html#1704232

关于push ebp,frame pointer,call-stack,debugging等。

有点长哦,一直往下看。
哈哈,评论的主角就是这篇文章中提到的RednaxelaFX。
应该是同一个人吧?
  回复  更多评论
  

# re: 函数调用栈初探 2009-11-27 19:49 唐风
谢谢你的提醒啊~!
呵呵,这两天跑去看 WinDbg 的扩展命令去了,企图看能不能通过写 WinDbg 的扩展来达到目的……有点头大的说,哈哈。

RednaxelaFX 这个“家伙”也是个神人,我在 JavaEye 上第一次发表这篇文章的时候就有他的回复,相信是同一个人,嘿嘿。(他在 JavaEye 上有关于 JavaScript 运行机制的文章,看得出是很有几把刷子的)

跑来跑去,发现世界挺大,圈子确挺小,哈哈哈哈 :P

嗯嗯,是够长的,明天早起好好看,嘿嘿!!

PS:
>>你本来写的是【quote】吧? cnblogs确实很偏心,cppblog确实是穿小鞋的……
确实是的,唉……
这段时间在通过 CSS 和 JS 脚本在配置 cnblogs 的博客,已经比较满意了,嘿嘿。  回复  更多评论
  


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理