内存
内存
程序的内存布局
-
内核空间
-
用户空间
-
栈:用于维护函数调用的上下文
-
堆:用来容纳应用程序动态分配的内存区域
-
可执行文件映像
-
保留区:对内存中受到保护而禁止访问的内存区域的总称,比如nullptr。
-
栈与调用惯例
-
栈保存了一个函数调用所需要的维护信息,叫堆栈帧(Stack Frame)或活动记录(Activate Record),包括以下内容:
-
函数的返回地址和参数
-
临时变量:包含函数的非静态局部变量以及编译器自动生成的其他的临时变量
-
保存的上下文:包括在函数调用前后需要保持不变的寄存器
-
-
esp寄存器始终指向栈的顶部,即当前函数的活动记录的顶部,ebp寄存器指向函数活动记录的一个固定位置,又称为帧指针
-
未初始化的指针的值一般是0xCCCCCCCC或0xCDCDCDCD,对应的文本是烫烫或屯屯。
-
在一些场合下,编译器生成函数的进入和退出指令序列时并不按照标准的方式进行
-
函数被声明为static
-
函数在本编译单元仅被直接调用,没有显示或隐式取地址(即没有任何函数指针指向过这个函数)
-
-
调用惯例:函数的调用方和被调用方对于函数如何调用要有一个明确的规定
-
函数参数的传递顺序和方式
函数参数的传递方式:最常见的是通过栈传递,有些调用惯例允许使用寄存器 -
栈的维护方式
栈中参数的弹出工作可以由函数的调用方来完成,也可以由函数本身完成 -
名字修饰(Name-mangling)的策略
为了链接的时候对调用惯例进行区分,要对函数本身的名字进行修饰。不同的调用管理有不同的名字修饰策略。C语言存在多个调用惯例,默认的是cdecl。
-
-
函数返回值传递
-
传递函数返回值的临时对象会作为一个隐藏的形参传递给函数
-
如果返回值类型的尺寸太大,返回值对象会被拷贝两次。(如果没有C++返回值优化)
-
堆与内存管理
-
什么是堆
-
堆是一块巨大的内存空间,常常占据整个虚拟空间的绝大部分。在这片空间里,程序可以请求一块连续内存,并自由地使用,这块内存在程序主动放弃之前都会一直保持有效。
-
每次程序申请或释放堆空间都需要进行系统调用,系统调用的性能开销很大。比较好的做法是程序向操作系统申请一块适当大小的堆空间,然后由程序自己管理这块空间。
-
管理着堆空间分配的往往是程序的运行库。它使用堆的分配算法来管理堆空间。
-
-
Linux进程堆管理:Linux下的进程堆管理有点复杂,因为它提供两种堆空间分配的方式,即两个系统调用,brk()和mmap()
-
brk()的作用实际上就是设置进程数据段的结束地址,即它可以扩大或缩小数据段。
-
mmap()的作用就是向操作系统申请一段虚拟地址空间,当它不将地址空间映射到某个文件时,我们又称为这块空间为匿名空间,就可以拿来作为堆空间。系统虚拟空间申请函数申请的空间的起始地址和大小都必须是系统页的整数倍,所以对于小额的请求会造成空间大量浪费。
-
2.6版的Linux的malloc的最大空间申请数在2.9GB左右
-
还有其他因素会影响malloc的最大空间大小,比如系统的资源限制、物理内存和交换空间的总和等。
-
-
Windows进程堆管理
-
Windows的进程将地址空间分配给了各种exe、dll文件、堆、栈,已经是支离破碎了。
-
Windows系统提供了一个API叫做VirtualAlloc(),用来向系统申请空间,与mmap()非常相似。实际上申请的空间不一定只用于堆,它仅仅 向系统预留了一块虚拟地址,应用程序可以按照需要随意使用。
-
在Windows下可以自己实现一个堆分配算法,不过没必要重复造轮子,直接使用堆管理器。它提供了一套与堆相关的API:HeapCreate、HeapAlloc、HeapFree、HeapDestroy。
-
当用户通过HeapAlloc申请的空间超过堆的大小,堆管理器就会通过VirtualAlloc扩展堆的大小。
-
进程中可能存在多个堆,但是一个进程中一次性能够分配的最大的堆空间取决于最大的那个堆。
-
-
堆分配算法
-
空闲链表:把堆中各个空间的块按照链表的方式连接起来,当用户请求一块空间时,可以遍历整个链表,直到找到合适大小的块并非将它拆分;当用户释放空间时,将它合并到空闲链表中。
-
位图:将整个堆分为大量的块,每个块大小相同。当用户请求内存的时候,总是分配整数个块的空间给用户,第一个块我们称为已分配区域的头(Head),其余的称为已分配区域的主体(Body)。我们可以使用一个整数数组来记录块的使用情况,由于每个块只有头/主体/空闲三种状态,因此仅仅需要两位即可表示一个块。
-
对象池:实际上在一些场合,被分配对象的大小是较为固定的几个值。
-
小结
- 有种替换的机制可以用来实现一种叫做钩子(Hook)的技术,允许用户在某些时刻截获特定函数调用。