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

本文详细解析C函数调用时的栈帧建立过程,包括函数参数、局部变量、返回地址等信息的存储。通过实例分析汇编代码,阐述了VC6.0环境下函数调用的内存布局、栈帧变化以及返回机制,同时介绍了现场保护的概念。
摘要由CSDN通过智能技术生成

关于栈帧,从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器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        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在他们所在的位置。
这里写图片描述

push        ebp

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

mov         ebp,esp

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

 sub         esp,4Ch
 push        ebx
  push        esi
push        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        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

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

1:    #include<stdio.h>
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
00401020   push        ebp

在这里再次进行压栈,在这首先压栈ebp,在这压进去的是main函数的ebp,这些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]

这些指令进行的其实也就和上面我们对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         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指令就完成了。

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

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值