首页 > 代码库 > C程序的编译和链接(一)
C程序的编译和链接(一)
本文主要讲述了一个C程序从源代码到目标文件所经过的步骤,介绍了编译系统,预处理、编译、汇编和链接的相关知识。
一、编译系统
一个C程序的生命周期从高级C语言程序开始。想要在系统上执行.c程序,每条C语句都必须翻译为低级的机器语言指令,将这些指令按照可执行目标程序的格式打包,以二进制磁盘文件的形式存放,这就是可以由系统执行的可执行目标文件。这些工作包含如下图所示的四个过程,由编译系统完成。
一般而言,编译系统包括预处理器、编译器、汇编器和链接器。
注意一下上图中各个阶段输出的文件格式是文本文件还是二进制文件。
1.预处理阶段
源代码.c文件和相关的头文件如stdio.h等被预处理器编译为一个.i文件。
预处理过程主要处理那些源代码文件中的以#开头的预处理指令。主要规则如下:
a.删除所有的#define,并展开所有的宏定义;
b.处理所有条件预处理指令,如#if #ifdef #elif #else #endif;
c.处理#include预处理指令,将被包含的文件插入到该预处理指令的位置;
d.删除所有的注释// /* */;
e.添加行号和文件名标识,如#2 ‘‘hello.c" 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号;
f.保留所有的#pragma编译器指令,因为编译器要使用它们。
从上面几条规则我们可以知道:
a.预处理得到.i文件中,已经将所有宏都展开,不再包含任何宏定义,而且头文件也被插入到相应的位置。
b.注释信息也是在预处理阶段进行处理的,也就是说构建后得到的目标文件不包含注释内容,源文件中添加多少注释对最后的文件的大小没有影响。
c.编译报错时的行号信息和文件标识也是在这里产生的。
2.编译阶段
预处理之后得到.i文件,再对.i文件进行编译,得到.s文件。
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后产生相应的汇编代码文件。汇编程序中的每条语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。
3.汇编阶段
.s文件汇编之后得到.o文件,即目标文件。
汇编器将汇编代码转变成机器可以执行的指令,并把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件中。
.o文件是二进制文件,其字节编码是机器语言指令。
4.链接阶段
链接器将相关的.o文件合并,得到可执行目标文件。可执行文件能够被加载到内存中,由系统执行。
二、编译器的工作
编译过程如下图所示,一般可以分为6步:扫描、语法分析、语义分析、源代码优化、代码生成和目标代码优化。
1.词法分析
记号(Token)一般可以分为如下几类:关键字、标识符、字面量(数组、字符串等)和特殊符号(加减号、等号等)。
词法分析的工作由扫描器(Scanner)完成,它利用类似于有限状态机的算法将源代码的字符序列分割成一系列的记号,同时将标识符放到符号表,将字面量放到文字表等。
2.语法分析
语法分析器(Grammer Parser)采用用上下文无关语法(Context-free Grammar)对记号进行语法分析,产生语法树(Syntax Tree)。许多运算符号的优先级和含义也在这个阶段确定,如圆括号优先级比乘法高,*表示乘法还是对指针取内容。
语法树就是以表达式(Expression)为节点的树。
如果发现表达式不合法,编译器会报错。
3.语义分析
语法分析仅仅完成了对表达式的语法分析,但并不了解表达式的含义。
语义分析器(Semantic Analyzer)完成语义分析。
注意编译器只能做静态语义(Static Semantic)分析,就是在编译器可以确定的语义,包括声明和类型的匹配、类型的转换。语义分析后,语法树的表达式会被标识类型。
动态语义(Dynamic Semantic)只有在运行期才能确定,编译器不能确定。
4.中间语言生成
对代码进行优化时,源码级优化器(Source Code Optimizer)会在源代码上进行优化,但直接在语法树上做优化比较困难,因此源码级优化器将语法树转换为中间代码(Intermediate Code)。
中间代码是语法树的顺序表示,它接近目标代码,但是它不包含数据的尺寸、变量地址和寄存器的名字等,所以跟目标机器和运行时环境无关。
中间代码类型有许多,常见的有三地址码和P-代码。
5.目标代码的生成与优化
生成中间代码之后的过程,就属于编译器后端了。编译器后端主要包括代码生成器(Code Generator)和目标代码优化器(Target Code Optimizer)。
代码生成器将中间代码转换为目标机器代码,这个过程要确定字长、寄存器、整数数据类型和浮点数类型等,因此十分依赖于目标机器。
目标代码优化器对目标代码进行优化,如确定寻址方式、删除多余指令和用位移代替乘法运算等操作。
经过这些步骤,源代码被编译为目标文件。
这里需要考虑一个问题,如果源代码所需的变量名、函数名等都在同一个编译单元里,编译器可以确定它们的地址。但是如果有些变量名或函数名定义在其他的程序模块里,该如何处理?
事实上,定义在其他模块的全局变量和函数在最终运行时的绝对地址都是在链接的时候确定下来的。
三、链接器的作用
一个程序被分为很多模块,模块之间如何通信呢?
静态语言C模块之间的通信有两种方式,即函数调用和变量访问。函数调用需要知道目标函数的地址,变量访问要知道目标变量的地址,这两种方式归结起来就是模块间符号的引用。而这个工作就由链接器完成。
链接程序的主要工作就是将每个源代码独立编译后得到的目标文件连起来,即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有目标文件能正确地衔接,成为一个能够由操作系统装入执行的统一整体。
链接过程主要包括了地址和空间分配、符号决议和重定位等步骤。
连接方式有两种:静态链接和动态连接。在后续文章中进行详细的介绍。
C程序的编译和链接(一)