程序的编译,装载与链接过程

1. 程序编译四个过程:

  • 预处理(Prepressing):源代码和相关的头文件被预编译器cpp预编译为一个 .i 文件(#define ,#include,#if,删除注释行)
  • 编译(Compilation):将预处理之后的文件进行一系列词法分析,语法分析,语义分析及优化后生产相应的汇编代码文件
  • 汇编(Assembly):将汇编代码转化为机器可以执行的机器代码,例如使用gcc命令从C源代码文件开始,经过预编译,编译和汇编直接输出目标文件(Object File)(还没有经过链接的过程)
  • 链接(Linking):
    目标文件和库一起链接形成最中的可执行文件
    • 经过扫描,语法分析,语义分析,源代码优化,代码生成和目标代码优化,编译器忙活了这么多个步骤以后,源代码终于可以被编译成了目标代码。但是这个目标代码有一个问题:index和array的地址还没有确定。
    • 如果我们要把目标代码使用汇编器编译成能够执行的指令,那么index和array的地址应该从哪里得到呢还有and so on?事实上,定义其他模块的全局变量和函数在最终运行时的绝对地址都是要在最终链接的时候才能确定。
    • 所以现代编译器可以将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终确定这些目标文件链接起来形成可执行文件。
    • 可执行文件的文件格式在linux下为ELF,文件后缀为 .o ,在 windows下为PE文件格式。
    • 文件格式如下:文件头,代码段,数据段和只读数据段,BSS段,其他段等等
      程序的编译,装载与链接过程

2. 静态链接

对于链接器而言,整个链接过程就是将几个输入目标文件加工后合并成一个输出文件,如何将多个输入文件,将他们的各个段合并输出到输出文件?
相似段合并:
程序的编译,装载与链接过程

  1. 第一步:空间与地址分配(分析这两个步骤中链接器的工作过程,在第一步的扫描和空间分配阶段,链接器按照前面介绍的空间分配方法进行分配,这时输入文件中各个段在链接后的虚拟地址就已经确定,比如.text段的起始位置和.data的起始位置)
  2. 第二步:符号解析与重定位

重定位:我们在程序模块main.C中使用另外一个模块func.c中的函数foo()我们在每一处调用foo的时候都必须确切知道foo这个函数的地址,所以它暂时把这些调用foo的指令的目标地址搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正,则填入正确的foo函数地址。当func.c模块重新编译,foo函数位置地址有可能改变,那么我们在main.c中所有使用到foo的地址的指令将要全部重新调整。

3. 可执行文件的装载

程序是一个静态的概念:预编译好的指令和数据集合的一个文件。进程则是一个动态的概念:是程序运行时的一个过程。

  • 我们知道每个程序运行起来,它将有自己独立的虚拟地址空间(Virtual address Space)
  • 程序运行时需要将所需要的指令和数据必须放在内存中才能够正常运行。
页映射

就是将内存和所有磁盘中的数据和指令按照页(page)为单位进行划分,然后一一形成映射关系。

从操作系统角度看可执行文件的装载

如果程序使用物理地址直接操作,那么每次页被装入时都需要进行重定位。在虚拟存储中,现代的硬件MMU提供地址转换功能。有了硬件的地址转换+页映射机制,操作系统动态加载可执行文件与静态加载有了很大的区别。

进程的建立

  1. 创建一个独立的虚拟地址空间(虚拟空间由一组页映射函数将虚拟空间的各个页映射至相应的物理空间)
  2. 读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系
  3. 将CPU的指令寄存器设置成可执行文件(映像文件)的入口地址,启动运行
    页错误

4.动态链接

静态链接浪费空间:每个程序内部除了都保留和printf()函数,scanf()函数等这样的公共库函数,还有数量相当可观的其他库函数以及辅助数据结构。例如,program1与program2都用到了Lib.o这个模块,它们同时在链接输出可执行文件program1和program2有两个副本。当我们同时运行program1与program2时,Lib.o在磁盘和内存中都有两份副本。

静态链接更新,部署和发布也很困难。如program1所使用的Lib.o是一个第三方提供的,当该方更新了Lib.o时候,那么program厂商就需要拿到最新的Lib.o,然后将其与Program1.o链接后,将新的program1整个发布给用户。
导出表
导入表

5. 堆栈管理

栈:用于维护函数调用的上下文
堆:是用来容纳应用程序动态分配的内存区域。光有栈对于面向过程的程序设计还远远不够,因为栈上的数据在函数返回的时候就会被释放掉,所以无法将数据传递至函数外部。而全局变量没法动态的产生,只能编译的时候定义,有很多情况下缺乏表现力。在这种情况下,堆是唯一的选择。