关于C函数的调用过程-栈帧

来源:互联网 发布:淘宝客服人工电话多少 编辑:程序博客网 时间:2024/05/16 11:15

关于栈帧,从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),我们称为栈底指针,寄存器esp指向当前的栈帧的顶部(低地址),我们称为栈顶指针。
注意:EBP指向当前位于系统栈最上边一个栈帧的底部,而不是系统栈的底部。严格说来,“栈帧底部”和“栈底”是不同的概念;ESP所指的栈帧顶部和系统栈的顶部是同一个位置。

接下来我们通过下面的这个小程序来进行分析这个过程。
测试环境:VC6.0

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

首先拿出这段程序的汇编代码

9:    {00401060   push        ebp00401061   mov         ebp,esp00401063   sub         esp,4Ch00401066   push        ebx00401067   push        esi00401068   push        edi00401069   lea         edi,[ebp-4Ch]0040106C   mov         ecx,13h00401071   mov         eax,0CCCCCCCCh00401076   rep stos    dword ptr [edi]10:       int a=10;00401078   mov         dword ptr [ebp-4],0Ah11:       int b=20;0040107F   mov         dword ptr [ebp-8],14h12:       int c=0;00401086   mov         dword ptr [ebp-0Ch],013:       c=Sub(a,b);0040108D   mov         eax,dword ptr [ebp-8]00401090   push        eax00401091   mov         ecx,dword ptr [ebp-4]00401094   push        ecx00401095   call        @ILT+0(_Sub) (00401005)0040109A   add         esp,80040109D   mov         dword ptr [ebp-0Ch],eax14:15:       return 0;

接下来,我们对此进行分析:
在这里我们要知道在VC++下,连接器对控制台程序设置的入口函数是 mainCRTStartup,mainCRTStartup 再调用main 函数;
所以当我们操作时,首先会给mainCRTStartup()函数开辟一段空间,然后esp和ebp在他们所在的位置。
这里写图片描述

push        ebp

push意思是压栈的意思,我们在这里就可以理解为入栈操作,在这里就是把ebp栈,就是ebp的地址压入。

mov         ebp,esp

这个的意思就是上一栈帧的顶部,就是这个栈帧的底部。所以这时,ebp和esp都位于栈顶。
这里写图片描述

 sub         esp,4Ch push        ebx  push        esipush        edi

在这里的,第一句说的就是开辟空间4ch这么一块的大小,接下来继续三次压栈。

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

接下来所做的就是初始化的操作,

   lea         edi,[ebp-4Ch]

这一句话是把ebp-44h放到edi中

   mov         ecx,13h

这句话把13h放到ecx中去,ecx是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器。作循环用。

  mov         eax,0CCCCCCCCh

接下来把0CCCCCCCCh放到eax里面去,eax 是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器。

 rep stos    dword ptr [edi]

这句话的意思就是对edi开始,向高地址的部分进行字节拷贝,每一次拷贝4个字节。
拷贝的内容就是eax的内容,拷贝次数为13h次
这里写图片描述

VC 6.0通过查看内存验证效果:
这里写图片描述

10:       int a=10;00401078   mov         dword ptr [ebp-4],0Ah

在这就是把a放到ebp-4的位置

11:       int b=20;0040107F   mov         dword ptr [ebp-8],14h

在这就是把b放到ebp-8的位置

12:       int c=0;00401086   mov         dword ptr [ebp-0Ch],0

在这就是把c放到ebp-12的位置
这里写图片描述
在这其实把所有的变量转换成了地址,在这里我们要清楚机器不认识你的变量名,它认识的只是地址。

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

在这其实你就是传递参数,把两个参数进行了压栈
这里写图片描述
内存中的情况:
这里写图片描述

00401095   call        @ILT+0(_Sub) (00401005)0040109A   add         esp,80040109D   mov         dword ptr [ebp-0Ch],eax

call指令在这里会再次进行一次压栈,把它下面那条语句的地址压进去,也就是0040109A
这里写图片描述
在这是因为小端存储,所以出现到了这种情况。
接下来,就进入Sub()函数:

1:    #include<stdio.h>2:    int Sub(int x,int y)3:    {00401020   push        ebp00401021   mov         ebp,esp00401023   sub         esp,44h00401026   push        ebx00401027   push        esi00401028   push        edi00401029   lea         edi,[ebp-44h]0040102C   mov         ecx,11h00401031   mov         eax,0CCCCCCCCh00401036   rep stos    dword ptr [edi]4:        int t=0;00401038   mov         dword ptr [ebp-4],05:        t=x-y;0040103F   mov         eax,dword ptr [ebp+8]00401042   sub         eax,dword ptr [ebp+0Ch]00401045   mov         dword ptr [ebp-4],eax6:        return t;00401048   mov         eax,dword ptr [ebp-4]7:    }0040104B   pop         edi0040104C   pop         esi0040104D   pop         ebx0040104E   mov         esp,ebp00401050   pop         ebp00401051   ret
00401020   push        ebp

在这里再次进行压栈,在这首先压栈ebp,在这压进去的是main函数的ebp,这些ebp在后期都会有很大的作用。

内存效果图:
这里写图片描述

00401021   mov         ebp,esp00401023   sub         esp,44h00401026   push        ebx00401027   push        esi00401028   push        edi00401029   lea         edi,[ebp-44h]0040102C   mov         ecx,11h00401031   mov         eax,0CCCCCCCCh00401036   rep stos    dword ptr [edi]

这些指令进行的其实也就和上面我们对main()的分析一样在这移动ebp,然后进行压栈三次,然后进行初始化。

内存效果如图:
这里写图片描述

4:        int t=0;00401038   mov         dword ptr [ebp-4],0

在这把t放到ebp-4的位置

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

在这就相当于进行了对ebp+8保存的参数和ebp+12保存的参数进行了减法。然后,把算出来的结果,放到ebp-4的位置。也就是算出来的结果放到了t里面去
这里写图片描述

6:        return t;00401048   mov         eax,dword ptr [ebp-4]

在这把ebp-4的值放到寄存器eax返回,这的eax直到返回程序才会再次看到。在这要注意,这里不一定只用寄存器返回,毕竟寄存器是有大小的,太大也会放不下。通常情况下自定义类型返回采用寄存器,因为自定义类型比较小,32位平台下最大也只有8个字节

在这里要牵扯一个概念:
现场保护:当出现中断时,把CPU现在的状态,也就是中断的入口地址保存在寄存器中,随后转向执行其他任务,当任务完成,从寄存器中取出地址继续执行。保护现场其实就是保存中断前一时刻的状态不被破坏。保护现场通过利用一系列PUSH指令保护CPU现场,即将相关寄存器的内容入栈保护起来。
在这你就可以知道为什么对ebp要push进去。

7:    }0040104B   pop         edi0040104C   pop         esi0040104D   pop         ebx0040104E   mov         esp,ebp00401050   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指令就完成了。

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

接下来是分析main函数中的这两句,在这首先给esp+8,加了8之后,就把形参也弹出去,形参这是也没用了。
这里写图片描述
然后,ebp-0Ch,就是c,然后把eax放到c中,意思就是把刚才计算的结果放到c中。

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

博客写的肯定有不足指出,望大家多多指出!!

2 1