程序的装载是程序运行前,从磁盘中载入到内存的过程,装载是一个很重要的过程。
页映射是虚拟存储机制的一部分。现代操作系统的装载机制也是用到了页映射的概念。页映射是将内存和磁盘中的数据和指令按照“页”为单位划分若干个页,以后所有的装载和操作的单位就是页。
以32位平台为例,每个进程拥有4G的虚拟空间,那么我们是否可以任意使用呢?不行,以linux系统为例,操作系统本身要占去一部分。也就是不管是哪个进程,都会用到操作系统。剩下3G,其实也不能全部可用,有一部分是预留给其它用途的。
这里抛出一个有趣的问题,32位CPU能否使用超过4G的空间呢,如果这个空间指的是虚拟地址空间,那么是不行的,因为32bit CPU只能使用32bit的指针,最大的寻址范围为4G. 但如果是问能否访问超过4G的物理内存空间呢。答案是可以的。有一种叫做窗口映射的办法。其意为,某一段虚拟地址空间可以根据实际需要和申请映射到额外的物理内存上去。这种方法叫做AWE(Address Windowing Extensions)
注意力继续回到装载的过程。我们不妨多想一下,对比单片机中,我们似乎就没有这个概念,那么,我们为什么需要装载呢。在单片机中,代码是烧录到norflash中的,单片机支持从norflash启动,也就是说我们那几十KB的代码,直接就被CPU取指令执行了。那linux光image就几百M,你norflash压根就不够用,所以代码都是在磁盘等存储介质中。另外,我们不行单片机,上电直接就跑了,我们的linux系统,是有根据用户指令动态运行某个程序的需求,这个时候一定会产生把可执行文件load到内存的过程。所以,还是和场景和需求有关,技术总是有其发展轨迹的。
linux启动后,会运行一个bash,在bash下面,当我们输入某个指令的时候,bash进程首先会调用fork创建一个新的进程,然后新的进程会调用execve()系统调用执行指定的ELF文件,原先的bash会返回并等待刚才的启动的新的进程结束,然后继续等待用户输入命令。execve()系统调用中,内核会调用do_execve 先查找被执行的文件,找到后会先读头128字节,确定文件类型,然后调用search_binary_handle() 搜索匹配合适的可执行文件装载的过程。以load_elf_binary为例,主要有以下步骤:
由于理论上elf文件中的每个段映射到虚拟内存空间中时都需要按照系统的page size为单位进行对齐【这样方便发生页错误时把对应的页分配到内存中】,这样会造成不小的空间浪费。其实,站在操作系统的角度上看,它关心的实际上是段的权限(可读,可写,可执行),于是一个简单的节省内存的方案就是:合并若干相同访问权限的段(section)到段(segment)中。
描述section的结构叫做段表,描述segment的结构叫做“程序头(Program Header)”, 它描述了ELF文件该如何被操作系统映射到进程的虚拟地址空间
可以看到一共有9个segment. 其中每个程序头的属性是被一个名为Elf32_Phdr的结构体所描述,这个程序头只在可执行文件中,不在目标文件中,因为目标文件不需要被装载。
我们来看一下各个属性的基本含义。
Type: segment的类型
Offset: Segment在文件中的偏移
VirtAddr: Segment第一个字节在进程虚拟空间的起始地址
PhysAddr: 物理装载地址,也就是我们前面讲的LMA
FileSize: 该段在文件中所占的长度
MemSize: 该段在进程虚拟地址空间中所占的长度
Flags: 权限
Align: 段的对齐属性