2.1 编译过程
在Linux系统上,通常,只需使用gcc就可以完成整个编译过程。但不要被gcc的名字误导,事实上,gcc并不是一个编译器,而是一个驱动程序(driver program)。在整个编译过程中,gcc就像一个导演一样,编译过程中的每一个环节由具体的组件负责,如编译过程由cc1负责、汇编过程由as负责、链接过程由ld负责。
我们可以通过传递参数"-v"给gcc来观察一个完整的编译过程中包含的步骤,下面是一个典型的编译过程中gcc的输出信息,为了更清楚地看到编译过程中的主要步骤,对输出信息进行了适当删减。
根据gcc的输出可见,对于一个C程序来说,从源代码构建出可执行程序经过了三个阶段:
(1)编译
gcc调用编译器cc1进行编译,产生的汇编代码保存在目录/tmp下的文件ccYBInzt.s中。
(2)汇编
gcc调用汇编器as,汇编编译过程产生汇编文件cca2nBio.s,产生的目标文件保存在目录/tmp下的文件ccj54pkM.o中。
(3)链接
我们看到,gcc并没有如我们想象的那样直接调用ld进行链接,而是调用collect2进行链接。实际上,collect2只是一个辅助程序,最终它仍将调用链接器ld完成真正的链接过程。举个例子,对于C++程序来说,在执行main函数前,全局静态对象必须构造完成。也就是说,在main之前程序需要进行一些必要的初始化,gcc就是使用collect2安排初始化过程中如何调用各个初始化函数的。根据链接过程可见,除了main.c对应的目标文件ccj54pkM.o外,ld也链接了libc、libgcc等库,以及所谓的包含启动代码(start code)的启动文件(start/startup file),包括crt1.o、crti.o、crtbegin.o、crtend.o和crtn.o。
事实上,对于C程序来说,编译过程也可以拆分为两个阶段:预编译(或称为编译预处理)和编译。所以,软件构建过程通常分四个阶段:预编译、编译、汇编以及链接,如图2-1所示。
图 2-1 C程序的构建过程
在接下来讨论编译过程的章节中,如无特殊说明,都将以下面的程序为例。
2.1.1 预编译
在预编译阶段,预编译器将处理源代码中的预编译指令。一般而言,C语言中的预编译指令以“#”开头,常用的预编译指令包括文件包含命令"#include"、宏定义"#define",以及条件编译命令"#if"、"#else"、"#endif"等。
在工具链中,一般都提供单独的预编译器,比如GCC中提供的预编译器为cpp。但是,因为预编译也可以看作编译过程的第一遍(first pass),是为编译做的一些准备工作,所以通常编译器中也包含了预编译的功能。如在前面的编译过程中,我们看到gcc并没有单独调用cpp,而是直接调用cc1进行编译,原因就在于此。
以下面的程序为例,该程序使用了典型的预编译指令,我们通过观察这个程序的预处理的结果,来直观体会预编译过程。
我们可以使用选项-E告诉编译器仅作预处理,不进行编译、汇编和链接,具体命令如下:
预编译后的结果保存在文件hello.i中,其内容如下:
根据预编译后的结果可见,典型的预编译指令按照如下方式进行处理。
(1)文件包含
文件包含命令指示预编器将一个源文件的内容全部复制到当前源文件中。在上面的代码中,hello.c使用命令"#incude"指示预编器包含文件foo.h。而在预处理的输出文件hello.i中,结构体foo_struct的定义确实已经被复制到了文件hello.i中,也就是说,文件foo.h中的内容被包含到了文件hello.i中。
(2)宏定义
宏可以提高程序的通用性和易读性,减少不一致性和输入错误,便于维护。在预处理过程中,预编器将宏名替换为具体的值。比如,在hello.c的main函数中,经过预处理后,宏名PI已经被替换为具体的值3.1415926。
(3)条件编译
在大多数情况下,源程序中所有的语句都参加编译,但有的时候用户希望按照一定的条件去编译源程序的不同部分,这时可以使用条件编译。比如在函数main中,当定义了变量AREA时,编译器将编译"#ifdef"块的代码,否则编译"#else"块的代码。在上面代码中,因为在foo.h中定义了变量AREA,所以,在经过预处理的文件hello.i中,条件编译指令中的"#else"块的代码从源代码中被删除了,只保留了"#ifdef"块的代码。