上传时间:190826 (jzy)
C/C++语言由源代码生成可执行文件的各阶段如下:
源程序->预处理->编译、优化->汇编->链接->可执行文件
编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。链接是把目标文件、操作系统的启动代码和用到的库文件进行组织,形成最终生成可执行代码的过程。从上图可以看到,整个代码的编译过程分为编译和链接两个过程,编译过程对应预处理、编译优化、汇编等阶段,其余的属于链接过程。
目录
一、预处理阶段(preprocessor):
预处理过程读入源代码,检查包含伪指令(预处理指令)的语句和特殊符号等,即扫描源代码,对其进行初步地转换,产生新的源代码提供给编译器。预处理过程还会删除程序中的注释和多余的空白字符。尽管在目前绝大多数编译器都包含了预处理程序,但通常认为它们是独立与编译器的,且预处理过程先于编译器对源代码进行处理。
1、预处理指令
伪指令(预处理指令):预处理指令是以#号开头的代码行。#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符。整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。
预处理指令主要包含以下四个方面:
1.1 宏定义指令#define
宏定义了一个代表特定内容的标识符。预处理过程会把源代码中出现的宏标识符替换成宏定义时的值。宏定义最常见的用法是定义代表某个值的全局符号。宏的第二种用法是定义带参数的宏(宏函数),这样的宏可以像函数一样被调用,但它是在调用语句处展开宏,并用调用时的实际参数来代替定义中的形式参数。
注意:
(1)作为一种约定,习惯上总是全部用大写字母来定义宏,这样易于把程序的宏标识符和一般变量标识符区别开来。使用宏的好处有:一是使用方便;二是定义的宏有了意义,可读性强;三是容易修改。
(2)宏表示的值可以是一个常量表达式,允许宏嵌套(必须在前面已定义)。例如:
#define ONE 1
#define TWO 2
#define SUM (ONE+TWO)
这里需要注意括号的使用,尽管它们并不是必须的。但出于谨慎考虑,还是应该加上括号的。预处理仅是简单的字符替换,并不会处理优先级。
(3)宏还可以代表一个字符串常量,例如:
#define VERSION "Version 1.0"
(4)带参数的#define指令(宏函数)
带参数的宏和函数调用看起来有些相似。看一个例子:
#define SUM(x,y) ((x)+(y))
可以使任何数字表达式甚至函数调用来代替参数x、y。这里再次注意括号的使用。宏展开后完全包含在一对括号中,而且参数也包含在括号中,这样就保证了宏和参数的完整性。看一个用法:
sum = SUM(2, 3); 展开后变为 sum = (2 + 3);
1.2 #运算符(字符串化运算符)
出现在宏定义中的#运算符把跟在其后的参数转换成一个字符串。有时把这种用法的#称为字符串化运算符。例如:
1.3 ##运算符(参数连接运算符)
##运算符用于把参数连接到一起。预处理程序把出现在##两侧的参数合并成一个。如:
2、条件编译指令
可以通过定义不同的宏来决定编译程序对哪些代码进行处理。条件编译指令将决定那些代码被编译,而哪些是不被编译的。可以根据表达式的值或者某个特定的宏是否被定义来确定编译条件。这些指令包括:#if/#ifdef/#ifndef/#else/#elif/#endif,#ifdef和#ifndef这二者主要用于防止重复包含。
3、特殊符号
预编译程序可以识别一些特殊的符号。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
__FILE__ 包含当前程序文件名的字符串
__LINE__ 表示当前行号的整数
__DATE__ 包含当前日期的字符串
__STDC__ 如果编译器遵循ANSI C标准,它就是个非零值
__TIME__ 包含当前时间的字符串
注意:是双下划线,而不是单下划线
#error指令将使编译器显示一条错误信息,然后停止编译。
#line指令改变_LINE_与_FILE_的内容,它们是在编译程序中预先定义的标识符。
#pragma指令没有正式的定义。编译器可以自定义其用途。典型的用法是禁止或允许某些烦人的警告信息。
4、头文件包含指令
采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
#include预处理指令的作用是在指令处展开被包含的文件。包含可以是多重的,也就是说一个被包含的文件中还可以包含其他文件。标准C编译器至少支持八重嵌套包含。预处理过程不检查在转换单元中是否已经包含了某个文件并阻止对它的多次包含,这个的处理办法使用上面给出的条件预处理指令(#ifdef、#ifndef)。
二、编译及优化
经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字,如main, if , else , for , while , { , } , + , - , * , \ 等等。编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。
对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。
后一种类型的优化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。
经过优化得到的汇编代码必须经过汇编程序的汇编转换成相应的机器指令,方可能被机器执行。
三、汇编
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件(object file)。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。
目标文件由段组成。通常一个目标文件中至少有两个段:
1) 代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
2) 数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
UNIX环境下主要有三种类型的目标文件:
1) 可重定位文件
其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。
2) 共享的目标文件
这种文件存放了适合于在两种上下文里链接的代码和数据。
第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件;
第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。
3) 可执行文件
它包含了一个可以被操作系统创建一个进程来执行之的文件。汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。
四、链接
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。
根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:
1. 静态链接
在这种链接方式下,函数的代码将从其所在的静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
2. 动态链接
在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。
自我总结:在编译(此处的编译是指整个编译过程)完可执行文件后,若使用的是静态链接,则静态库会被编译进可执行文件,此时无需将静态库放入可执行文件运行的环境。若使用的是动态链接,则动态库不会被编译进可执行文件,此时运行可执行文件需将动态库放入运行环境中的能被找到库文件的路径中。
五、GCC的编译链接命令
在linux使用的gcc编译器便是把以上的几个过程进行捆绑,使用户只使用一次命令就把编译工作完成,下图为GCC代理的编译过程。
从上图可以看到:
1) 预编译
将.c 文件转化成 .i文件
使用的gcc命令是:gcc –E
对应于预处理命令cpp
2) 编译
将.c/.h文件转换成.s文件
使用的gcc命令是:gcc –S
对应于编译命令 cc –S
3) 汇编
将.s 文件转化成 .o文件
使用的gcc 命令是:gcc –c
对应于汇编命令是 as
4) 链接
将.o文件转化成可执行程序
使用的gcc 命令是: gcc
对应于链接命令是 ld
一般情况下,只需要知道分成编译和链接两个阶段,编译阶段将源程序(*.c) 转换成为目标代码(一般是obj文件),链接阶段是把源程序转换成的目标代码(obj文件)与你程序里面调用的库函数对应的代码连接起来形成对应的可执行文件。
注:对于makefile中如何链接库的命令语句、GCC的编译命令以及部分对上文内容的补充,以后有机会再添加。如有错误,可评论修改或联系我。