概述
从汇编层面上看程序,最主要的事务是函数调用,所以掌握函数调用和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/
最后
以上就是发嗲鱼为你收集整理的stack frame和函数调用--眼见为实的全部内容,希望文章能够帮你解决stack frame和函数调用--眼见为实所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复