Skip to content

Latest commit

 

History

History
105 lines (81 loc) · 10.9 KB

LinkLoadAndLib.md

File metadata and controls

105 lines (81 loc) · 10.9 KB

链接、装载与库

系统软件体系结构:

源代码到可执行文件经历过程:
预处理(Prepressing)
编译(Conpilation)
汇编(Assembly)
链接(Linking)

编译过程
编译过程一般可分为6步:
扫描,语法分析,语义分析,源代码优化,代码生成和目标代码优化

最基本的静态链接过程:

静态链接的具体过程:
对于链接器,整个链接过程中是将几个输入的目标文件进行加工合并后输出一个可执行文件。加工过程主要包括 空间与地址分配&符号解析与重定位 (连接过程中计算得到的地址为虚拟地址)
对于多个目标文件的合并过程,主要采用相似段合并策略:

空间与地址分配
链接器扫描所有输入的目标文件,得到各个段的长度,属性及位置,将输入目标文件中的符号表所有的符号定义和符号引用收集起来,统一放到一个全局符号表。链接器将所有输入的目标文件合并,计算输出文件中各个段合并后的长度与位置,建立映射关系。
符号解析与重定位
链接器利用上一步收集到的信息,读取输入文件中段的数据、重定位信息,进行符号解析和重定位、调整代码中的地址。

为什么需要进行符号解析与重定位?
在编写源代码时,各个模块之间需要进行相互引用,各个模块在编译后得到对应的目标文件。而在编译时,全局变量和函数(即符号)的位置分为两种情况:
1.符号定义在模块内
定义在模块内的符号在编译时会存储在目标文件的符号表(段)中,记录相对于段首的偏移,在进行链接时,多个目标文件的相似段需要进行合并,因此需要根据合并后的情况调整定义在各个模块内的符号地址。
2.符号定义在模块外
这种情况下,符号定义在其他模块中,该模块引用这些符号。这种情况下在进行编译时,编译器并不知道这些符号的准确地址,因此针对引用的符号,会有一个对应的重定位段,专门记录这些引用的符号,编译器会给这些符号一个临时地址,真正的地址交给链接器来处理,链接器会对每个需要重定位的符号进行地址修正。

程序如何使用操作系统提供的API?
一般情况下,一种语言的开发环境会附带有相应的语言库(Language Library),这些库就是对操作系统的API包装。例如在Windows平台,最常使用的C语言库是由集成开发环境(IDE)附带运行库,这些库一般由编译器厂商提供。运行库向应用程序或开发环境提供操作系统应用程序接口,以软件中断的形式调用系统调用与操作系统通信。
运行库或者静态库可以看成是一组目标文件的集合,即很多目标文件经过压缩打包后形成的文件。C语言的运行库中包含很多与系统功能相关的代码,如输入输出,文件操作,时间日期,内存管理等。
程序源代码在编译完成后,链接器会将目标文件与静态库中引用的目标文件进行链接,形成最终的可执行文件。

静态运行库中一个目标文件只包含一个函数。
这是由于在链接过程中,是以目标文件为单位,链接器将多个目标文件合并在一起形成可执行文件,若将多个函数写在同一个目标文件中,在链接时,许多没有使用到的函数也会被包含进来,并输出到最终的可执行文件,这样会造成空间的浪费。

Windows的二进制文件PE/COFF
PE文件格式是Win平台下的可执行文件格式,PE与ELF一样,由COFF(Common Object File Format)发展而来。Win下的目标文件,可执行文件采用PE/COFF文件格式。PE/COFF采用基于段的格式,段可以包含代码,数据,或者其他信息,与ELF文件格式类似。
COFF文件结构:

装载
可执行文件只有被装载到内存以后才能被CPU执行。
将指令和数据在程序运行前全部装入内存中,是最简单的静态装入方式,由于大多数情况下程序需要的内存大于物理内存,因此这种方法并不能有效利用内存。
由于程序运行时是有局部性原理的,因此可以将程序最常用的部分驻留在内存中,将一些不太常用的数据存放在磁盘中,即动态载入方式。

覆盖装入页映射是两种典型的动态装载方法。

覆盖装入在虚拟存储技术之前应用广泛,目前几乎已淘汰。需要程序员手工将程序分割成若干块,然后编写辅助代码管理模块何时驻留在内存何时被替换掉,辅助代码即为覆盖管理器,通常很小,常驻内存中。

页映射是虚拟存储机制一部分。
将内存和磁盘中的数据和指令按照“页”为单位划分成若干页,所有装载和操作的单位是页。在程序运行时,操作系统的存储管理器根据程序运行的需求将对应的页加载进内存中。当物理内存已满,程序需要加载新的页时,由存储管理器进行抉择舍弃当前不需要使用的页,来加载新的页。

