编译和链接

简述

本文将从程序源代码到最终可执行文件的4个步骤:预编译编译汇编链接。说明它们的作用和相互之间的联系,IDE集成开发工具和编译器默认的命令通常会将这些步骤合并成一步,使得我们通常很少关注这些步骤。整个编译过程分为两大步:

  1. 编译:把文本形式的源代码翻译成机器语言,并形成目标文件。
  2. 链接:把目标文件 操作系统的启动代码和库文件组织起来形成可执行程序。

下图给出的是GCC的编译过程分解图:
编译和链接_第1张图片

1. 预编译

预编译过程主要处理那些源代码文件中以 “ # ” 开始的预编译指令。比如 “ #include ”、“ #define ”,主要处理规则如下:

  • 将所有的 “ #define ” 删除,并且展开所有的宏定义。
  • 处理所有条件预编译指令,比如 “ #if ”、“ #ifdef ” 、“ #elif ”、“ #else ”、“ #endif ”。
  • 处理 “ #include ” 预编译指令,将被包含的文件插入到改预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
  • 删除所有的注释 “ // ” 和 “ /**/ ” 。
  • 添加行号和文件名标识,比如#2 “ hello.c ”,以便于编译时编译器产生调试用的行号信息及用于编译错误或警告时能够显示行号
  • 保留所有的 #pragme 编译器指令,因为编译器须要使用它们。经过预编译后的 .i 文件不包括任何的宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到 .i 文件中。所以当我们无法判断宏定义是否正确或头文件包含是否正确时,可以查看预编译后的文件来确定问题。

2. 编译

编译过程就是把预处理玩的文件进行一系列的词法分析语法分析语义分析优化后生成相应的汇编代码文件,这个过程往往是我们所说的整个程序构建的核心部分,也是最复杂的部分之一。上述的四大步骤不属于本文的谈论范围,在这里就不过分深说。

3. 汇编

汇编器是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令。所以汇编器的汇编过程相对于编译器来讲比较简单,它没有复杂的语法,也没有语义,也不需要坐指令优化,只是根据汇编指令和机器指令的对照表一一翻译就可以了,“汇编”这个名字也来源于此。

4. 链接

链接过程的本质就是要把多个不同的目标文件相互 “粘” 到一起,或者说像玩具积木一样,可以拼装形成一个整体。为了使不同目标文件之间能够相互粘合,这些目标文件之间必须有固定的规则才行,就像积木模块必须有凹凸部分才能够拼合。在链接中,目标文件之间的相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。比如目标文件 B 要用到目标文件 A 中的函数 “ foo ” ,那么我们就称目标文件 A 定义(Define) 了函数 “ foo ” ,称目标文件 B 引用(Reference) 了目标文件 A 中的函数 “ foo ”。这两个概念也同样适用于变量。每个函数或变量都有自己独特的名字,才能避免链接过程中不同变量和函数之间的混淆。在链接中,我们将函数和变量统称为符号(Symbol),函数名和变量名就是符号名(Symbol Name)
我们可以将符号看作是链接中的粘合剂,整个链接过程正是基于符号才能够正确完成。链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表(Symbol Table),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(Symbol Value),对于变量和函数来说,符号值就是他们的地址。除了函数和变量之外,还存在其他几种不常用到的符号。我们将符号表中所有的符号进行分类,它们有可能是下面这些类型中的一种:

  • 定义在本目标文件的全局符号,可以被其他目标文件引用。
  • 在本目标文件中引用的全局符号,却没有定义在本目标文件,这一般叫做外部符号(External Symbol),也就是我们前面所讲的符号引用。
  • 段名,这种符号往往由编译器产生,它的值就是该段的起始地址。
  • 局部符号,这类符号只在编译单元内部可见。调试器可以使用这些符号来分析程序或崩溃时的核心转储文件。这些局部符号对于链接过程没有作用,链接器往往也忽略它们。
  • 行号信息,即目标文件指令与源代码中代码行的对应关系,它也是可选的。

对于我们来说,最值得关注的就是全局符号,即上面分类中的第一类和第二类。因为链接过程只关心全局符合的相互 “粘合” ,局部符号、段名、行号等都是次要的,它们对于其他目标文件来说是 “不可见” 的,在链接过程中也是无关紧要的。

链接还分为静态链接动态链接,具体详述的话,篇幅较长,所以留到其他文章中说明。

你可能感兴趣的:(C++)