内存分布
32 位系统有 4GB 的地址空间,其中 0x08048000
~ 0xbfffffff
是用户空间(3GB),0xc0000000
~ 0xffffffff
是内核空间(1GB)。(0x00000000
~ 0x08048000
有 128M。为什么是0x00000000
而不是0x00000000
为起始?)
栈
栈帧
栈帧(Stack Frame,也称堆栈帧)是在堆栈中为未运行完的函数分配的内存区域,用于保存调用这些函数所需维护的信息。
在 x86 体系中,寄存器ebp
指向堆栈帧的底部,esp
指向堆栈帧的顶部。栈由高地址向低地址生长。
在 Linux 环境下,ELF 文件的入口其实是_start
而非main()
。
堆
- 堆是除了存放栈中东西之外的所有其他内存区域,由动态内存分配器负责维护。
- 堆由低地址向高地址生长(这是由于系统用链表来存储的空闲内存地址,而链表的遍历方向是由低地址向高地址。),是不连续的内存区域。分配器将堆视为一组大小不同的块(block)的集合来维护,每个块就是一个连续的虚拟内存片(chunk)。堆的大小受限于计算机系统中有效的虚拟内存。因此,堆获得的空间比较灵活,也比较大。
- 如果每次申请内存时都直接使用系统调用,会严重影响程序的性能。通常情况下,运行库先向操作系统“批发”一块较大的堆空间,然后 “零售” 给程序使用。当全部 “售完” 之后或者剩余空间不能满足程序的需求时,再根据情况向操作系统“进货”。当使用
malloc()
和free()
时就是在操作堆中的内存。对于堆来说,由于释放工作由程序员控制,容易产生内存泄露。
进程堆管理
Linux 提供了 2 种堆空间分配的方式,一个是系统调用brk()
,另一个是系统调用mmap()
。可以使用 man brk
、man mmap
查看。
brk()
的声明如下:
#include <unistd.h>
int brk(void *addr);
void *sbrk(intptr_t increment);
参数*addr
是进程数据段的结束地址,brk()
通过改变该地址来改变数据段的大小,当结束地址向高地址移动,进程内存空间增大,当结束地址向低地址移动,进程内存空间减小。brk()
调用成功时返回0
,失败时返回-1
。sbrk()
与brk()
类似,但是参数increment
表示增量,即增加或减少的空间大小,调用成功时返回增加后减小前数据段的结束地址,失败时返回-1
。
在上图中可以看到brk
指示堆结束地址,start_brk
指示堆开始地址。BSS segment
和heap
之间有一段Random brk offset
,这是由于ASLR
的作用,如果关闭了ASLR
,则Random brk offset
为0
,堆起始地址和数据段结束地址重合。
mmap()
的声明如下:
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags,
int fildes, off_t off);
mmap()
函数用于创建新的虚拟内存区域,并将对象映射到这些区域中,当它不将地址空间映射到某个文件时,我们称这块空间为匿名(Anonymous)空间,匿名空间可以用来作为堆空间。mmap()
函数要求内核创建一个从地址addr
开始的新虚拟内存区域,并将文件描述符fildes
指定的对象的一个连续的片(chunk)映射到这个新区域。连续的对象片大小为len
字节,从距文件开始处偏移量为off
字节的地方开始。prot
描述虚拟内存区域的访问权限位,flags
描述被映射对象类型的位组成。
munmap()
则用于删除虚拟内存区域,其声明如下:
#include <sys/mman.h>
int munmap(void *addr, size_t len);
通常情况下,我们不会直接使用brk()
和mmap()
来分配堆空间,C 标准库提供了一个叫做malloc
的分配器,程序通过调用malloc()
函数来从堆中分配块。