c语言函数调用过程原理及函数栈帧分析

栈是什么

(1)简单来说,栈是一种LIFO形式的数据结构,所有的数据都是后进先出。这种形式的数据结构正好满足我们调用函数的方式: 父函数调用子函数,父函数在前,子函数在后;返回时,子函数先返回,父函数后返回。栈支持两种基本操作,push和pop。push将数据压入栈中,pop将栈中的数据弹出并存储到指定寄存器或者内存中。

(2)栈的生长方向是从高地址到低地址的,这是因为在下文讲的栈帧中,栈就是向下生长的。pop操作后,栈中的数据并没有被清空,只是该数据我们无法直接访问。

何为栈帧

(1)栈帧,也就是stack frame,其本质就是一种栈,只是这种栈专门用于保存函数调用过程中的各种信息(参数,返回地址,本地变量等)。每次函数调用,都为函数开辟一块空间,成为栈帧。

(2)栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。C语言中,每个栈帧对应着一个未运行完的函数。从逻辑上讲,栈帧就是一个函数执行的环境:函数调用框架、函数参数、函数的局部变量、函数执行完后返回到哪里等等。栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。

(3)对x86体系的CPU而言,其中
寄存器ebp(base pointer )可称为“帧指针”或“基址指针。
寄存器esp(stack pointer)可称为“ 栈指针”。
要知道的是
ebp 在未受改变之前始终指向栈帧的开始,也就是栈底,所以ebp的用途是在堆栈中寻址用的。esp是会随着数据的入栈和出栈移动的,也就是说,esp始终指向栈顶。

(4)一般来说,我们将 ebp 到 esp 之间区域当做栈帧(也有人认为该从函数参数开始,不过这不影响分析)。并不是整个栈空间只有一个栈帧,每调用一个函数,就会生成一个新的栈帧。在函数调用过程中,我们将调用函数的函数称为“调用者(caller)”,将被调用的函数称为“被调用者(callee)”。在这个过程中,1)“调用者”需要知道在哪里获取“被调用者”返回的值;2)“被调用者”需要知道传入的参数在哪里,3)返回的地址在哪里。同时,我们需要保证在“被调用者”返回后,ebp, esp 等寄存器的值应该和调用前一致。因此,我们需要使用栈来保存这些数据。

注意:ebp指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念esp所指的栈帧顶部和系统栈的顶部是同一个位置。

代码剖析

#include <stdio.h>

int Sub(int x,int y)
{
    int t = 0;
    t = x-y;
    return t;
}

int main()
{
    int a = 10;
    int b = 20;
    int c = 0;
    c = Sub(a,b);
    return 0;
}

剖析上面代码的运行过程:运行环境:VS2008

给出主函数的汇编代码

int main()
{
003113F0  push        ebp  
003113F1  mov         ebp,esp 
003113F3  sub         esp,0E4h 
003113F9  push        ebx  
003113FA  push        esi  
003113FB  push        edi  
003113FC  lea         edi,[ebp-0E4h] 
00311402  mov         ecx,39h 
00311407  mov         eax,0CCCCCCCCh 
0031140C  rep stos    dword ptr es:[edi] 
    int a = 10;
0031140E  mov         dword ptr [a],0Ah 
    int b = 20;
00311415  mov         dword ptr [b],14h 
    int c = 0;
0031141C  mov         dword ptr [c],0 
    c = Sub(a,b);
00311423  mov         eax,dword ptr [b] 
00311426  push        eax  
00311427  mov         ecx,dword ptr [a] 
0031142A  push        ecx  
0031142B  call        Sub (31108Ch) 
00311430  add         esp,8 
00311433  mov         dword ptr [c],eax 
    return 0;
00311436  xor         eax,eax 
}
00311438  pop         edi  
00311439  pop         esi  
0031143A  pop         ebx  
0031143B  add         esp,0E4h 
00311441  cmp         ebp,esp 
00311443  call        @ILT+315(__RTC_CheckEsp) (311140h) 
00311448  mov         esp,ebp 
0031144A  pop         ebp  
0031144B  ret             

