介绍ELF文件在linux下的装载过程和linux下共享库的管理问题
装载
程序==菜谱
CPU==人
硬件==其他厨具
进程==炒菜的过程
32位平台下,有4GB虚拟空间、linux默认分配1GB给OS,3GB给进程
对于程序来说,将程序需要的所有指令和数据载入内存运行,就是静态装入,但是当所需要的内存经常大于物理内存的数量,又不能无限添加内存。由于程序运行时局部性原理,就可以将程序最常用的部分驻留在内存中,这就是动态装入,覆盖装入和页映射是典型的动态装载方法
覆盖装入几乎已经被淘汰,大致思想就是,把挖掘内存潜力的任务交给程序员,并把程序分成多个模块,并编写一个管理这些模块合适驻留内存的辅助器称为覆盖管理器,不详述
页映射随着虚拟存储的发明而诞生。把内存和所有磁盘数据和指令按照页为单位划分成若干页,以后所有装载和操作的单位就是页。有时候程序的页数远超过物理页数,此时就需要算法去选择舍去那个页来装载需要的页。一种是先进先出算法,另一种是最少使用算法,而这个控制程序就是现代操作系统,更准确的说是操作系统的存储管理器
可执行文件的页可能被装入内存中的任意页,程序如果使用物理地址直接操作,每次装入时都需要重定位,因此才需要虚拟存储和现代的硬件MMU提供地址转换功能
接下来剖析:创建一个进程,然后装载相应的可执行文件并执行
- 创建一个独立的虚拟地址空间:由一组页映射函数将虚拟空间的各个页映射至相应的物理空间,那么创建一个虚拟空间实际上就是创建映射函数需要的数据结构??
- 读取可执行文件头,并建立虚拟空间与可执行文件的映射关系:第一步做的是虚拟空间和物理内存的映射关系,这步要做的是虚拟空间和可执行文件的映射关系。当操作系统捕捉到页错误时,需要知道程序需要的页在可执行文件的哪个位置,这就是虚拟空间和可执行文件之间的映射关系。可执行文件在装载时实际就是被映射的虚拟空间,但是考虑到偏移、字段对齐等因素,还是会有些许区别。linux把进程的虚拟空间中得到一个段叫做虚拟内存区域(VMA,如代码区、只读常量区、全局区、BSS段、堆区、栈区)
- 将CPU的PC设置为可执行文件的入口地址,启动执行:这一步在OS层面很复杂,设计内核堆栈和用户堆栈的切换、CPU运行权限的切换,不过从进程角度就简单认为执行了一条跳转指令,跳到可执行文件的入口地址
页错误
上面的步骤只是通过可执行文件头部的信息建立可执行文件和进程虚拟内存之间的映射关系,可执行文件的真正指令和数据并没有载入内存
当CPU执行指令时,发现指令对应的页面是一个空页面,就认为这是一个页错误(Page Fault)。CPU会将控制权交给操作系统,其由专门的页错误处理程序
OS会先查询上面装载中第二步构造的数据结构,找到缺失页所在的VMA,计算出相关页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中该虚拟页和物理页之间建立映射关系,之后把控制权交还给进程
进程虚拟内存空间分布
可执行文件拥有众多的段,但在映射的时候,是以系统的页长度为单位,这就会造成内存的浪费。为解决这个问题,可以将拥有相同权限的段合并在一起,然后映射到页中
在ELF中,数据分类的单位是Section,而从装载的角度,用Segment重新划分了ELF的每个Section,ELF中描述Segment的结构叫做Program Header,其描述了ELF文件如何被OS映射到进程的虚拟空间
堆和栈
在进程的虚拟地址空间中,除了保存ELF文件的Segment,还保存进程需要的栈和堆
进程刚启动的时候,需要知道一些进程运行环境,如运行参数和环境变量,OS在进程启动之前将这些信息提前保存到进程的虚拟空间的栈中(也就是上图的STACK VMA)