5. 可执行文件的装载与进程【《链接、装载与库》学习笔记】
进程虚拟地址空间
每个程序被运行起来以后拥有自己独立的虚拟地址空间,大小由计算机硬件平台决定,具体说是由CPU位数决定。
一般来说,C语言指针大小的位数与虚拟空间的位数相同。
PAE(Physical Address Extension)可以访问更多的物理内存。
操作系统而已提供一个窗口映射的方法,把额外的内存映射到进程地址空间。
装载
动态装入:把程序最常用的部分留在内存中,将一些不太常用的数据存放在磁盘中。覆盖装入(Overlay)和页映射(Paging)是两种典型动态装载方法。
覆盖装载
程序员通过手工方式将模块按照他们的调用依赖关系组成树状结构。
页映射
使用一种”装载管理器“(其实就是操作系统的存储管理器)使用FIFO的方式将程序的不同页装入内存。
因为这样动态装载的机制,每次页装入时都需要进行重定位。
进程的建立
- 创建一个独立的虚拟地址空间:分配一个页目录
- 读取可执行文件头,并建立虚拟空间与可执行文件的映射关系:装载段进入虚拟内存区域(VMA, Virtual Memory Area)
由于可执行文件在装载时实际上是被映射的虚拟空间,所以可执行文件很多时候被称为映像文件(Image)
- 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。
页错误
但通过CPU发现某页面是空页面,被认为是一个页错误,开始计算VMA并进行装载。随着进程的执行,页作物也会不断产生,操作系统也会为进程分配相应物理页面满足进程执行需求。
进程虚存空间分布
操作系统实际上并不关心而执行文件各个段所包含的实际内容,只关心与装载相关的问题,如权限(可读、可写、可执行)。
为了内存浪费,对于相同权限的段,可以合并到一起当作一个段来映射。ELF的Segment包含一个或多个属性类似的Section。
可以使用readelf查看ELF的Segment。描述Segment的结构叫程序头。
1 | |
总的来说,Segment和Section是从不同的角度划分一个elf文件,被称为不同的视图(View),Section对应链接视图,Segment对应执行视图。
ELF的程序头表专门用来保存Segment的信息(只有可执行文件和共享库文件有,目标文件没有)。程序头表也是结构体数组。
堆和栈
栈和堆也是以VMA的形式存在的。而已通过查看 /proc 来查看进程的虚拟空间分布。
一个进程基本上可以分为以下几种VMA:
- 代码VMA:只读,可执行,有image。
- 数据VMA:可读写,可执行,有image。
- 堆VMA:可读写,可执行,无image,匿名,可向上扩展。
- 栈VMA:可读写,不可知性,无image,匿名,可向下扩展。
堆的最大申请数量收到操作系统版本,程序本身大小,用到的库数量,大小,程序栈数量,大小等不同。
进程在启动以后,程序的库部分会把堆栈里的初始化信息中的参数信息传递给main()函数,也就是argc和argv,分别对应命令行参数数量和命令行参数字符串指针数组。
Linux内核装载ELF过程
首先在用户层面,bash进程会调用fork()系统调用创建一个新的进程,然后新的进程调用execve()执行指定ELF文件,原先bash进程继续返回等待刚才新进程结束,然后继续等待用户输入命令。
execve() -> sys_execve() -> do_execve()。do_execve()先判断文件格式(魔数),然后搜索合适的可执行文件装载处理过程。
当sys_execve()系统调用从内核态返回用户态时,EIP寄存器直接跳转到了elf程序入口地址,装载完成。
Windows PE的装载
PE文件中,所有段的起始位置都是也的倍数,如果不是页的整数倍,那么在映射时向上补齐到页的整数倍。
装载过程:
- 先读取第一页,包含DOS头,PE文件头和段表
- 检查目标地址是否可用
- 使用段表中提供的信息一一映射
- 装载所有需要的DLL
- 解析符号
- 根据参数初始化堆和栈
- 建立主线程并启动进程