• C/C++函数调用过程中栈的变化


         不知道大家有没有思考过一个问题,就是进程在内存中到底是怎么样的,具体内存是怎么分配的?

         我就是突然萌发了这种想法,于是我就从网上开始找相关的资料,下面我就把我理解的函数调用过程中栈的变化写下来。

    1、进程在内存中的情况

         首先需要知道的就是当创建了一个进程后,这个进程在内存中的布局到底是什么样的?我就借用网上盗来的图展示出来。

                                            

        这个图是一个CSDN博主的,因为忘了从哪里找来的,所以在这里我先对那个博主说声抱歉,我实在找不到你的博客了,请你原谅,我这里就

    引用你的博客里面的一些内容了。好了,上面这个图就是一个进程在内存中的分布。

    .text(代码段):里面存放着程序代码(机器码),CPU取指令就是在这里取的,因此这块内存只能读。

    .data和.bbs(数据段):这两块放一起就叫数据段,其中.data存放的是已经初始化的全局变量和静态变量,而.bbs段存放的是未初始化的全局和

    静态变量。

    heap(堆):这个就是我们在写程序的时候使用new,malloc,分配的空间就是在这里,从图中示意的箭头就看出来堆的生长方向(堆的增长方向)

    是从低地址往高地址方向。在这个进程结束后,这个堆还是会被操作系统回收的。

    stack(栈):栈的生长方向和堆的生长方向相反,从高地址往低地址,这里面存放的就是函数调用所传递的参数、函数的返回地址和局部变量等。

    2、函数调用过程

           有了上面的进程在内存中的分布,下面就介绍当进程执行的时候,也就是调用函数的时候,函数的栈到底是怎么变化的?在讲函数调用过程

    之前我需要讲函数的调用约定。

             我就介绍经常用到的两种函数调用约定:__cdecl和__stdcall,当然还有__fastcall和ATPCS约定。

    __cdecl:这个函数调用约定是C/C++编译器默认的调用约定,这个约定就是函数参数从右往左依次入栈,但由调用者负责清除栈的内容。

    __stdcall:WIN API基本上都采用这种函数调用约定,这个约定也是函数参数从右往左依次入栈,但由被调函数负责清除栈的内容。

            可能有的同学有疑惑,负责清除栈的内容是什么鬼?为什么有两种调用约定?

            我就根据我浅薄的理解解释一下:这两种约定为什么一个是调用函数负责清除栈,而一个是被调函数清除栈。当我们在使用printf函数的时候是

    不是我们可以传递很多参数,printf("%d %f %d",a,b,c)而这种不确定参数的函数调用如果用__stdcall,那被调用函数(printf)不知道给它传递了多少

    参数,那么让它自己清除栈空间是不是会出错,这就是__cdecl调用和__stdcall的区别。__stdcall调用约定使得跨平台变得容易,函数自己清除自己的

    栈,这样兼容性变强,更好地移植。可能还有同学不懂,为什么要清除栈?为什么是调用者清除?为什么是被调函数清除?下面就介绍函数调用过程。

    我的理解都是看了这个大神的文章:https://blog.csdn.net/IT_10/article/details/52986350,里面讲解的非常详细。如果有汇编语言基础的同学看起来很

    容易,也很快能理解,如果没有学习过汇编的同学可能就吃力一些了。这里我就大致总结一下调用过程。

            首先介绍几个基本的概念:

            EBP:基址指针寄存器,里面保存的栈低的地址,就可以认为它指向栈低(就是我们学的数据结构里面的栈低指针)

            ESP:堆栈指针寄存器,里面保存的栈顶的地址,认为它指向栈顶(就是栈顶指针)

            因为我们不是学习汇编,只要知道这两个寄存器是怎么回事就可以了,其他的不用过多考虑(当然,感兴趣和想完全了解详细过程的同学可以自己

    学习剩下的几个寄存器),我们只是学会函数的调用栈变化,不同过多考虑其他的。

            还有一个问题,不知道大家想过没有,从上C语言的时候老师都告诉我们,程序的入口是main函数,那我们定义在main函数之外的全局变量谁来控制

    赋值的?难道就执行main函数之前就没有其他函数了吗?当然有了,就是mainCRTStartup函数,main函数也是被调用的函数。

    具体的函数调用过程的图解和汇编解释看大神的文章:https://blog.csdn.net/IT_10/article/details/52986350,我这里就简单总结一下,当一个函数调用另外

    一个函数时,栈的变化:比如main函数调用add(int a , int b)函数

    1)在main函数中压入a和b的参数值

    2)保存call指令的下一条指针的地址

    3)压入EBP,EBP=ESP,保存main的栈

    4)开辟add函数的栈空间

    5)add函数中的指令

    6)add函数保存计算值

    7)ESP=EBP,add函数开始退栈

    8)EBP出栈,就是还原main函数的栈底指针

    9)清除add函数栈空间,继续执行main

            我上面列出的步骤可能不好理解,如果感兴趣的同学可以把程序反编译出来看下汇编语言就懂了,因为上面内容都是根据我自己

    的理解,如果有不对的地方,还希望大家多多指正!

     

  • 相关阅读:
    strdup和strndup函数
    c# 获取客户端IP地址方法
    The 'Microsoft.Jet.OLEDB.4.0' provider is not registered on the local machine.报错解决办法
    C#将Excel数据表导入SQL数据库的两种方法(转)
    Winform 无法监听方向键(向上,向下,向左,向右)
    一个优秀的.net程序员必须要学会的技能 (转)-----参照学习目标
    iTextSharp 使用详解(转)
    mac 快捷键
    mvc 项目运行报错检查web.config
    C语言堆栈入门——堆和栈的区别 -- 转
  • 原文地址:https://www.cnblogs.com/cuglzf/p/8634360.html
Copyright © 2020-2023  润新知