可执行文件的装载
页映射通过动态方式将可执行文件的页载入内存中。可执行文件的装载伴随一个进程的创建。
从操作系统的角度来看,一个进程最关键的特征在于拥有独立的虚拟地址空间,以区别其它进程。一个进程被创建,然后相应的可执行文件被装载。上述过程最开始经历了三件事情:

  • 创建一个独立的虚拟地址空间
    一个虚拟空间由一组页映射函数将虚拟空间的各个页映射至相应的物理空间,创建一个虚拟空间并不是创建空间而是创建映射函数所需要的数据结构,记录个个页对应映射关系,将该数据结构保存在内存中。
  • 读取可执行文件头,建立虚拟空间与可执行文件的映射关系
    第一步中的页映射函数是虚拟空间与物理内存的映射关系,这一步是建立可执行文件与虚拟空间的映射关系,是为了当发生物理内存中缺页时,操作系统需要知道程序所缺的页在可执行文件的哪个位置,以便找到该页进行加载。可执行文件会通过页对齐的方式映射到虚拟空间中的某一段,这种映射关系同样是以某种数据结构的方式保存在内存中,虚拟空间中的一个段称为虚拟内存区域(VMA Linux下)或 虚拟段(VS Windows下)
  • 将CPU的指令寄存器设置成可执行文件的入口地址,启动运行
    操作系统通过设置CPU的指令寄存器将控制权交给进程,由此进程开始执行。可以简单认为操作系统执行一条跳转指令,直接跳到可执行文件的入口地址。

页错误
上述过程执行完成后,可执行文件的真正指令和数据并没有装入内存中,内存中存储的只是映射关系的数据结构。假如程序的入口地址为0x080480000,即VMA中代码段的起始地址,当CPU开始执行地址中的指令时,发现内存中的页0x08048000~0x08049000是空页面,于是CPU认为这是一个页错误 CPU将控制权交给操作系统,操作系统利用虚拟空间与可执行文件的映射关系的数据结构,查找对应的页面,然后分配一个物理页面,将进程中该虚拟页与分配的物理页建立映射关系,将控制权再交给进程。在进程运行的过程中,会重复出现“页错误”的情况。

可执行文件的装载过程

ELF文件中,各个段的权限基本上是以下3种:

  • 以代码段为代表的的可读且可执行段
  • 以数据段和BSS段为代表的的权限为可读可写的段
  • 以只读数据段为代表的权限为只读的段

一个ELF文件中往往有十几个段,在进行映射时是按照页为单位,不足一个页的内容也会单独占据一个页,这样会造成内存空间的浪费。
当操作系统装载可执行文件时,并不关心可执行文件各个段所包含的实际内容,而是段的权限。
因此,对于权限相同的段,操作系统将它们合并到一起作为一个段(Segment)进行映射

ELF可执行文件引入“Segment”,一个“Segment”包含一个或多个权限相同的”Section“
图中如果将.text和.init段合并在一起看做一个“Segment”,那么装载时可以看做一个整体一起映射,也就是映射以后在进程的虚拟空间只有一个对应的VMA,而不是两个,这样可以有效减少内存碎片,节省空间。(也就是说“Segment”的大小实际上是“页”的整数倍,可执行文件中的各个段在映射时还是以“页”为单位,而“Segment”对应一个VMA,这个VMA内是权限属性相同的段以“页”映射的合并)

"Segment"和"Scetion"从不同的角度划分同一个可执行文件,"Section"是从链接过程的角度,将目标文件的相似段进行合并,得到可执行文件中的段,而"Segment"则是从可执行文件的装载过程的角度,将权限相似的段合并成"Segment"与虚拟空间建立映射关系,节省内存空间。
ELF可执行文件有一个专门的数据结构叫程序头表(Program Header Table),来保存Segment的信息,由于ELF目标文件不需要装载,因此没有程序头表,而ELF文件与共享库文件都有。

进程虚拟地址空间小结:
操作系统通过给进程空间划分出一个个VMA来管理进程的虚拟空间;基本原则是将相同权限属性的、有相同映像文件的映射成一个VMA;一个进程基本上可以分为以下几种VMA区域:

  • 代码VMA,权限只读,可执行;有映像文件
  • 数据VMA,权限可读写,可执行;有印象文件
  • 堆VMA,权限可读写,可执行;无映像文件;匿名,可向上扩展
  • 栈VMA,权限可读写,不可执行;无映像文件,匿名,可向下扩展

当讨论进程虚拟空间的“Segment”时,基本上为上述几种VMA。
ELF与Linux进程虚拟空间映射关系