【C语言内功心法】——函数栈帧的创建和销毁

简介: 你真的了解函数栈帧吗

前言

我们在C语言的学习过程中可能存在很多困惑
比如

  • 局部变量是怎么创建的?
  • 为什么局部变量不初始化时的值是随机值?
  • 函数是怎么传参的?传参顺序是什么样的?
  • 形参和实参是什么关系?
  • 函数调用是怎么做的?
  • 函数调用结束后是怎么返回的?

当我们学了今天的内容后,就会对这些内容有更深的理解

1.什么是函数栈帧

栈帧也叫过程活动记录,是编译器用来实现函数调用过程的一种数据结构。C语言中,每个栈帧对应着一个未运行完的函数。从逻辑上讲,栈帧就是一个函数执行的环境:函数调用框架、函数参数、函数的局部变量、函数执行完后返回到哪里等等。栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器ebp指向当前的栈帧的底部(高地址),寄存器esp指向当前的栈帧的顶部(低地址)。

2.汇编基础——寄存器和常用汇编指令

2.1寄存器是什么?

CPU由运算器、 控制器、 寄存器等器件构成,这些器件靠内部总线相连。内部总线实现CPU内部各个器件之间的联系, 外部总线实现CPU和主板上其他器件的联系。 简单地说, 在CPU中:

  • 运算器进行信息处理;
  • 寄存器进行信息存储;
  • 控制器控制各种器件进行工作;
  • 内部总线连接各种器件, 在它们之间进行数据的传送。
  • CPU中的主要部件是寄存器。 寄存器是CPU中程序员可以用指令读写的部件。 程序员通过改变各种寄存器中的内容来实现对CPU的控制。不同的CPU, 寄存器的个数、 结构是不相同的。

常见的寄存器

  • eax——累加和结果寄存器
  • ebx——数据指针寄存器
  • ecx——循环计数器
  • edx——i/o指针
  • esi——源地址寄存器
  • edi——目的地址寄存器
  • esp——栈顶寄存器
  • ebp——栈底寄存器

今天我们主要用到的寄存器就是==esp==和==ebp==,这两个寄存器中存放的地址是用来==维护函数栈帧的==。

2.2相关指令介绍

  1. mov——数据传送指令(将一个源操作数送到目的操作数中)
  2. push——将数据压入栈
  3. pop——从堆栈弹出数据至指定位置
  4. sub——减法指令
  5. add——加法指令
  6. call——实现对一个函数的调用
  7. jump——修改eip,转入目标函数进行调用
  8. ret——恢复返回地址,压入eip

3.栈帧的创建和销毁

函数栈帧创建和销毁的过程在不同环境的编译器下的实现也有所不同这里以VS2019为例

:cat:main函数的调用

这里我们以这段简单而详细的代码为例:

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>

int Add(int x, int y)
{
   
   
    int z = 0;
    z = x + y;
    return z;
}
int main()
{
   
   
    int a = 10;
    int b = 20;
    int c = Add(a, b);
    printf("%d\n", c);
    return 0;
}

1.首先我们按f10进入调试,找到调用堆栈
image.png

image.png

这里我们可以看到main函数是被一个叫invoke_main()的函数所调用。

2.接下来我们看一下main函数被调用的过程
image.png

image.png

image.png

3.1main函数的栈帧创建过程

我们进入调试,转到汇编语言,一探究竟

image.png