接下来分析这段汇编代码:
在这里我们要知道在VC++下,连接器对控制台程序设置的入口函数是 _mainCRTStartup,mainCRTStartup 再调用main 函数;
所以当我们操作时,首先会给mainCRTStartup()函数开辟一段空间,然后esp和ebp在他们所在的位置
反汇编1.png
(1)push ebp
push就是压栈,把ebp的地址压入栈中,
注意:每次压栈后,esp都指向最新的栈顶位置。
(2)mov ebp,esp
使ebp=esp,即ebp也指向栈顶位置。
反汇编1.png
(3)为main函数预开辟空间

003113F3  sub         esp,0E4h 

反汇编1.png
(4)3个push 以及初始化开辟的空间

003113F9  push        ebx  
003113FA  push        esi  
003113FB  push        edi  
003113FC  lea         edi,[ebp-0E4h] 
00311402  mov         ecx,39h 
00311407  mov         eax,0CCCCCCCCh 
0031140C  rep stos    dword ptr es:[edi] 

1)3个push 分别把ebx,esi,edi 3个寄存器压入栈中。
2)lea 就是把 [ebp-0E4h]的地址放在edi中,ebp-0E4h是3个push之前esp的位置。
3)2个move操作,ecx寄存器的值为39h,eax为初始化值0CCCCCCCCh 。
4)然后rep stos操作:实际上就是把初始化开辟的空间,初始值为eax寄存器内的值0CCCCCCCCh,从edi开始(edi保存的esp的位置),向高地址的部分进行字节拷贝,每一次拷贝4个字节。拷贝的内容就是eax的内容,拷贝次数为39h次。
注意:用0xccccccccch初始化,所以未初始化的字符串,经常看到“烫烫”
反汇编1.png
可以查看内存值的变化,来验证
反汇编2.png
(5)实参入栈

int a = 10;
0031140E  mov         dword ptr [a],0Ah 
    int b = 20;
00311415  mov         dword ptr [b],14h 
    int c = 0;
0031141C  mov         dword ptr [c],0 

反汇编1.png
(6)调用Sub函数准备,形参入栈
形参从右向左入栈的,看出形参是实参的一份拷贝

    c = Sub(a,b);
00311423  mov         eax,dword ptr [b] 
00311426  push        eax  
00311427  mov         ecx,dword ptr [a] 
0031142A  push        ecx  

ebp-8就是b的位置,ebp-4就是a的位置
反汇编1.png
(7)call指令

0031142B  call        Sub (31108Ch) 
00311430  add         esp,8 

call指令就是把下一条指令add的地址31108Ch压入栈中
反汇编1.png

给出Sub函数的汇编代码:

int Sub(int x,int y)
{
003113A0  push        ebp  
003113A1  mov         ebp,esp 
003113A3  sub         esp,0CCh 
003113A9  push        ebx  
003113AA  push        esi  
003113AB  push        edi  
003113AC  lea         edi,[ebp-0CCh] 
003113B2  mov         ecx,33h 
003113B7  mov         eax,0CCCCCCCCh 
003113BC  rep stos    dword ptr es:[edi] 
    int t = 0;
003113BE  mov         dword ptr [t],0 
    t = x-y;
003113C5  mov         eax,dword ptr [x] 
003113C8  sub         eax,dword ptr [y] 
003113CB  mov         dword ptr [t],eax 
    return t;
003113CE  mov         eax,dword ptr [t] 
}
003113D1  pop         edi  
003113D2  pop         esi  
003113D3  pop         ebx  
003113D4  mov         esp,ebp 
003113D6  pop         ebp  
003113D7  ret       

进入Sub函数:
步骤其实大致和main函数一样
(1)为Sub函数准备

