目录
在Linux下, 当我们使用GCC来编译程序时, 只须使用最简单的命令
$ gcc hello.c
事实上, 上述过程可以分解为4个步骤, 分别是预处理(Prepressing,又称为预编译) 、 编译(Compilation) 、 汇编(Assembly) 和链接(Linking) , 如图所示:
预处理
首先是源代码文件hello.c和相关的头文件, 如stdio.h等被预编译器cpp预编译成一个.i文件(对于C++程序来说, 它的源代码文件的扩展名可能是.cpp或.cxx,头文件的扩展名可能是.hpp, 而预编译后的文件扩展名是.ii)。 第一步预编译的过程相当于如下命令(-E表示只进行预编译) :
$gcc –E hello.c –o hello.i
or:
$cpp hello.c > hello.i
预编译过程主要处理那些源代码文件中的以“#”开始的预编译指令。 比如“#include”、 “#define”等, 主要处理规则如下:
- 将所有的“#define”删除, 并且展开所有的宏定义。
- 处理所有条件预编译指令, 比如“#if”、 “#ifdef”、 “#elif”、 “#else”、 “#endif”。
- 处理“#include”预编译指令, 将被包含的文件插入到该预编译指令的位置。 注意, 这个过程是递归进行的, 也就是说被包含的文件可能还包含其他文件。
- 删除所有的注释“//”和“/* */”。
- 添加行号和文件名标识, 比如#2“hello.c”2, 以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
- 保留所有的#pragma编译器指令, 因为编译器须要使用它们。
经过预编译后的.i文件不包含任何宏定义, 因为所有的宏已经被展开, 并且包含的文件也已经被插入到.i文件中,而此时的hello.i相比hello.c文件要庞大许多。 所以当我们无法判断宏定义是否正确或头文件包含是否正确时, 可以查看预编译后的文件来确定问题.
编译
编译过程就是把预处理完的文件进行一系列词法分析、 语法分析、 语义分析及优化后生产相应的汇编代码文件, 这个过程往往是我们所说的整个程序构建的核心部分, 也是最复杂的部分之一, 这涉及编译原理等一些内容.上面的编译过程相当于如下命令:
$gcc –S hello.c –o hello.s
现在版本的GCC把预编译和编译两个步骤合并成一个步骤, 使用一个叫做cc1的程序来完成这两个步骤。 这个程序位于“/usr/lib/gcc/i686-linux-gnu/5.4.0/cc1”, 我们也可以直接调用cc1来完成它:
$ /usr/lib/gcc/i686-linux-gnu/5.4.0/cc1 hello.i
以上两种方式都可以得到汇编输出文件hello.s。 对于C语言的代码来说, 这个预编译和编译的程序是cc1, 对于C++来说, 有对应的程序叫做cc1plus; Objective-C是cc1obj;fortran是f771; Java是jc1。 所以实际上gcc这个命令只是这些后台程序的包装,
它会根据不同的参数要求去调用预编译编译程序cc1、 汇编器as、 链接器ld。
汇编
汇编器是将汇编代码转变成机器可以执行的指令, 每一个汇编语句几乎都对应一条机器指令。 所以汇编器的汇编过程相对于编译器来讲比较简单, 它没有复杂的语法, 也没有语义, 也不需要做指令优化, 只是根据汇编指令和机器指令的对照表一一翻译就可以了, “汇编”这个名字也来源于此。 上面的汇编过程我们可以调用汇编器as来完成:
$as hello.s –o hello.o
or:
$gcc –c hello.c –o hello.o
链接
链接通常是一个让人比较费解的过程, 为什么汇编器不直接输出可执行文件而是输出一个目标文件呢? 链接过程到底包含了什么内容? 为什么要链接? 这恐怕是很多读者心中的疑惑。 事实上,链接也是一个比较复杂的过程,我们在单独的一篇文章链接中介绍。