C语言函数调用及栈帧结构
Posted C语言中文社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言函数调用及栈帧结构相关的知识,希望对你有一定的参考价值。
技术干货第一时间送达!
http://blog.csdn.net/qq_29403077/article/details/53205010
往期回顾
地址空间与物理内存
二、栈帧的建立
首先要明白几个地方:每一个函数都有自己的栈帧空间,并且独占自己的栈帧空间, 当前正在运行的函数的栈帧总是在栈顶。Win32系统提供两个特殊的寄存器用于标识位于系统栈顶端的栈帧。
-
ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶,即栈顶寄存器。
-
EBP:基址指针寄存器(extended base pointer),即栈底寄存器。
-
栈帧状态值:保存前栈帧的顶部和底部(实际上只保存前栈帧的底部,前栈帧的顶部可以通过栈帧平衡计算得到),用于在本栈被弹出后恢复出上一个栈帧。
-
「注:函数栈帧的大小并不固定,一般与其对应函数的局部变量多少有关。函数运行过程中,其栈帧大小也是在不停变化的。除了与栈相关的寄存器外,我们还需要记住另一个至关重要的寄存器。」
int fun(int a,int b)
{
int sum=0;
sum=a+b;
return sum;
}
int main()
{
fun(3,4);
printf("haha");
return 0;
}
这里main是调用者(caller);fun是被调用者(Callee)在函数调用前,main正在用ESP和EBP寄存器指示它自己的栈帧。main把EAX,ECX和EDX压栈。这是一个可选的步骤,只在这三个寄存器内容需要保留的时候执行此步骤。接着,main把传递给fun的参数一一进栈,最后的参数最先进栈,这里也就解释了函数在压栈过程中是从右往左,即先压4,再压3。
(1)这里首先main函数建立自己的栈帧结构;main()函数是由__tCRTStartup()
函数调用的,所以mainCRTStratup()
函数调用__tmainCRTStra()
函数的时候就会从栈上为__tmainCRTStra()
分配类似图中这么一块空间,因为我们现在要调用main()函数了,所以当然要先把__tmainCRTStartup()
函数的运行状态保存下来,这样main()函数才能返回的时候才能找得到!。然后继续执行下一条语句:mov ebp,esp
即把esp
的值赋给ebp
,这样,ebp
也就指向了现在esp
的位置 然后sub esp 0C0h
这样就为main函数开辟了一段空间然后将ebx、esi、edi
寄存器压栈就形成如图所示:
当函数fun,也就是被调用者取得程序的控制权,它必须做3件事:建立它自己的栈帧,为局部变量分配空间,最后,如果需要,保存寄存器EBX,ESI和EDI的值。首先fun必须建立它自己的栈帧。EBP寄存器现在正指向main的栈帧中的某个位置,这个值必须被保留,因此,EBP进栈。然后ESP的内容赋值给了 EBP。这使得函数的参数可以通过对EBP附加一个偏移量得到,而栈寄存器ESP便可以空出来做其他事情。如此一来,几乎所有的c函数都由如下两个指令开 始:
push ebp
mov ebp, esp
下一步,fun必须为它的局部变量分配空间,同时,也必须为它可能用到的一些临时变量分配 空间。比如,foo中的一些C语句可能包括复杂的表达式,其子表达式的中间值就必须得有地方存放。这些存放中间值的地方同城被称为临时的,因为他们可以为 下一个复杂表达式所复用 现在,局部变量和临时存储都可以通过基准指针EBP加偏移量找到了。最后,如果fun用到EBX,ESI
和EDI
寄存器,则它必须在栈里保存它们。这里写图片描述
fun的函数体现在可以执行了。这其中也许有进栈、出栈的动作,栈指针ESP也会上下移动,但EBP是保持不变的。这意味着我们可以一直用[EBP+…]
找到第一个参数,而不管在函数中有多少进出栈的动作。函数fun的执行也许还会调用别的函数,甚至递归地调用foo本身。然而,只要EBP寄存器在这些子调用返回时被恢复,就可以继续用EBP加上偏移量的方式访问实际参数,局部变量和临时存储。紧接着当被调用者执行完毕时将消除栈帧结构,调用pop指令。
在把程序控制权返还给调用者前,被调用者foo必须先把返回值保存在EAX寄存器中。其次,foo必须恢复EBX,ESI和EDI寄存器的值。进栈和出栈操作的次数必须保持平衡。
在程序控制权返回到调用者main)后,这时,传递给fun的参数通常已经不需要了。我们可以把参数一起弹出栈,这可以通过把栈指针实现:add esp, 8
此时fun函数调用结束栈帧结构恢复至图一。如果在函数调用前,EAX,ECX
和EDX
寄存器的值被保存在栈中,调用者main函数现在可以把它们弹出。这个动作之后,栈顶就回到了我们开始整个函数调用过程前的位置。这样整个函数的调用就结束了
9.25
点分享 点点赞 点在看
以上是关于C语言函数调用及栈帧结构的主要内容,如果未能解决你的问题,请参考以下文章
图解C/C++语言底层:函数调用过程之函数栈帧的创建和销毁(上)