stack frame和函数调用--眼见为实

从汇编层面上看程序,最主要的事务是函数调用,所以掌握函数调用和stack frame的概念是基本功。
以MSVC6为工具,debug一个小测试程序,就可以理解其中的原理了。
测试程序如下,是不是很简单?
void test2()
{
}
void test1()
{
    test2();
}
int main()
{
    test1();
}
这里的函数都没有带参数,可以集中精力考虑问题。

debug编译,F10(单步)运行。然后需要打开assembly窗口、Register窗口、memory窗口。这就可以看到所以需要的信息了。
等等,Register窗口里的东西真不少,看的有点乱,啊,没关系,EAX到EFL是常用的。而这里没有使用函数参数,所以只要关注3个东东:EIP,ESP,EBP。

EIP总是指向当前运行的代码,assembly窗口的最左边的编号就是代码地址,箭头所指的是不是对应着EIP?对了。

F10执行之后,EIP增加了4,哦,原来我的系统是32位的。那么64位、16位系统呢,你试试看,是不是心中所想的答案。
汇编的代码、变量是以16进制为默认的,如果你使用windbg/cdb来调试,就会更有感触。
回到正题,现在关注的是两个汇编命令:call 和ret。call在函数调用时发生,ret是被调用函数最后返回时发生。
自己试一试这两个指令,执行后发生了什么,EIP,ESP,EBP是怎么变化的?

答案自己来找是最好,我的结论是:它们各自有两步工作:
call: address 等于
push EIP            ;ESP-4
jump address

ret 等于
pop EIP            ;ESP+4
jump EIP

call 和ret是成对的,一个push,使ESP-4、另一个pop, 使ESP+4,这是一种对称。
为什么push是减呢,因为ESP是从高向低增加的。
在函数内部,还有一种对称,在函数开头和结束,那就是:
push        ebp
mov         ebp,esp
...
mov         esp,ebp
pop         ebp

当然,pop ebp之后是ret,函数调用就返回了。

一个进栈一个出栈,所以EBP维持着平衡,这意味着什么呢?
答案是:stack frame。想一想你在call stack窗口双击不同的函数时发生了什么,它怎么能跳到期望的函数代码位置?
我想是借助了stack frame。 现在该看看memory 窗口了,看一看在函数开头push ebp后,EBP所指的内存里都有什么?
比如这时我的EBP=0013FF2C(你看到的会和我的不同),memory中显示:
0013FF2C  80 FF 13 00 FD 17 40 00 45 3A 5C 74 ...


哦,[0013FF2C] = 0013FF80。等等,为什么我看到的是“ 80 FF 13 00”?little endian,x86电脑的思维方式, 进一步了解就wikipeida一下吧。是不是别扭?也许我应该从显示器的背面看,:-)。
随后的那个32位数是什么? FD 17 40 00, 对004017FD,看起来像什么,对了,代码地址,是函数返回的地址。都是400000开头的,验证一下,看看函数返回后是不是回到了这个代码地址?
有人问为什么是400,000H吗?我想说,是exe的image默认值,dll默认1000,000H,你可以改变它,对多个工程组成的软件系统优化,是有好处的,想知道更多,就google一下吧。需要学的东西还不少,不是吗?

我们还是回到那个[0013FF2C] = 0013FF80,因为没有参数,后面不用关注了,再看一看0013FF80指向了哪里?
0013FF80  C0 FF 13 00 E9 1A 40 00 01 00 00 00...


[0013FF80 = 0013FFC0,后面接着是外层的代码地址:00401AE9。
依次类推,你就会看到世界的尽头,:-)。
函数调用实现就是这样,你会看到外层函数、内层函数位于不同的stack地址,越内层地址越小。不是连续的对吧,因为要给形参和局部变量分配空间,如果有函数参数的话,位置应该紧接着函数返回值的地址,依次是第一个参数的地址、第二个、第三个...,地址越来越大,它们是call 调用之前push进去的,从右向左push参数,否则你就不会看到1,2,3...这样的顺序了。
EBP的上面,会给局部变量分配空间,所以你会在函数内看到sub  esp,40h类似的语句,
当然,为了对称,在后面还要加回来。
于是stack frame的概念在头脑中出现了。


局部变量
EBP
调用函数的返回地址
参数


stack里的这些信息,反映了函数调用的过程,每个stack frame是一层函数调用,这些数据可能不连续,但是会从高向低发展。利用函数开头的EBP,就可以把它们串起来,是个link list,对吗?存在于一维的stack 空间内。

 

stack地址          EBP         函数返回地址
...
0013FF2C : 80 FF 13 00  FD 17 40 00
...                          |                    
           ---------------    
...        |                                  
0013FF80 : C0 FF 13 00  E9 1A 40 00
...                          |                    
           ---------------    
...        |

0013FFC0 : ... ...

对了,如果看到ret 8之类的函数返回语句,那一定是个_stdcall了,windows API的方式;c/c++用的是__cdecl,有什么区别?查msdn吧。



参考文献:

PC Assembly Language, by Paul A. Carter, http://www.drpaulcarter.com/pcasm/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值