第五章 可执行文件的装载与进程
可执行文件的装载
进程的建立
- 创建虚拟地址空间。事实上只需要分配一个页目录表。其中的内容等到发生缺页中断再填写。
- 读取可执行文件头,建立虚拟地址空间与可执行文件的映射关系。以便在发生缺页中断时定位到该缺页在可执行文件中的位置。
- 将可执行文件入口地址写入CPU指令寄存器,CPU开始运行。
进程的虚拟空间分布
Segment & Section
在操作系统装载可执行文件时,对每个段只区分其权限(可读可执行、可读可写、只读)。为了提高内存的利用效率,可将相同权限的段合并,这时用Segment表示相同权限的段的合并。
Segment的信息保存在ELF可执行文件的程序头表中。
堆与栈
常见的进程的虚拟地址空间如下。
堆与栈共用同一内存区域,统称堆栈。堆从区域底部向上增长,栈从区域顶部向下增长,可用如下程序验证。
1 |
|
多段共享
由于页是(虚拟地址与物理地址)映射的最小单位。因此每一段的起始地址都要是4K的整数倍,这样会产生大量的内部碎片,从而极大降低了内存的利用效率。为此,Unix让各段相接部分共享同一物理页面,具体做法如下例。
ELF文件的起始虚拟地址为0x08048000,第一个虚拟内存区域VMA0的长度为0x709e5,因此第一个虚拟内存区域VMA0的结束地址为0x080b89e5。VMA0并未将以0x080b8000为起始地址的虚拟页对应的物理页面用完,于是VMA0要与下一段共享以0x080b8000为起始地址的虚拟页对应的物理页面。又因为段的起始地址必须是4字节的倍数,因此下一段从下一虚拟页的0x9e8偏移处开始,即0x080b99e8。
进程栈的初始化
进程在刚开始启动时,需要知道一些进程运行的环境参数,常见的做法是操作系统在进程启动前将这些信息保存到进程的虚拟地址空间的堆栈上。在bash上以prog 123
命令运行的进程的栈的初始化状态如下图所示。
栈顶指针esp指向栈顶(堆栈区域的最低地址),首4字节存放命令行参数的数目。接下来的两个4字节为指向两个参数的指针,然后一个值为0的4字节,作为分隔标记。接下来的两个4字节存放环境变量的指针。然后一个值为0的4字节,作为分隔标记。然后是命令行参数与环境变量。
ELF可执行文件的装载
宏观层面来说,Linux调用exec来装载可执行文件到内存。而exec调用sys_execve,sys_execve进行一些参数的检查复制后,调用do_execve,do_execve首先查找被执行的文件,如果找到,则读取其前128字节(这是因为Linux支持的可执行文件不止一种,因此需要通过文件最开始的128字节来判断文件格式),对于ELF可执行文件,do_execve确定其文件格式后,调用load_elf_binary来装载ELF可执行文件。load_elf_binary处理流程如下。
- 检查ELF可执行文件格式的有效性。
- 寻找动态链接的
.interp
段,设置动态链接器路径。 - 根据程序头表建立虚拟地址空间与可执行文件的映射。
- 初始化进程的运行环境。
- 修改系统调用的返回地址为ELF可执行文件的入口地址(对于静态链接的ELF文件,程序入口地址为ELF文件头中e_entry所指地址;对于动态链接的ELF文件,程序入口地址为动态链接器地址)。