OS - Loading
Created by : Mr Dk.
2020 / 06 / 28 17:13
Nanjing, Jiangsu, China
本文内容来自于 程序员的自我修养 - 链接、装载与库,俞甲子 石凡 潘爱民著。
可执行文件只有装载到内存中以后才能被 CPU 执行。
进程虚拟地址空间
每个程序被运行起来后,将拥有自己独立的 虚拟地址空间 (Virtual Address Space)。硬件的寻址能力决定了地址空间的最大理论上限。由于程序运行时处于 OS 的监督下,进程只能使用 OS 分配给进程的地址空间,否则就会产生段错误。比如,32-bit 平台下的 Linux 会将 1GB 保留给 OS 使用,进程只能使用剩下的 3GB。因此进程在原则上最多能够使用 3GB 的虚拟地址空间 (虽然实际上 3GB 中还是会有一部分留作其它用途)。
装载的方式
静态装入:直接将程序运行需要的指令和数据装入内存。
但很多情况下,程序所需的内存数量大于实际内存数量。根据程序局部性原理,可以将最常用的程序驻留在内存中,将一些不太常用的数据存放在磁盘上,即动态装入。
覆盖装入
由程序员写一些辅助代码来管理哪些程序模块应当驻留在内存中。目前已经基本被淘汰
页映射
将所有的数据和指令按照 页 为单位分成若干页,之后装载和操作的单位就是页。由装载管理器来决定哪些页应当驻留内存,哪些页应当被换出。这个装载管理器就是现代操作系统中的内存管理器。
从 OS 角度看可执行文件装载
进程的建立
很多时候,程序被执行同时伴随着一个新的进程被创建:
- 创建虚拟地址空间,建立映射函数所需要的数据结构 - 在 Linux 上,就是分配了一个页目录
- 读取可执行文件头,建立进程虚拟地址空间与可执行文件的映射关系 (使 OS 知道缺页所需的内容在可执行文件中的哪一个位置)
- 将系统调用的返回地址设置为可执行文件的入口地址。
进程虚拟地址空间与可执行文件之间的映射关系是保存在 OS 中的数据结构。Linux 中将进程虚拟空间中的一个段称为 虚拟内存区域 (VMA),相应的数据结构记录以下内容:
- VMA 与 ELF 中 (代码/数据) 段的映射关系 (在 ELF 文件中的偏移)
- VMA 在进程虚拟地址空间中的地址范围
- 段属性 (只读)
- ...
当发生缺页时,OS 查询上述数据结构找到 VMA,计算出该页面在可执行文件中的偏移量,然后在物理内存中分配页面,并建立虚拟页与物理页之间的映射。最终把 CPU 控制权还给进程,进程重新从缺页位置开始执行。
进程虚存空间分布
ELF 文件链接视角与执行视角
在一个正常运行的进程中,可执行文件的多个段都会被映射到进程虚拟地址空间中。由于映射时,段以页为单位进行映射,那么 ELF 文件中的多个段 (十多个段) 分别映射到虚拟地址空间时,就会产生很多内存碎片造成浪费。比如段的最后一小块剩余部分必须占用一个页。从 OS 的角度来说,其不关心每个段中的具体内容,只关心与装载相关的问题,尤其是段的权限:
- 可读可执行 (代码段)
- 可读可写 (数据段)
- 只读 (只读数据段)
ELF 中引入了 segment 的概念,一个 segment 包含一个或多个 section,将相同权限的 section 作为一个 segment 映射到虚拟地址空间,从而节省了内存空间。在映射后,一个 segment 对应于一个 VMA,能够明显地节省内存空间。Segment 实际上是从装载的角度重新划分了 section,链接器会尽量把具有相同属性的 section 被归类到一个 segment,从而映射到同一个 VMA。
ELF 文件中,通过 程序头表 (Program Header Table) 来描述 segment 信息,即描述了 ELF 文件该如何被 OS 映射到进程的虚拟空间。在所有 segment 中,只有类型为 LOAD
的 segment 才是需要被映射的。ELF 目标文件没有程序头表,因为它不需要被装载。可执行文件带有程序头表:
typedef struct
{
Elf64_Word p_type; /* Segment type */
Elf64_Word p_flags; /* Segment flags */
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
其中,p_type
指示 segment 的类型,对于装载来说,只关心 LOAD
类型;p_offset
指示 segment 在 ELF 文件中的偏移;p_vaddr
和 p_paddr
一般情况下相同,前者代表 segment 的第一个字节在进程虚拟地址空间的起始位置;p_filesz
指示 segment 在 ELF 文件中的长度;p_memsz
指示 segment 在进程虚拟地址空间中的长度;p_flags
和 p_align
分别指示权限和对齐属性。
总体来说,从 section 的角度来看 ELF 文件就是 链接视角,从 segment 的角度来看 ELF 文件就是 执行视角。
VMA 除了被用于映射可执行文件中的各个 segment 以外,还可以管理进程的栈、堆。OS 通过给进程划分一个个 VMA 来管理进程的虚拟空间,基本原则是将相同权限属性的、相同映像文件的部分映射成一个 VMA。进程一般的 VMA 包含:
- 代码 VMA (只读可执行,有映像文件 (ELF))
- 数据 VMA (可读写可执行,有映像文件)
- 堆 VMA (可读写可执行,无映像文件,可向上扩展)
- 栈 VMA (可读写不可执行,无映像文件,可向下扩展)
Linux 内核装载 ELF 的过程
fork()
+ execve()
。取可执行文件的头 128 字节判断 magic value,找到合适的可执行文件装载函数。在 load_elf_binary()
中:
- 检查 ELF 文件格式的有效性 - 比如 magic value、段数量等
- 寻找动态链接的
.interp
section,设置动态链接器路径 - 根据 program header table,对 ELF 文件进行映射 (代码段、数据段、只读数据段)
- 初始化进程环境
- 将系统调用的返回地址修改为 ELF 的入口地址
- 静态链接的 ELF,入口为 ELF header 中的入口地址
- 动态链接的 ELF,入口为动态链接器的地址