您的位置:首页 > 编程语言 > C语言/C++

【C语言】函数运行过程-----栈帧调用

2017-07-04 19:17 302 查看
每次函数调用,都为函数开辟一块空间,成为栈帧。

首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),我们称为栈底指针,寄存器esp指向当前的栈帧的顶部(低地址),我们称为栈顶指针。

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

给段代码,剖析下面函数运行过程。

运行环境:VC6.0,(相比VS,更容易查看内存)

#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;
}


给出这段代码的汇编代码

8:    int main()
9:    {
00401060   push        ebp
00401061   mov         ebp,esp
00401063  sub         esp,4Ch
00401066   push        ebx
00401067   push        esi
00401068   push        edi
00401069   lea         edi,[ebp-4Ch]
0040106C   mov         ecx,13h
00401071   mov         eax,0CCCCCCCCh
00401076   rep stos    dword ptr [edi]
10:       int a=10;
00401078   mov         dword ptr [ebp-4],0Ah
11:       int b=20;
0040107F   mov         dword ptr [ebp-8],14h
12:       int c=0;
00401086   mov         dword ptr [ebp-0Ch],0
13:       c=Sub(a,b);
0040108D   mov         eax,dword ptr [ebp-8]
00401090   push        eax
00401091   mov         ecx,dword ptr [ebp-4]
00401094   push        ecx
00401095   call        @ILT+0(_Sub) (00401005)
0040109A   add         esp,8
0040109D   mov         dword ptr [ebp-0Ch],eax
14:
15:       return 0;


接下来分析这段汇编代码

在这里我们要知道在VC++下,连接器对控制台程序设置的入口函数是 mainCRTStartup,mainCRTStartup 再调用main 函数;

所以当我们操作时,首先会给mainCRTStartup()函数开辟一段空间,然后esp和ebp在他们所在的位置



(1) push ebp

push就是压栈,把ebp 的地址压入栈中,

注:每次压栈后,esp都指向最新的栈顶位置

(2) mov ebp,esp

使ebp=esp,即ebp也指向栈顶位置



(3) 为函数预开辟空间

sub         esp,4Ch




(4)3个push 以及初始化开辟的空间

push        ebx
push        esi
push        edi
lea         edi,[ebp-4Ch]
mov         ecx,13h
mov         eax,0CCCCCCCCh
rep stos    dword ptr [edi]


解释一下,3个push 分别把ebx,esi,edi 3个寄存器压入栈中。

lea 就是把 [ebp-4Ch]的地址放在edi中,ebp-4Ch是3个push之前esp的位置

2个move操作,ecx寄存器的值为13h,eax为初始化值0ccccccccch

然后rep stos:实际上就是把初始化开辟的空间,初始值为eax寄存器内的值0CCCCCCCCh,

从edi开始(edi保存的esp的位置),向高地址的部分进行字节拷贝,每一次拷贝4个字节。

拷贝的内容就是eax的内容,拷贝次数为13h次。

注:用0xccccccccch初始化,所以未初始化的字符串,经常看到“烫烫”



可以查看内存值的变化,来验证



(5)实参入栈

10:       int a=10;
00401078   mov         dword ptr [ebp-4],0Ah
11:       int b=20;
0040107F   mov         dword ptr [ebp-8],14h
12:       int c=0;
00401086   mov         dword ptr [ebp-0Ch],0




(6)调用sub函数准备,形参入栈

形参从右向左入栈的,看出形参是实参的一份拷贝

13:       c=Sub(a,b);
0040108D   mov         eax,dword ptr [ebp-8]
00401090   push        eax
00401091   mov         ecx,dword ptr [ebp-4]
00401094   push        ecx


ebp-8就是b的位置,ebp-4就是a的位置

(7)call指令

00401095   call        @ILT+0(_Sub) (00401005)
0040109A   add         esp,8


call指令就是把下一条指令add的地址0040109A压入栈中



(8)进入sub函数

2:    int Sub(int x,int y)
3:    {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,44h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-44h]
0040102C   mov         ecx,11h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]
4:        int t=0;
00401038   mov         dword ptr [ebp-4],0
5:        t=x-y;
0040103F   mov         eax,dword ptr [ebp+8]
00401042   sub         eax,dword ptr [ebp+0Ch]
00401045   mov         dword ptr [ebp-4],eax
6:        return t;
00401048   mov         eax,dword ptr [ebp-4]
7:    }
0040104B   pop         edi
0040104C   pop         esi
0040104D   pop         ebx
0040104E   mov         esp,ebp
00401050   pop         ebp
00401051   ret


步骤其实大致和main函数一样

(8.1) 为sub函数准备

00401020   push        ebp


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

00401021   mov         ebp,esp
00401023   sub         esp,44h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-44h]
0040102C   mov         ecx,11h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]


以上代码 就不细细分析,大概和main函数2,3,4步骤差不多



(8.2)指向sub函数,计算差值

4:        int t=0;
00401038   mov         dword ptr [ebp-4],0
5:        t=x-y;
0040103F   mov         eax,dword ptr [ebp+8]
00401042   sub         eax,dword ptr [ebp+0Ch]
00401045   mov         dword ptr [ebp-4],eax
6:        return t;
00401048   mov         eax,dword ptr [ebp-4]


看出,计算机只认识地址,不认识变量名,

把t初始化为0,然后计算t=x-y,把ebp+8的值(a) 存放在eax,然后把eax值为ebp+12的值(b) 相减 放在eax中,

然后把eax值保存在t中

返回值 t,把ebp-4内的值(t)取出放在eax中

(9)函数调用结束,释放栈帧

这里先介绍一个概念

现场保护 当出现中断时,把CPU现在的状态,也就是中断的入口地址保存在寄存器中,随后转向执行其他任务,当任务完成,从寄存器中取出地址继续执行。保护现场其实就是保存中断前一时刻的状态不被破坏。保护现场通过利用一系列PUSH指令保护CPU现场,即将相关寄存器的内容入栈保护起来。

所以要把ebp 入栈push

0040104B   pop         edi
0040104C   pop         esi
0040104D   pop         ebx
0040104E   mov         esp,ebp
00401050   pop         ebp


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

00401051   ret


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

0040109A   add         esp,8
0040109D   mov         dword ptr [ebp-0Ch],eax


main函数中

esp+8 :把形参a,b 释放

mov dword ptr [ebp-0Ch],eax:把eax中值(返回值t)保存在ebp-12(c的位置)中

接下来,和对函数的返回类似,对main()函数的返回,然后再销毁main()函数,执行ret指令。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: