目录

前言

一、预备知识

1.内存区域的划分和分配

2.栈帧简介

3.寄存器简介

二、函数栈帧介绍

1.源代码

2.如何查看汇编代码

3.函数栈帧的创建与销毁(重点)

三、小彩蛋

总结


前言

最近在学习C语言的过程中遇到了一些问题,在询问老师和查询相关资料的基础上了解到了函数栈帧的相关概念,对下列问题也有了答案。

  1. 局部变量是如何创建的?
  2. 未初始化的局部变量为什么是随机值?(如果给一个变量未初始化,打印该变量中的内容就会出现一些没有实际意义的文字或字母)
  3. 函数是如何调用的?(过程是什么样的?)
  4. 函数在调用过程中是如何传参的?
  5. 形参和实参有什么联系和区别?(这两者有什么关系?)

如果你在学习过程中也产生了和我相同的疑惑,请阅读这篇文章,或许对你有所启发。

一、预备知识

在正式了解函数栈帧以前,我们需要先了解一些预备知识。

1.内存区域的划分和分配

内存按照内存地址从高(0xffffffff)到低(0x00000000)的顺序排列,可以分为五个大分区:

栈区堆区全局静态区常量区代码区。

大致分布如图:

我们本次所要了解的栈帧属于栈区。(因此本次只对栈区进行介绍,其他部分之后遇到了会进行补充)

栈区内容简介:

  • 栈区的内存空间由系统管理和分配,即方法调用开始时开辟空间,方法调用结束时回收空间。
  • 栈区是从高地址向低地址扩展,是一块连续的内存区域,遵循先进后出,后进先出(FILO)原则,使用效率高。
  • 方法的入参,内部定义的局部变量等,都存放在栈区。

2.栈帧简介

  • 栈帧也叫过程活动记录,是编译器用来实现过程/函数调用的一种数据结构。
  • 函数的每次调用,都有它自己独立的栈帧。栈帧中维持着函数调用所需要的各种信息,包括函数的入参、函数的局部变量、函数执行完成后下一步要执行的指令地址、寄存器信息等。
  • 栈帧使用了栈这一数据结构,达到了后进先出(First In Last Out)的内存管理原则。不管是插入数据还是删除数据,都是在栈顶进行的。栈顶和栈底都有指针,栈顶指针是esp,栈底指针是ebp,即esp指向栈顶,ebp指向栈底。

3.寄存器简介

我们所说的寄存器是CPU内部用来存放数据的一些小型存储区域,用来暂时存放参与运算的数据和运算结果。简单来说就是用来存储二进制代码的机器。

通常能了解到的eax,ebx,ecx,edx,esi,edi,edp,esp都是X86汇编语言中CPU上的32位的通用寄存器,如果站在C语言的角度,也可以将他们看做变量。举个例子:

add eax,-2;//可以认为是给变量eax加上-2这个值。

下面简单介绍几个寄存器:

  • EAX是"累加器"(accumulator), 它是很多加法乘法指令的缺省(默认)寄存器;

  • EBX 是"基地址"(base)寄存器, 在内存寻址时存放基地址;

  • ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器;

  • EDX 则总是被用来放整数除法产生的余数。

  • EBP是"基址指针"(BASE POINTER), 它最经常被用作高级语言函数调用的"框架指针"(frame pointer)。在破解的时候,经常可以看见⼀个标准的函数起始代码:

    push edp    //保存当前edp
    mov edp,esp //EBP设为当前栈指针
    sub esp,xxx //预留xxx字节给函数临时变量
    ……

    这样⼀来,EBP 构成了该函数的⼀个框架, 在EBP上方分别是原来的EBP, 返回地址和参数,BP下方则是临时变量。函数返回时作

    mov esp,ebp
    pop ebp
    ret

    即可。

  • ESP 专门用作堆栈指针,被形象地称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,ESP也就越来越小。在32位平台上,ESP每次减少4字节。

其中EBP和ESP需要重点了解一下。

二、函数栈帧介绍

每个函数被调用时都会建立栈帧,在接下来的调试过程中我将会进一步解释。(本次的代码调试我使用的环境是VS2013版,其他版本可能会有细微差别,但大体步骤和内容是类似的)

1.源代码

为了演示这次函数栈帧的创建与销毁,我们将以一次简单的程序来作为范例。

代码如下:

#include<stdio.h>
int ADD(int x, int y)
{int z = 0;z = x + y;return z;
}
int main()
{int a = 15;int b = 30;int c = 0;c = ADD(a, b);printf("%d\n", c);return 0;
}

2.如何查看汇编代码

为了理解每一行代码是怎样被计算机执行的,它的原理是什么,我们必须从源代码转化的汇编代码着手去了解(汇编语言相较于高级语言,更面向机器,底层逻辑更完善。通过了解汇编语言有助于我们了解代码的真正运行过程)。

将源代码转为汇编代码的步骤(以本例题为例):

  1. 在main函数的第一行设置断点;
  2. 按F10(Ctrl+Fn+F10)进入调试;
  3. 鼠标右击选择转到反汇编;
  4. 为了方便观察,在出现反汇编代码后,可以选择取消显示符号名。

                             

                 

3.函数栈帧的创建与销毁(重点)

该程序的汇编代码如下:(注释有每一步的原理)

--- d:\c语言\函数栈帧hszz\函数栈帧hszz\hszz.c --------------------------------------------
int main()
{
00E91410  push        ebp  //把edp压入栈顶,此时esp指向ebp的地址
00E91411  mov         ebp,esp  //把esp的值赋值给ebp,(寄存器放了谁的地址就指向谁)即ebp所指向的地址和esp相同
00E91413  sub         esp,0E4h  //将esp所指向的地址减0E4h位字节,这一步是为mian函数在栈区预创建空间
00E91419  push        ebx  //把ebx压入栈中
00E9141A  push        esi  //把esi压入栈中
00E9141B  push        edi  //把edi压入栈中
00E9141C  lea         edi,[ebp+FFFFFF1Ch]  //lea(load Effective Adress的简称)取有效地址,将ebp+FFFFFF1Ch的有效地址传给edi。
00E91422  mov         ecx,39h  //把39h赋值给ecx
00E91427  mov         eax,0CCCCCCCCh  //把0CCCCCCCCh赋值给eax
00E9142C  rep stos    dword ptr es:[edi]  //dword ptr(double word pointer的缩写)双字指针,(一个字是两个字节,两个字就是四个字节)。stos指令是指把eax的值拷贝到es:[edi]指向的地址(edi所指向的地址到edp所指向的地址)ecx是重复的次数。int a = 15;
00E9142E  mov         dword ptr [ebp-8],0Fh  //给变量a赋值,a = 15;这里a变量的地址为ebp-8;要注意的是变量a为双字变量,所以a占四个字节int b = 30;
00E91435  mov         dword ptr [ebp-14h],1Eh  //同上,变量b地址为ebp-14hint c = 0;
00E9143C  mov         dword ptr [ebp-20h],0  //同上,变量c地址为ebp-20hc = ADD(a, b);
00E91443  mov         eax,dword ptr [ebp-14h]  //把b的值也就是30赋值给eax
00E91446  push        eax  //把eax压入栈顶
00E91447  mov         ecx,dword ptr [ebp-8]  //把a的值也就是15赋值给ecx
00E9144A  push        ecx  //把ecx压入栈顶
00E9144B  call        00E91127  //call指令开始调用函数,同时push call指令的下一条地址(00E91127)。这样调用完函数以后可以找到call指令的下一条地址,继续执行程序int ADD(int x, int y)
{
00E913C0  push        ebp  //把ebp压入栈顶(从这里开始是为ADD函数开辟栈内空间,过程和给main函数创建栈内空间类似,以下不进行赘述)
00E913C1  mov         ebp,esp  //同main函数
00E913C3  sub         esp,0CCh  //同main函数
00E913C9  push        ebx  //同main函数
00E913CA  push        esi  //同main函数
00E913CB  push        edi //同main函数
00E913CC  lea         edi,[ebp+FFFFFF34h]  //同main函数
00E913D2  mov         ecx,33h  //同main函数
00E913D7  mov         eax,0CCCCCCCCh  //同main函数
00E913DC  rep stos    dword ptr es:[edi]  //同main函数int z = 0;
00E913DE  mov         dword ptr [ebp-8],0  //建立z变量赋值为0z = x + y;
00E913E5  mov         eax,dword ptr [ebp+8]  //将a的值也就是15赋值给eax(由此可见,函数传参时并不是把实参变量直接传给函数,而是通过寄存器将变量的值进行了临时拷贝并且传给函数,即形参是实参的临时拷贝)
00E913E8  add         eax,dword ptr [ebp+0Ch]  //将a+b的值赋值给eax
00E913EB  mov         dword ptr [ebp-8],eax  //把eax的值也就是a+b的值赋值给变量zreturn z;
00E913EE  mov         eax,dword ptr [ebp-8]  把c的值赋值给eax(eax在main函数的栈帧中)
}
00E913F1  pop         edi  //把edi从栈中弹出(删除)
}
00E913F2  pop         esi  //把esi从栈中弹出(删除)
00E913F3  pop         ebx  //把ebx从栈中弹出(删除)
00E913F4  mov         esp,ebp  //把ebp的值赋值给esp,即esp指向ebp的地址
00E913F6  pop         ebp  //把ebp从栈中弹出(删除)
00E913F7  ret  //ADD函数调用结束返回main函数00E91450  add         esp,8  //给esp+8,也就是让esp朝栈底方向移动8个字节(因为我们把两个存着双字变量的值的寄存器eax和ecx也压入栈了,现在函数调用结束,我们就不需要那两个寄存器存储双字变量的值了,所以esp+8)
00E91453  mov         dword ptr [ebp-20h],eax  //把eax的值也就是z的值赋值给变量cprintf("%d\n", c);
00E91456  mov         esi,esp  //后面的内容是销毁main函数,过程和销毁ADD函数类似,因此以下不再赘述
00E91458  mov         eax,dword ptr [ebp-20h]
00E9145B  push        eax
00E9145C  push        0E95858h
00E91461  call        dword ptr ds:[00E99114h]
00E91467  add         esp,8
00E9146A  cmp         esi,esp
00E9146C  call        00E9113B  return 0;
00E91471  xor         eax,eax
}
00E91473  pop         edi
00E91474  pop         esi
00E91475  pop         ebx
}
00E91476  add         esp,0E4h
00E9147C  cmp         ebp,esp
00E9147E  call        00E9113B
00E91483  mov         esp,ebp
00E91485  pop         ebp  //把ebp从栈中弹出(删除)
00E91486  ret  

三、小彩蛋

main函数可以调用别的函数,它自己也是可以被调用的。

main
__tmainCRTStartup
__mainCRTSartup

总结

以上就是今天要讲的内容,本文用一个范例介绍了函数栈帧的创建与销毁,文章开头所提出的问题也在文章正文中做出了解答。

本文的作者也只是一个正在学习C语言等编程知识的萌新,若这篇文章中有哪些不正确的内容,请在评论区向作者指出(也可以私信作者),欢迎大佬们指点,也欢迎其他正在学习C语言的萌新和作者进行交流。

最后,如果本篇文章对你有所启发的话,也希望可以支持支持作者,后续作者也会定期更新学习记录。谢谢大家!

函数栈帧的创建与销毁相关推荐

  1. 函数调用过程详解:函数栈帧的创建与销毁

    前言:我们在学习C语言的过程中,可以会产生很多疑问,比如: 局部变量是怎么创建的 为什么局部变量的值不做初始化就是随机值 函数是怎么传参的?传参的顺序是怎么样的? 形参和实参是什么关系? 函数调用是怎 ...

  2. (动图详解)汇编视角观察函数栈帧的创建和销毁

    目录 ​1.阅读本文的价值 ​2.函数栈帧及栈的概念 ​3.部分寄存器及汇编指令 ​4.main函数的调用 5.main函数的栈帧创建 ​6.变量的栈帧创建 ​6.函数传参 ​7.函数内部运算及销毁 ...

  3. 程序员内功心法之函数栈帧的创建和销毁

    目录 1.本节目标 2.相关寄存器 3.相关汇编指令 4.什么是函数栈帧 5.什么是调用堆栈 6.函数栈帧的创建和销毁 (1).main函数栈帧的创建与初始化 (2).main函数的核心代码 (3). ...

  4. 【C语言】程序员筑基功法——《函数栈帧的创建与销毁》

    <函数栈帧的创建与销毁> 文章目录 1. 前言 2. 问题引入 3. 前提准备 3.1 寄存器 3.2 汇编指令 4. 函数栈帧的维护 5. 如何调用堆栈 6. 函数栈帧的创建和销毁 6. ...

  5. C语言内功修炼之函数栈帧的创建与销毁(举例加图解)

    大家可能会函数栈帧不了解,可能都没有听过这个,不用着急,在理解函数栈帧之前,我们先来了解一下程序对内存使用的分区大概情况:  区域 作用 栈区(stack) 由编译器自动分配和释放,存放函数的参数值, ...

  6. 函数栈帧的创建和销毁图解

    目录 一.问题: 二.寄存器 栈区 1.寄存器有哪些?有什么作用? 2.编译环境 3.栈区的使用习惯: 4.main函数也是被其他函数调用的 5.汇编代码 三.为main函数创建栈帧 1.main函数 ...

  7. 程序员内功修炼——函数栈帧的创建与销毁

    一.什么是函数的栈帧 c语言是由函数构成的,那么函数是如何进行传参的?如何调用的?如何返回值的?这些问题与函数的栈帧有关. 函数栈帧:就是函数调用过程中程序的调用栈所开辟的空间,这些空间用来存放: 1 ...

  8. 内功修炼《函数栈帧的创建和销毁》建议收藏

    文章目录 前言 一. 寄存器的概念 二. 通用寄存器的结构 三. 指针寄存器和变址寄存器 四. EBP和ESP 五.总结 前言 在前期的学习过程中,我们可能会有很多的困惑: 1️⃣ 局部变量是怎么创建 ...

  9. 一文带你深入了解函数栈帧的创建和销毁

    作者介绍:友友们好我是沐曦希,可以叫我小沐

最新文章

  1. ASP.NET AJAX学习记要(2)-下手之DOM
  2. android官方夜间模式,Android夜间模式实践
  3. Google 发布网页统计报告
  4. python绘图函数教程_OpenCV中的绘图函数
  5. CSS之不使用Media Queries的自适应CSS
  6. python定义三维数组不指定长度_插值/调整三维数组大小
  7. “白领复工10大热销商品”榜单:口罩及相关商品位居第一
  8. No input file specified的解决方法
  9. 1 | GNN基础理论
  10. Spring.NET 中的 ADO.NET 数据访问的示例
  11. php gtk 中文手册,PHP-GTK
  12. n次独立重复试验暨伯努利试验
  13. 英雄联盟修改服务器封3年,LOL自定义也被封三年 竟然因为这个原因?
  14. dataframe两个表合并_python处理DataFrame数据的一些常用操作
  15. aecmap快捷键_arcmap快捷键
  16. 9.7 电机控制程序基础
  17. 【测绘程序设计】——椭球面弧长计算
  18. Spring Cloud ZooKeeper Discovery Client Not Register on ZooKeeper when using SpringBootServletInitia
  19. JOBDU-OJ 1456 胜利大逃亡
  20. C++ 不要忘记指针变量的初始化

热门文章

  1. Adobe RIA 开发工程师认证考试大纲
  2. 说说Android的广播(3) - 什么样的广播是并发的?
  3. 衡量预测变量/自变量重要性
  4. 最近在看些远程办公的机会
  5. R-Breaker策略交易代码,适用MT4,MT5平台
  6. 中国三相电子负载限制器行业运营状况与需求前景预测报告(2022-2027)
  7. java计算机毕业设计计算机实验中心网站MyBatis+系统+LW文档+源码+调试部署
  8. 【C#基础1-8】C#的正则表达式
  9. 时钟电路程序设计c语言,STC89C52单片机简易时钟程序电路设计(附源代码和电路图)...
  10. 递归下楼梯方法 5: Binets 方法