003113A0  push        ebp  

此时ebp指向的main函数的栈底指针

003113A1  mov         ebp,esp 
003113A3  sub         esp,0CCh 
003113A9  push        ebx  
003113AA  push        esi  
003113AB  push        edi  
003113AC  lea         edi,[ebp-0CCh] 
003113B2  mov         ecx,33h 
003113B7  mov         eax,0CCCCCCCCh 
003113BC  rep stos    dword ptr es:[edi] 

(2)以上代码与main函数2,3,4步骤差不多,简单说就是让ebp指向esp指向的位置,为Sum函数分配栈帧,esp指向栈顶,将寄存器ebx、esi、edi依次压入栈顶,edi的值是ebp-0CCh,然后依次从edi的值开始初始化开辟的空间。
反汇编1.png
(3)指向Sub函数,计算差值

int t = 0;
003113BE  mov         dword ptr [t],0 
t = x-y;
003113C5  mov         eax,dword ptr [x] 
003113C8  sub         eax,dword ptr [y] 
003113CB  mov         dword ptr [t],eax 
    return t;
003113CE  mov         eax,dword ptr [t] 

在VS中可以通过变量名找到地址。
把t初始化为0,然后计算t=x-y,把ebp+8(x)的值(a) 存放在eax,然后把eax值与ebp+12(y)的值(b)相减放在eax中,然后把eax值保存在t中返回值 t,把ebp-4内的值(t)取出放在eax中。
反汇编1.png
(4)函数调用结束,释放栈帧
这里先介绍一个概念:
现场保护 当出现中断时,把CPU现在的状态,也就是中断的入口地址保存在寄存器中,随后转向执行其他任务,当任务完成,从寄存器中取出地址继续执行。保护现场其实就是保存中断前一时刻的状态不被破坏。保护现场通过利用一系列PUSH指令保护CPU现场,即将相关寄存器的内容入栈保护起来。所以要把ebp 入栈push。

003113D1  pop         edi  
003113D2  pop         esi  
003113D3  pop         ebx  
003113D4  mov         esp,ebp 
003113D6  pop         ebp  

接下来的指令就是返回,先进行3次出栈,把栈顶的指令分别给了edi,esi,ebx三个寄存器。然后把ebp给了esp,这时也就是让esp指向了ebp的位置,这是ebp和esp指向同一位置,这个位置就是你所保存的main函数的ebp,然后再pop ebp,这样ebp就维护到main函数的栈帧了。

003113D7  ret    

在这,当ret指令执行之后,会pop一下,把这个地址pop以后,就从Sub函数返回了main函数,这也是最初为什么要保存这个地址的原因。这样call指令就完成了。此时指向main函数中call指令的下一条指令add。
(5)

00311430  add         esp,8 
00311433  mov         dword ptr [c],eax 
    return 0;
00311436  xor         eax,eax 
}
00311438  pop         edi  
00311439  pop         esi  
0031143A  pop         ebx  
0031143B  add         esp,0E4h 
00311441  cmp         ebp,esp 
00311443  call        @ILT+315(__RTC_CheckEsp) (311140h) 
00311448  mov         esp,ebp 
0031144A  pop         ebp  
0031144B  ret             

main函数中:
esp+8 :把形参a,b 释放
mov dword ptr [c],eax:把eax中值(返回值t)保存在ebp-12(c的位置)中
接下来,和对Sub函数的返回类似,对main函数返回,然后再销毁main函数,执行ret指令。

参考
http://m.blog.csdn.net/wenqiang1208/article/details/74353303
https://segmentfault.com/a/1190000007977460
继续学习:
http://www.cnblogs.com/java20130723/archive/2013/07/24/3211358.html
http://blog.csdn.net/jelly_9/article/details/53239718
http://blog.csdn.net/rxan1234/article/details/49862043
http://15129279495.blog.51cto.com/10845420/1735749

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值