天天看点

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

每次函数调用,都为函数开辟一块空间,成为栈帧。

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

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

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

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

#include<stdio.h>
int Sub(int x,int y)
{
    int t=;
    t=x-y;
    return t;
}
int main()
{
    int a=;
    int b=;
    int c=;
    c=Sub(a,b);

    return ;
}
           

给出这段代码的汇编代码

:    int main()
:    {
   push        ebp
   mov         ebp,esp
   sub         esp,Ch
   push        ebx
   push        esi
   push        edi
   lea         edi,[ebp-Ch]
C   mov         ecx,h
   mov         eax,CCCCCCCCh
   rep stos    dword ptr [edi]
:       int a=;
   mov         dword ptr [ebp-],Ah
:       int b=;
F   mov         dword ptr [ebp-],h
:       int c=;
   mov         dword ptr [ebp-Ch],
:       c=Sub(a,b);
D   mov         eax,dword ptr [ebp-]
   push        eax
   mov         ecx,dword ptr [ebp-]
   push        ecx
   call        @ILT+(_Sub) ()
A   add         esp,
D   mov         dword ptr [ebp-Ch],eax
:
:       return ;
           

接下来分析这段汇编代码

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

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

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

(1) push ebp

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

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

(2) mov ebp,esp

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

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

(3) 为函数预开辟空间

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

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

push        ebx
push        esi
push        edi
lea         edi,[ebp-Ch]
mov         ecx,h
mov         eax,CCCCCCCCh
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初始化,所以未初始化的字符串,经常看到“烫烫”

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

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

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

(5)实参入栈

10:       int a=;
00401078   mov         dword ptr [ebp-4],0Ah
11:       int b=;
0040107F   mov         dword ptr [ebp-8],14h
12:       int c=;
00401086   mov         dword ptr [ebp-0Ch],0
           
【C语言】函数运行过程-----栈帧调用

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

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

:       c=Sub(a,b);
D   mov         eax,dword ptr [ebp-]
   push        eax
   mov         ecx,dword ptr [ebp-]
   push        ecx
           

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

(7)call指令

00401095   call        @ILT+(_Sub) ()
A   add         esp,
           

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

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

(8)进入sub函数

:    int Sub(int x,int y)
:    {
   push        ebp
   mov         ebp,esp
   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;
8   mov         dword ptr [ebp-],
:        t=x-y;
F   mov         eax,dword ptr [ebp+]
   sub         eax,dword ptr [ebp+0Ch]
00401045   mov         dword ptr [ebp-4],eax
6:        return t;
8   mov         eax,dword ptr [ebp-]
:    }
B   pop         edi
C   pop         esi
D   pop         ebx
E   mov         esp,ebp
   pop         ebp
   ret
           

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

(8.1) 为sub函数准备

push        ebp
           

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

mov         ebp,esp
   sub         esp,h
   push        ebx
   push        esi
   push        edi
   lea         edi,[ebp-h]
C   mov         ecx,h
   mov         eax,CCCCCCCCh
   rep stos    dword ptr [edi]
           

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

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

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

4:        int t=;
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

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

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

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

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

main函数中

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

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

接下来,和对函数的返回类似,对main()函数的返回,然后再销毁main()函数,执行ret指令。