1、虚拟地址空间
进程地址空间需要隔离,防止恶意的程序修改其它程序的内存数据,因此计算机中引入虚拟地址空间。
(1)每一个进程都会被分配4G的虚拟地址空间,之所以是4G,是因为在32位系统里,这个内存空间有着(2的32次方)的寻址能力,即从0x00000000~0xFFFFFFFF。
(2)大多数操作系统都会将4G的内存空间中的一部分挪给内核使用,应用程序无法直接访问这一段内存,这一部分地址空间被称为内核空间。Windows在默认情况下会将高地址的2G空间内核(也可以配置为1G),而Linux默认情况下会将高地址的1G空间分配给内核。用户使用的剩下的2G或3G的内存空间被称为用户空间。
简单来看看一个C进程的虚拟地址空间布局:
其中heap空间属于堆空间,在没有申请动态内存时候该空间为空洞,一旦申请,则该空间从起始位置向高地址增长。Stack空间属于函数调用栈空间,函数调用开辟栈帧,stack空间从起始位置向低地址增长。
.data段保存的是初始化且初始化不为0的static和全局变量。
.bss段保存的是未初始化或者初始化为0的static和全局变量。
(3)目前PC上页面大小为4K,程序运行到哪页就为哪页分配内存,并建立虚拟地址空间页和刚刚建立的物理内存页间的映射。
(4)一个可执行文件是一些编译好的数据和指令的集合,也被分为很多页。4G的地址空间并非真正创建,只是创建虚拟地址空间到物理地址空间映射的页表。
(5)可执行文件中所有的段会一一映射到虚拟地址空间中相应的页。
(6)当CPU访问某一个虚拟地址时,发现该地址并没有相关联的物理地址时,产生一个缺页错误,于是CPU将控制权交回OS,OS为该页面映射物理内存。
(7)当CPU要访问的数据在内存中时,可以直接访问。当CPU要访问的数据不再内存中,而位于页交换文件中,OS会在内存中找到一个闲置的页面,如果找不到闲置的页面,先释放一个已分配的页面。
2、编译链接原理
目标文件是源代码编译后但为进行链接的那些中间文件(Windows底下的.obj和Linux底下的.o),跟可执行文件的格式几乎是一样的,同采用一种格式存储。下面是.o文件简单的文件格式布局:
可以看到.bss段并没有划分出来,bss是英文 better save space的缩写,也就是更好的节省了文件的空间,因为.bss段存放的都是初始化为0或者没有初始化的数据(默认值也为0),也就是值都为0的数据,那么完全不用存储他们的值,也知道存在.bss段的数据值都是0。.bss虽然不占空间但是.bss段的详细信息都被保存在section headers中。
特例:存在* COM *这个块中的符号,和C语言中的强弱符号有关系。
强符号:全局的已初始化的符号
弱符号:全局的未初始化的符号
强弱符号的选取规则:
1. 两个强符号 编译报错
2. 一个强符号 一个弱符号 选择强符号
3. 两个弱符号 根据不同编译器处理方式不同
大家都知道一个项目可能有多个源文件,编译阶段都是每个文件单独编译的,可能在其他文件中存在强符号,所以没办法在编译期间确定具体的符号。
对于引用的外部符号符号表中是* UND *标志。也就是说这个符号没有确定具体定义的地方。
链接阶段链接器只关注符号表中的global符号,并不会解析local的符号。链接后,对每个符号给出了具体的虚拟地址。链接完了以后就可以确定弱符号的符号选择了。对于.o文件中引用的外部符号也确定了具体的定义位置,把这两个外部符号放到对应的段中。
符号解析: 在每个文件符号引用(引用外部符号)的地方找到符号的定义。
符号重定向:对.o文件中.text段指令中的无效地址给出具体的虚拟地址或者相对位移偏移量。
总体来说,程序源代码被编译以后主要分成两种段:指令和数据。代码段属于指令,二数据段和.bss段属于数据。这样做的优势:
1.程序被装载以后,数据和指令分别映射到两个虚存区域,由于对于进程,数据是可读可写的,指令是只读的,所以两个虚存区域的权限可以分别设置,可以防止程序的指令被修改;
2.现代计算机被设置成指令缓存和数据缓存分离,所以两者分开会提高CPU的缓存命中率;
3.当系统中运行着多个副本时,诸如指令这样只读的区域,每个副本进程是共享的,数据区域是不一样的,可以节省大量的内存。