浅谈函数调用,开辟栈帧的过程

函数的调用过程其实就是开辟栈帧的过程,首先了解一下栈的特性和几个常用的寄存器。

  • 栈:栈先进先出。
  • 栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),我们称为栈底指针,寄存器esp指向当前的栈帧的顶部(低地址),我们称为栈顶指针。
  • 通用寄存器:eax ebx ecx edx.
  • eip(程序计数器):存放当前正在执行指令的下一条指令的地址。

在函数调用开辟栈帧之前再了解一下地址空间。

一个由C/C++编译的程序占用的内存分为以下几个部分:
1、栈区(stack)— 由编译器自动分配释放 ,存放为运行函数而分配的局
部变量、函数参数、返回数据、返回地址等。其操作方式类似于数据结构中的
栈。
2、堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束
时可能由OS回收 。分配方式类似于链表。
3、全局区(静态区)(static)—存放全局变量、静态数据、常量。程序结
束后由系统释放。
4、文字常量区 —常量字符串就是放在这里的。 程序结束后由系统释放。
5、程序代码区—存放函数体(类成员函数和全局函数)的二进制代码。
这里写图片描述

下面我们看一段代码:

#include<stdio.h>
int Sub(int x,int y)
{
    int z=0;
   z=x+y;
    return z;
}
int main()
{
    int a=10;
    int b=20;
    int ret=0;
    ret=Add(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 ret=0;
00401086   mov         dword ptr [ebp-0Ch],0
13:       ret=Add(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) 为函数预开辟空间

 add         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次。
这里写图片描述
(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 ret=0;
00401086   mov         dword ptr [ebp-0Ch],0

这里写图片描述
(6)调用sub函数准备,形参入栈
形参从右向左入栈的,看出形参是实参的一份拷贝

13:       c=Add(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指令
call指令就是把下一条指令add的地址0040109A压入栈中
这里写图片描述
(8)进入Add函数

2:    int Add(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 z=0;
00401038   mov         dword ptr [ebp-4],0
5:        z=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 z;
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) 为Add函数准备

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)指向Add函数,计算求和

4:        int z=0;
00401038   mov         dword ptr [ebp-4],0
5:        z=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 z;
00401048   mov         eax,dword ptr [ebp-4]

看出,计算机只认识地址,不认识变量名,
把t初始化为0,然后计算z=x+y,把ebp+8的值(a) 存放在eax,然后把eax值为ebp+12的值(b) 相加放在eax中,
然后把eax值保存在z中
返回值 z,把ebp-4内的值(z)取出放在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指令。
这里写图片描述

猜你喜欢

转载自blog.csdn.net/t595180928/article/details/80415132