本文初学,若理解错误的地方,望拍砖留言
注: 有的资料是在网上收集的,可能有问题
程序运行后,操作系统分配内存,运行在一个虚拟内存空间里,在32位的系统里,这个内存空间拥有4GB的寻址能力。现代的应用程序可以直接使用32位的地址进行寻址,整个内存是一个统一的地址空间,用户可以使用一个32位的指针访问任意内存位置。
Linux 的虚拟内存管理有几个关键概念:
1、每个进程都有独立的虚拟地址空间,进程访问的虚拟地址并不是真正的物理地址;
2、虚拟地址可通过每个进程上的页表(在每个进程的内核虚拟地址空间)与物理地址进行映射,获得真正物理地址;
3、如果虚拟地址对应物理地址不在物理内存中,则产生缺页中断,真正分配物理地址,同时更新进程的页表;如果此时物理内存已耗尽,则根据内存替换算法淘汰部分页面至物理磁盘中
在进程的不同地址区间上有着不同的地位,Windows在默认情况下会将高地址的2GB空间分配给内核,而Linux默认将高地址的1GB空间分配给内核,具体的内存布局如下
Linux 使用虚拟地址空间,大大增加了进程的寻址空间,由低地址到高地址分别为:
1、只读段:该部分空间只能读,不可写;(包括:代码段、rodata 段(C常量字符串和#define定义的常量) )
2、数据段:保存全局变量、静态变量的空间;
3、堆 :就是平时所说的动态内存, malloc/new 大部分都来源于此。其中堆顶的位置可通过函数 brk 和 sbrk 进行动态调整。
4、文件映射区域 :如动态库、共享内存等映射物理空间的内存,一般是 mmap 函数所分配的虚拟地址空间。
5、栈:用于维护函数调用的上下文空间,一般为 8M ,可通过 ulimit –s 查看。
6、内核虚拟空间:用户代码不可见的内存区域,由内核管理(页表就存放在内核虚拟空间)。
linux0.01 最多有 64个进程。每个进程的内存分布如下:
栈是不断的加帧(函数调用)、减帧(函数返回)的,帧内的局部变量只能用相对于当前 esp(指向栈顶)或 ebp(指向当前帧)的相对地址来访问。
栈被放置在高地址也是有原因的: 调用函数(加帧)是减 esp 的,函数返回(减帧)是加 esp 的,调用在前,所以栈是向低地址扩展的,放在高地址再合适不过了
所以现代操作系统进程的内存分布就变成了下面这个样子:
当函数调用的时候发生了什么?
例如:
int main(void)
{
foo(1,2,3) ;
return 0 ;
}
当方法main需要调用foo时,它的标准行为:
1、在main方法的调用栈中,将 foo的参数从右向左 依次push到栈中。
2、把main方法当前指令的 下一条指令地址 (即return address)push到栈中。(隐藏在call指令中)
3、使用call指令调用目标函数体foo。
请注意,以上3步都处于main的调用栈,其中ebp保存其栈底,而esp保存其栈顶。
接下来,在foo函数中:
1、push ebp: 将ebp的当前值push到栈中,即保存ebp。
2、mov ebp,esp: 将esp的值赋给ebp,则意味着进入了foo方法的调用栈。
3、[可选]sub esp, XXX: 在栈上分配XXX字节的临时空间。(抬高栈顶)(编译器根据函数中的局部变量的总大小确定临时空间的大小)
4、[可选]push XXX: 保存(push)一些寄存器的值。
【注意:push寄存器的值,这一操作,可以在分配临时空间之前,也可在其之后,《程序员的自我修养》写的是在开辟临时变量之后】
(编译器中保存的有相应的变量名对应的临时空间中的位置)
而在foo方法调用完毕后,便执行前面阶段的逆操作:
1、保存返回值: 通常将函数的返回值保存在寄存器eax中。
2、[可选]恢复(pop)一些寄存器的值。
3、mov esp,ebp: 恢复esp同时回收局部变量空间。(恢复原栈顶)
4、pop ebp: 将栈顶的值赋给ebp,即恢复main调用栈的栈底。(恢复原栈底)
5、ret: 从栈顶获得之前保留的return address,并跳转到此位置继续执行
(1)esp:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
(2)ebp:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。(ebp在当前栈帧内位置固定,故函数中对大部分数据的访问都基于ebp进行)
(3)eip:指令寄存器(extended instruction pointer),其内存放着一个指针,该指针永远指向下一条等待执行的指令地址。 可以说如果控制了EIP寄存器的内容,就控制了进程——我们让eip指向哪里,CPU就会去执行哪里的指令。eip可被jmp、call和ret等指令隐含地改变(事实上它一直都在改变)(ret指令就是把当前栈顶保存的返回值地址 弹到eip中)
函数栈帧的大小并不固定,一般与其对应函数的局部变量多少有关。函数运行过程中,其栈帧大小也是在不停变化的。
* main函数在其栈中的局部变量区域中额外开辟一片空间,将其一部分作为传递返回值的临时对象temp。
* 将temp对象的地址作为隐藏参数传递给return_test函数。
* return_test函数将数据拷贝给temp对象,并 将temp对象的地址用eax传出。
* return_test返回后,main函数将eax指向的 temp对象的内容拷贝给n。
![\](http://www.2cto.com/uploadfile/Collfiles/20150522/20150522084441233.png)