内存和函数
创始人
2024-02-20 17:18:32
0

程序的内存布局 

        Linux默认情况下将高地址的1GB空间分配给内核,用户进程使用剩下2GB或者3GB的内存空间。在用户空间里,也有很多地址区间有特殊的地位,一般来讲,应用程序使用的内存空间里有如下"默认"的区域

        1、栈:用于维护函数调用的上下文,离开了栈函数调用就没法实现。栈通常在用户空间的最高地址处分配,通常有数兆字节的大小。

        2、堆:用来容纳应用程序动态分配的内存区域,当程序使用malloc或new分配内存时,得到的内存来自堆里。堆通常在于栈的下方(低地址方向),在某些时候,堆也可能没有固定统一的存储区域。堆一般比栈大很多,可以有几十至数百兆字节的容量。

        3、可执行文件影像:这里存储着可执行文件在内存里的影像,由装载器在装载时将可执行文件的内存读取或映射到这里

        4、保留区:保留区并不是一个单一的内存区域,而是对内存中受到保护而禁止访问的内存区域的总称呼,例如,大多数操作系统里,极小的地址通常都是不允许访问的,如NULL。通常C语言将无效指针赋值为0也是出于这个考虑,因为0地址上正常情况下不可能有有效的可访问数据。

        如图1是Linux下一个进程里典型的内存布局。

                         图1  Linux进程地址空间布局

        在图1中,有一个没有介绍的区域:"动态链接库映射区",这个区域用于映射装载的动态链接库。在Linux下,如果可执行文件依赖其他共享库,那么系统就会为它在从0x40000000开始的地址分配相应的空间,并将共享库载入到该空间。

栈和调用惯例

        栈是现代计算机程序里最为重要的概念之一,几乎每一个程序都使用了栈,没有栈就没有函数,没有局部变量,也就没有我们如今能够看见的所有的计算机语言。

        在经典的计算机科学中,栈被定义为一个特殊的容器,用户可以把数据压入栈中(入栈),也可以将已经压入栈中的数据弹出。

        在计算机系统中,栈则是一个具有以上属性的动态内存区域。程序可以将数据压入栈中,也可以将数据从栈顶弹出。压栈操作使得栈增大,而弹出操作使得栈减少。

        在经典的操作系统里,栈总是向下增长的。在i386下,栈顶由称为esp的寄存器进行定位。压栈的操作使得栈顶的地址减小,弹出的操作使得栈顶的地址增大。图2是一个程序栈实例。

                图2 程序栈实例

        这里的栈底的地址是0xbfffffff,而esp寄存器标明了栈顶,地址为0xbffffff4。在栈上压入数据会导致esp减小,弹出数据使得esp增大。相反,直接减少esp的值也等效于在栈上开辟空间,直接增大esp的空间等效于在栈上回收空间。

        栈在程序运行中具有举足轻重的地址。最重要的,栈保存了一个函数调用所需要的维护信息,这常常被称为堆栈帧或者活动记录。堆栈帧一般包括如下几方面的内容:

        1、函数的返回地址

        2、临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量

        3、保存的上下文:包括在函数调用前后需要保持不变的寄存器

        

        在i386中,一个函数的活动记录用ebp和esp这两个寄存器划定范围。esp寄存器始终指向栈的顶部,同时也就指向了当前函数的活动记录的顶部。而相对,ebp寄存器指向了函数活动记录的一个固定位置,ebp寄存器又被称为帧指针。一个很常见的活动记录所图3所示。

                         图3  活动记录

        在参数之后的数据(包括参数)即是当前函数的活动记录,ebp固定在图中所示的位置,不随这个函数的执行而变化,相反地,esp始终指向栈顶,因此随着函数的执行,esp会不断变化。固定不变的ebp可以用来定位函数活动记录的各个数据。在ebp之前首先是这个函数的返回地址,它的地址是ebp-4,再往前是压入栈中的参数,它们的地址分别是ebp-8,ebp-12等,视参数数量和大小而定。ebp所直接指向的数据是调用该函数前ebp的值,这样在函数返回的时候,ebp可以通过读取这个值恢复到调用前的值。之所以函数的活动记录会形成这样的结构,是因为函数本身是如此书写的:一个i386的函数总是这样调用:

        1、把所有或一部分参数压入栈中,如果有其他参数没有入栈,那么使用某些特定的寄存器传递

        2、把当前指令的下一条指令的地址压入栈中

        3、跳转到函数体执行

        其中第2步,第3步由指令call一起执行。跳转到函数体之后即开始执行函数,而i386函数体的"标准"开始是这样的:

        *        push ebp:把ebp压入栈中(称为old ebp)

        *        move ebp, esp:ebp=esp(这时ebp指向栈顶,而此时栈顶就是old ebp)

        *        sub esp,XXX:在栈上分配XXX字节的临时空

        *        push XXX:如有必要,保存名为XXX寄存器

        把ebp压入栈中,是为了在函数返回的时候便于恢复以前的ebp的值。而之所以可能要保存一些寄存器,在于编译器可能要求某些寄存器在调用前后保持不变,那么函数就可以在调用开始时将这些寄存器的值压入栈中,在结束后再取出。不难想象,在函数返回时,所进行的"标准'结尾和"标准"开始正好相反。

        *        pop XXX:如有必要,恢复保存国的寄存器

        *        move esp,ebp:恢复ESP同时回收局部变量空间

        *        pop ebp:从栈中恢复保存的ebp的值

        *        ret:从栈中取得返回地址,并跳转到该位置

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
有效的括号 一、题目 给定一个只包括 '(',')','{','}'...
【Ctfer训练计划】——(三... 作者名:Demo不是emo  主页面链接:主页传送门 创作初心ÿ...