反推函数调用栈

理解调用栈最重要的两点是:栈的结构,EBP寄存器的作用。

首先要认识到这样两个事实:

1、一个函数调用动作可分解为:零到多个PUSH指令(用于参数入栈),一个CALL指令。CALL指令内部其实还暗含了一个将返回地址(即CALL指令下一条指令的地址)压栈的动作。

2、几乎所有本地编译器都会在每个函数体之前插入类似如下指令:PUSH EBP; MOV EBP ESP;

即,在程序执行到一个函数的真正函数体时,已经有以下数据顺序入栈:参数,返回地址,EBP。
由此得到类似如下的栈结构(参数入栈顺序跟调用方式有关,这里以C语言默认的CDECL为例):

+| (栈底方向,高位地址) |
 | .................... |
 | .................... |
 | 参数3 |
 | 参数2 |
 | 参数1 |
 | 返回地址 |
-| 上一层[EBP] | <-------- [EBP]

“PUSH EBP”“MOV EBP ESP”这两条指令实在大有深意:首先将EBP入栈,然后将栈顶指针ESP赋值给EBP。“MOV EBP ESP”这条指令表面上看是用ESP把EBP原来的值覆盖了,其实不然——因为给EBP赋值之前,原EBP值已经被压栈(位于栈顶),而新的EBP又恰恰指向栈顶。

此时EBP寄存器就已经处于一个非常重要的地位,该寄存器中存储着栈中的一个地址(原EBP入栈后的栈顶),从该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值,而该地址处又存储着上一层函数调用时的EBP值!

一般而言,ss:[ebp+4]处为返回地址,ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用4字节内存),ss:[ebp-4]处为第一个局部变量,ss:[ebp]处为上一层EBP值。

由于EBP中的地址处总是“上一层函数调用时的EBP值”,而在每一层函数调用中,都能通过当时的EBP值“向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数局部变量值”。
如此形成递归,直至到达栈底。这就是函数调用栈。

编译器对EBP的使用实在太精妙了。

从当前EBP出发,逐层向上找到所有的EBP是非常容易的:

unsigned int _ebp;
__asm _ebp, ebp;
while (not stack bottom)
{
  //...
  _ebp = *(unsigned int*)_ebp;
}

如果要写一个简单的调试器的话,注意需在被调试进程(而非当前进程——调试器进程)中读取内存数据。

 

========================================================================== 

根据ebp可以找到函数的返回地址,而返回地址恰恰是调用此函数的下一条指令的地址,而根据此指令的地址减4就是一个指令字长,就可以找到调用此函数的地方即call xxx_yyy(),call后面的就是函数的地址了,以此类推就可以找到整个函数的调用过程。

==========================================================================

前几天和柯柯交流一个小问题,说是如何在一个函数内得到调用该函数的函数地址。有点拗口,就是说如果有一个函数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是跳到函数代码块中去的。于是也就得到了想要的结果。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值