一、进程的内存布局
一般来讲进程有如下默认区域:
栈:用于维护函数调用上下文,通常从用户空间高地址开始向低地址增长;
堆:进程动态分配内存区域,一直存在直到被手动回收或者进程结束;
可执行文件和共享对象镜像:
保留区:内存中受保护而禁止访问区域,访问该处会出现段错误,通常靠近0x0处。比如int *p = NULL就是无效指针。
其中栈和堆是我们主要需要分析的部分。
二、栈和惯例
栈的布局如下图。
函数调用过程:
调用方需要做的事情:
(1)将参数压入栈 (2)将当前指令下一条指令地址压入栈 (3)跳转到被调函数体; 其中(2)(3)两步通过call函数一起执行。
被调函数首先需要做的事情:
- push %ebp 将old ebp压入栈中
- mov %esp %ebp 将新的ebp指向栈顶
- sub %esp XXX 分配新的临时栈空间
- push XXX 将一些需要保存寄存器压栈
- 函数参数的传递顺序:即多个参数的压栈顺序
- 栈的维护方式,比如函数调用结束,由谁来弹出栈中函数参数
- 名字修饰策略:调用惯例需要对函数本身名字进行修饰
函数返回值参数传递:
函数可以将返回值存储在寄存器中。对于返回值5~8字节情况,一般通过eax和edx联合返回方式进行。
如果是返回对象很长的情况。。。
(1)调用方使用临时栈上空间作为中转,并将此区域首指针作为隐含参数传递给被调方;
(2)被调方根据该隐含参数,将返回对象拷贝给中转区域;
(3)调用方将中转区域拷贝给最终对象。
三、堆和内存管理
Linux进程堆管理
Linux提供两种堆空间分配方式:
- int brk(void *end_data_segment) 实际作用是扩大或缩小数据段,若我们将数据段结束地址向高端地址移动,则扩大的空间将作为堆空间,这是最常用的做法之一。
- void mmap(*start, length, prot,flags,...) 指定需要申请的地址大小和长度,都是页的整数倍。
位图:将整个堆划分成大量的块,每个块大小相同。用位图记录所有块的使用情况。
对象池