编译和链接

现在IDE一般都将编译和链接的过程一步完成,通常将这种编译和链接合并到一起的过程称为构建 (Build)。 即使使用命令行来编译一个源代码文件,简单的一句“gcc hello.c”命令就包含了非常复杂的过程。

事实上,上述过程可以分解为4个步骤,分别是预处理 (Prepressing)、 编译 (Compilation)、 汇编 (Assembly) 和链接 (Linking)

预编译 (预处理)

首先是源代码文件 hello.c 和相关的头文件,如 stdio.h等被预编译器cpp 预编译成一个.i文件
第一步预编译的过程相当于如下命令(-E 表示只进行预编译 , -o表示输出文件):

$gcc -E hello.c -o hello.i

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

  1. 将所有的“#define” 删除,并且展开所有的宏定义。
  2. 处理所有条件预编译指令,比如“#if” 、“#ifdef” 、“#elif” 、”#else" 、“#endif"。
  3. 处理“#include” 预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这 个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
  4. 删除所有的注释“//”和“/**/”。
  5. 添加行号和文件名标识,比如#2 “hello.c”2,以便于编译时编译器产生调试用的行号 信息及用于编译时产生编译错误或警告时能够显示行号。
  6. 保留所有的#pragma 编译器指令,因为编译器须要使用它们。

编译 (转为汇编)

编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生 产相应的汇编代码文件
上面的编译过程相当于如下命令:

$gcc -S hello.i -o hello.s

或者使用如下命令:

$gcc -S hello.c -o hello.s

现在版本的GCC 把预编译和编译两个步骤合并成一个步骤,使用一个叫做 cc1的程序 来完成这两个步骤。这个程序位于“/usr/lib/gcc/i486-linux-gnu/4.1/”, 我们也可以直接调用cc1 来完成它
对于C 语言的代码来说,这个预编译和编译的程序是ccl, 对于C++来说,有对应的程序叫做 cclplus;Java是jcl。所以实际上 gcc 这个命令只是这些后台程序的包装,它会根据不同的参数要求去调用 预编译编译程序 cc1、 汇编器 as、链接器 ld。

汇编 (转为机器语言)

汇编器是将汇编代码转变成机器可以执行的指令
上面的汇编过程我们可以调用汇编器 as来完成:

$as hello.s -o hello.o

或者

$gcc -c hello.s -o hello.o

可以使用 gcc命令从C 源代码文件开始,经过预编译、编译和汇编直接输出目标文件

gcc -c hello.c -o hello.o

链接(将多个源文件链接)

链接通常是一个让人比较费解的过程,为什么汇编器不直接输出可执行文件而是输出一 个目标文件呢?链接过程到底包含了什么内容?为什么要链接?

ld -static crtl.o crti.o crtbeginT.o hello.o-start-group -lgcc  -lgcc_eh  -lc-end-group crtend.o crtn.o

我们需要将一大堆文件链接起来才可以得到“a.out”, 即最终的可执行文件。 看了这行复杂的命令,可能很多读者的疑惑更多了, crtl.o、crti.o、crtbeginTo、crtend.o、 crtn.o这些文件是什么?它们做什么用的?-lgcc -lgcc ch -lc 这些都是什么参数?为什么要 使用它们?为什么要将它们和 hello.o链接起来才可以得到可执行文件?
这我们就要看看编译器做了什么

编译器做了什么

编译器就是将高级语言翻译成机器语言的一个工具
编译过程一般可以分为6步:扫描、语法分析、 语义分析、源代码优化、代码生成和目标代码优化

  1. 词法分析
    首先源代码程序被输入到扫描器,运用一种类似于有限状态机(Finite State Machine)的算法可以很轻松地将源 代码的字符序列分割成一系列的记号
  2. 语法分析
    接 下来语法分析器将对由扫描器产生的记号进行语法分析,从而 产生语法树。整个分析过程采用了上下文无关语法的分析手段
  3. 语义分析
    由语义分析器来完成。语法分析仅 仪是完成了对表达式的语法层面的分析,但是它并不了解这个语句是否真正有意义
  4. 中间语言生成
    源码级优化器,源代码级优化器会在源代码级别进行优化
  5. 目标代码生成与优化
    编译器后端主要包括代码生成器和目标代码优化器,代码生成器将中间代码转换成目标机器代码,最后目标代码优化器对上述的目标代码进行优化,比如选择合适的寻址方式、使用位移来代替乘法运算、删除多余的指令等

编译器所能分析的语义是静态语义所谓静态语义是指在编译期可以确定的语义,与之对应的动态语义就是只有在运行期才能确定的语义
静态语义通常包括声明和类型的匹配,类型的转换
动态语义一般指在运行期出现的语义相关的问题,比如将0作为除数是一 个运行期语义错误。

目标代码中有变量定义在其他模块, 该怎么办?事实上,定义其他模块的全局变量和函数在最终运行时的绝对地址都要在最终链 接的时候才能确定。所以现代的编译器可以将一个源代码文件编译成一个未链接的目标文 件,然后由链接器最终将这些目标文件链接起来形成可执行文件。

链接器

重新计算各个目标的地址过程被叫做重定位
属于静态语言的C/C++ 模块之间通信有两种方式 一种是模块间的函数调用,另外一种是模块间的变量访问。这两种方式都可以归结为一种方式,那就是模块间符号的引用
这个模块的拼接过程就是:链接

模块拼装—静态链接

人们把每个源代码模块独立地编译,然后按照须要将它们“组装”起来,这个组装模块的过程就是链接
链接过程主要包括了地址和空间分配、 符号决议和重定位等这些步骤
每个模块的源代码文件(如.c) 文件经过编译器 编译成目标文件, 目标文件和库一起链接形成最终可执行文件

比如我们在程序模块 main.c中使用另外一个模块func.c中的函数 foo()。 我们在 main.c 模块中 每一处调用foo 的时候都必须确切知道foo 这个函数的地址,但是由于每个模块都是单独编 译的,在编译器编译main.c 的时候它并不知道foo 函数的地址,所以它暂时把这些调用foo 的指令的目标地址搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正。如果 没有链接器,须要我们手工把每个调用 foo 的指令进行修正,则填入正确的foo 函数地址。 当func.c模块被重新编译, foo 函数的地址有可能改变时,那么我们在 main.c 中所有使用到 foo 的地址的指令将要全部重新调整。这些繁琐的工作将成为程序员的噩梦。使用链接器,你可以直接引用其他模块的函数和全局变量而无须知道它们的地址,因为链接器在链接的时 候,会根据你所引用的符号 foo, 自动去相应的 func.c模块查找 foo 的地址,然后将main.c 模块中所有引用到 foo 的指令重新修正,让它们的目标地址为真正的foo 函数的地址。这就 是静态链接的最基本的过程和作用。

假设我们有个全局变量叫做var, 它在目标文件A 里面。我们在目标文件B 里面要访问这个全局变量,由于在编译目标文件B 的时候,编译器并不知道变量 var的目标地址,所以编译器在没法确定地址的情况下,将这条 mov 指令的目标地址置为0,等待链接器在将目标文件A 和 B 链接起来的时候再将其修正
这个地址修正的过程也被叫做 重定位 (Relocation),每个要被修正的地方叫一个重定位入口


参考资料:<<程序员的自我修养>>

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