int main()
{
   
   
00C918B0  push        ebp  //将ebp的值压入栈顶,esp会指向ebp
00C918B1  mov         ebp,esp  //esp的值给ebp,ebp成为新的栈底
00C918B3  sub         esp,0E4h  //将esp的值减去0E4h(16进制数字),esp的值发生变化;
                           //新的esp和ebp所围成的空间即为main函数开辟的栈帧
00C918B9  push        ebx  //向栈顶压入ebx
00C918BA  push        esi  //向栈顶压入esi
00C918BB  push        edi  //向栈顶压入edi
                          //此时esp指向了edi所在的栈顶
00C918BC  lea         edi,[ebp-24h]  //将[ebp-24]这个地址放在edi里面  
00C918BF  mov         ecx,9  //9放在ecx中
00C918C4  mov         eax,0CCCCCCCCh  
00C918C9  rep stos    dword ptr es:[edi]  //从edi位置开始向下的ecx空间
                                           //全部初始化为 0CCCCCCCCh
00C918CB  mov         ecx,0C9C003h  
00C918D0  call        00C9131B  
    int a = 10;
00C918D5  mov         dword ptr [ebp-8],0Ah  //把0Ah放在[ebp-8]的位置
    int b = 20;
00C918DC  mov         dword ptr [ebp-14h],14h //把14h放在[ebp-14h]的位置
    int c = Add(a, b);

image.png

3.2Add函数的栈帧创建过程

int c = Add(a, b);
00C918E3  mov         eax,dword ptr [ebp-14h]  //把[ebp-14]也就是b的值
                                              //放到eax中
00C918E6  push        eax //将eax压入栈顶
00C918E7  mov         ecx,dword ptr [ebp-8]//把[ebp-8]也就是a的值放到ecx 中 
00C918EA  push        ecx//将ecx压入栈顶  
00C918EB  call        00C910B4//调用函数,call指令调用函数之前会把call
                             // 指令的下一条指令压入栈顶,目的是为了调用
                             //函数结束之后会回到call指令下一条指令处继续执行
00C918F0  add         esp,8

image.png

:pig:Add函数内部执行过程

00C91770  push        ebp// 来自main函数的ebp被压入栈顶
00C91771  mov         ebp,esp//esp的值给ebp,ebp成为新的栈底  
00C91773  sub         esp,0CCh//将esp的值减去0cch(esp和ebp之间的空间
                              //为Add函数的调用分配函数栈帧)  
00C91779  push        ebx //向栈顶压入ebx 
00C9177A  push        esi //向栈顶压入esi 
00C9177B  push        edi //向栈顶压入edi
                          //此时esp指向了edi所在的栈顶 
00C9177C  lea         edi,[ebp-0Ch]//将[ebp-0ch]这个地址放在edi里面   
00C9177F  mov         ecx,3 //3放在ecx中 
00C91784  mov         eax,0CCCCCCCCh  
00C91789  rep stos    dword ptr es:[edi]//从edi位置开始向下的ecx空间
                                        //全部初始化为 0CCCCCCCCh  
00C9178B  mov         ecx,0C9C003h  
00C91790  call        00C9131B

image.png

00C91790  call        00C9131B  
    int z = 0;
00C91795  mov         dword ptr [ebp-8],0//将z放入[ebp-8]这块空间  
    z = x + y;
00C9179C  mov         eax,dword ptr [ebp+8]//[ebp+8]的值放入eax中 
00C9179F  add         eax,dword ptr [ebp+0Ch]//将[ebp+0Ch]
                                             //也就是20加到eax中去=30  
00C917A2  mov         dword ptr [ebp-8],eax//再将eax的值放在
                                           //[ebp-8]也就是z中  
    return z;
00C917A5  mov         eax,dword ptr [ebp-8]//把[ebp-8]的值放在eax中去

image.png

3.3Add函数栈帧的销毁

00C917A8  pop         edi//把栈顶元素弹出放到edi中 
00C917A9  pop         esi//把栈顶元素弹出放到esi中  
00C917AA  pop         ebx//把栈顶元素弹出放到ebx中  
00C917B8  mov         esp,ebp//将ebp赋给esp,回收了Add开辟的空间  
00C917BA  pop         ebp//弹出栈顶的值存放到ebp中,ebp指向了
                         //mian函数栈帧的栈底  
00C917BB  ret//从栈顶弹出call指令下一条指令的地址,
               //直接跳转到该指令处,继续执行

image.png

回到主函数

00C918F0  add         esp,8//esp指向esp+8的地址,相当于把
                           //形参x,y的空间还给了操作系统 
00C918F3  mov         dword ptr [ebp-20h],eax//把eax的值
                                            //放到[ebp-20h]这块空间

image.png

4.总结

1. 局部变量的创建
首先为函数分配好栈帧空间,然后给局部变量在栈帧中分配一些空间。
2.未初始化时局部变量的值为什么是随机值
函数栈帧被创建后编译器会在栈帧所在的空间放入随机值。
image.png

3.函数是怎么传参的,传参的顺序是什么:
当还未调用函数时,参数就会被从右向左依次压栈,当真正进入形参函数时,在被调函数的函数栈帧中,通过指针的偏移量找到形参。
4.形参和实参的关系:形参是在压栈时开辟的空间,形参和实参在值上是相同的,但它的空间是独立的,所以形参是实参的一份临时拷贝,改变形参并不会影响实参。
5.函数调用的结果是怎么返回的
在函数调用之前把call指令下一条指令的地址存进了栈中,使得函数调用结束返回时就可以跳转到该地址处,让函数调用可以返回,返回值通过寄存器带回。

到这里函数栈帧的创建和销毁就结束了,如果有大家觉得有帮助的话还望多多支持

相关文章
|
6月前
|
存储 安全 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-2
|
6月前
|
存储 编译器 C语言
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
深度剖析c语言程序 -- 函数栈帧的创建和销毁(纯肝货)-1
|
6月前
|
存储 小程序 编译器
c语言内功修炼--深度剖析数据的存储
c语言内功修炼--深度剖析数据的存储
|
3月前
|
存储 C语言
【C语言】——函数栈帧的创建与销毁
【C语言】——函数栈帧的创建与销毁
|
6月前
|
存储 编译器 C语言
C语言内功修炼--指针详讲(进阶)
C语言内功修炼--指针详讲(进阶)
|
6月前
|
存储 C语言
C语言内功修炼---指针详讲(初阶)
C语言内功修炼---指针详讲(初阶)
|
6月前
|
存储 编译器 C语言
C语言:底层剖析——函数栈帧的创建和销毁
C语言:底层剖析——函数栈帧的创建和销毁
|
6月前
|
存储 编译器 程序员
C语言之反汇编查看函数栈帧的创建与销毁(二)
C语言之反汇编查看函数栈帧的创建与销毁(二)
|
1月前
|
C语言 C++
C语言 之 内存函数
C语言 之 内存函数
34 3
|
7天前
|
C语言
c语言调用的函数的声明
被调用的函数的声明: 一个函数调用另一个函数需具备的条件: 首先被调用的函数必须是已经存在的函数,即头文件中存在或已经定义过; 如果使用库函数,一般应该在本文件开头用#include命令将调用有关库函数时在所需要用到的信息“包含”到本文件中。.h文件是头文件所用的后缀。 如果使用用户自己定义的函数,而且该函数与使用它的函数在同一个文件中,一般还应该在主调函数中对被调用的函数做声明。 如果被调用的函数定义出现在主调函数之前可以不必声明。 如果已在所有函数定义之前,在函数的外部已做了函数声明,则在各个主调函数中不必多所调用的函数在做声明
22 6