第10章 内存

第4部分库与运行库

程序的环境由内存、运行库和系统调用(API)等构成。

系统调用和API是程序与内核之间交互的中介。

第10章内存

10.1程序的内存布局

C/C++语言中指针的大小是4个字节是有原因的,因为指针实际上是个地址值,地址值决定于内存空间大小,而CPU支持的最大内存容量是由地址总线的宽度决定的,因为32位地址总线的宽度是32位,即4字节,所以指针的大小也是4字节。

内存中有一部分称为内核空间,普通的应用程序是无法访问的。

除此之外,内存还有栈空间、堆空间、可执行映像、保留区。

栈:用于维护函数调用的上下文。

堆:用于动态分配空间,它通常位于栈的下方。

可执行映像:用于装载可执行文件在内存中的映像。

保留区:内存中受保护而禁止访问的区域,如NULL。

内存中还有一段动态链接库映射区,用于装载动态链接库。

10.2栈与调用惯例

10.2.1什么是栈

栈总是向低地址方向增长的。

栈最重要的功能是保存一个函数调用所需要的信息,这信息通常被叫做堆栈帧或活动记录。

堆栈帧一般包括:

1、函数的返回地址和参数。

2、临时变量,包括非晶态局部变量和编译期自动生成的临时变量。

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

ESP寄存器总是指向栈顶,EBP(栈指针)寄存器总是指向函数活动记录的固定位置。

P313描述了i386函数调用的过程。

为什么未初始化的数组元素被打印出来以后是汉字的“烫”?

这是因为它们在DEBUG模式下被初始化为0xCC,而两个连续的0xCC就是0xCCCC,即,汉字的“烫”。

还有的时候是使用0xCD初始化未初始化的数组,这时候显示的汉字是“屯”。

“钩子”函数可以通过替换掉Windows函数的汇编序列前面几条“无用”的指令实现。

10.2.2调用惯例

是指函数的调用方和被调用方都遵守同样的函数调用方式。

它一般规定如下几个方面的内容:

1、函数参数的传递方式和顺序。大部分函数参数是通过栈传递,有些是通过寄存器。

2、栈的维护方式。就是调用完了就把被调方全部弹出栈。

3、名字修饰策略。调用惯例会对函数本身的名字进行修饰。

_cdecl就是一种调用惯例。

栈在函数调用过程中的变化是这样的:

1、首先将参数压入栈中。

2、继续压入被调函数在主调函数中的下一条指令的地址,改地址在内存中就是返回地址。比如:

现在假设f是被调函数,那么return

0的地址就是要被压入栈中的地址。

3、跳转到函数体执行。

现在就P322的图说明几点问题。

old ebp是啥意思?首先要明确图中的意思是个值,即ebp寄存器中的值,这个值有着特殊的意义。这个值是个地址,而这个地址是当前函数的调用函数的ebp的值在内存中的位置。所以这就是个无限循环。

EBP(Extended Base Pointer):扩展基址指针寄存器。

由此可见EBP其实起到了一个基址的作用,它通常与ESP(Extended

Stack Pointer)搭配使用,前者是基址,后者是偏移量,这样的话就可以指向某函数的寄存器和局部变量了。

保存这个旧的EBP值的作用在于它能够恢复调用函数的EBP值,调用函数的EBP值是个绝对地址,如果你不恢复它,那么调用函数就无法再利用ESP的值来指向它自己的寄存器和全局变量了。

C++中的this指针存放在ECX(Extended

Counter miX,扩展计数寄存器)中。

10.2.3函数返回值传递

EAX(Extended Accumulator miX,扩展累加寄存器)负责传递返回值,它能返回4字节的返回值。

对于小于等于8字节并且大于4字节的返回值,可以搭配EDX(Extended

Data miX,扩展数据寄存器)进行返回。

如果返回值太大,那就必须开辟一段栈空间来临时存放返回值。首先产生返回值的函数会把返回值写到这段空间中,然后这段空间中的值再写到真正的用于装载这个返回值的变量中,这个变量是我们常说的返回值,至于该段中所说的返回值就是指需要被返回的数据。也就是说在这个过程中会产生两次复制。

10.3堆与内存管理

堆的处理过程比栈要复杂,因为程序在执行过程中随时可能申请空间和释放空间,而且申请和释放的空间大小也不确定。

10.3.1什么是堆

堆是一块巨大的内存空间,占据了虚拟内存的绝大部分。

堆可以将数据传递到函数外面,而且能动态地产生对象。

直接操作堆空间的是程序的运行库,而不是系统调用,因为这样做比较高效。

10.3.2

Linux进程堆管理

有两种方式:

1、通过调整数据段的结束地址来调整堆空间的大小。

2、直接申请一段内存空间用作堆空间。

申请的空间的起始地址和大小都是页大小的整数倍。

在Linux系统下,堆可以存在于从BSS段到共享库装载的位置和从共享库结束位置到栈这两部分空间,现在的话能有2.9G大小。也就是说现在你用malloc申请堆空间至多能申请2.9G左右。

10.3.3

Windows进程堆管理

Windows下的堆和栈空间是非常零散的,因为每个线程在建立的时候都需要自己的栈空间,堆空间就是在剩下的零碎的空间中分配的。

它的对空间大小和起始地址也必须是页的整数倍。

无论是Linux还是Windows都有自己的堆分配算法。

每个进程在创建的时候,系统都会给它分配一个堆,并在进程结束的时候销毁堆。

堆空间并不是连续的。

堆空间不一定都是向上增长的。

10.3.4堆分配算法

它就是负责管理堆空间的申请、分配和释放的算法。

1、空闲链表法。

2、位图。把整个堆划分成大小相同的块,第一个块叫头,其余的称为主体,用一个数组来记录块的使用情况,每个块有三种状态,即,头、主体和空闲。

3、对象池。如果每次对象申请的堆空间大小相同,就可以把堆划分成对象大小的若干块,每次只需要取一块分配出去即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容