一、为什么需要编译器
使用编辑器创建C语言源文件,并保存为以c为扩展名结尾的源文件。
比如下面的C程序保存为A.c。
#include <stdio.h>
int main()
{
printf("hello, world!\n");
}
创建该文件后,该文件中的程序代码以字节序列的形式存储在文件中。每个字节都有一个对应于某个字符的值。比如,第一个字节的值为35,对应于字符“#”;第二个字节值为105,对应于字符“i”,以此类推。
但这些字节仅仅表示一个个字符,机器不能读懂这些信息,所以我们需要一些程序来将这些信息翻译成机器能读懂的信息,而这些程序就组成了编译系统。在编译系统中,最主要的就是编译器。
我们只需要把C语言源文件作为输入,让其经过编译系统的处理,便可得到可执行文件,机器就通过读可执行文件来执行相应的功能。
二、Linux中编译和执行C程序的步骤
编译系统可划分为四个程序——预处理器、编译器、汇编器与链接器。
下图展示了一个C语言源文件是如何被编译系统处理的。
接下来我将依次介绍编译系统处理C语言源文件的四个阶段。
(一)预处理(Preprocessing)
预处理由预处理器完成,而预处理器由编译器自动调用。
编译C程序从编译预处理指令(例如#include)开始。
以A.c为例,预处理指令(#include<stdio.h>)告诉预处理器读取系统头文件stdio.h的内容,并将该文件内容直接插入源文件里面,这称为预处理指令被展开。
源文件被预处理后生成以i为扩展名的文件,该文件通常不会存储在磁盘上。
但可以通过单独执行以下命令来得到存储在磁盘上的以i为扩展名的文件。
cpp A.c > A.i
生成A.i后,我们可以查看该文件里面的内容。内容已展示在附录A。
我们会发现,原本5行的源文件,经过预处理器处理后,生成的文件有743行,这些多出来的内容就是所有宏的源代码。
(二)编译(Compilation)
编译由编译器完成。
现代编译可分为6个阶段——词法分析、语法分析、语义分析、中间代码生成、机器无关代码优化与目标代码生成。
通过执行下面命令便可得到汇编语言源程序文件A.s。
gcc -S A.i
A.s会被存储在磁盘上,其内容已展示在附录B中。
(三)汇编(Assembly)
汇编由汇编器完成。
汇编器会将汇编语言翻译为机器语言指令,生成以o为扩展名的目标文件A.o。
我们可以通过下面命令来得到A.o。
as A.s -o A.o
A.o被存储在磁盘上,该文件是二进制文件,我们现在没必要去查看其内容。
(四)链接(Linking)
链接由链接器完成。
为什么需要链接呢?不是已经得到机器语言指令了吗?
这是因为一个可执行文件可能需要许多外部资源(系统函数、C运行时库等)。具体到A.c,就是需要打印(printf)函数,该函数被包含在一个单独的预编译目标文件printf.o中。所以,我们需要链接这些文件。最终,我们可以得到一个可执行文件A。
执行链接的命令十分复杂,有兴趣的读者可以去了解。
从上面可以发现,编译系统处理一个C语言源文件需要执行较多的命令,尤其是链接命令十分复杂。为了提高效率,可以通过一个命令直接将C语言源文件翻译成可执行文件,如下。
gcc A.c -o A
三、现代编译器的构造/阶段
现代编译器的整体结构如下图。
或者说按如下方式划分。
而本博客所介绍的内容如下:
- 词法分析
- 语法分析
- 语义分析
- 中间代码生成
- 机器无关代码优化
- 目标代码生成
(一)词法分析
功能:将程序字符流分解为记号流(Tokens)。
(二)语法分析(Parsing)
功能:由记号流创建语法树。
(三)语义分析
检查程序中的不一致。
(四)中间代码生成
生成与文法和目标机器都无关的中间语言。
(五)机器无关代码优化
(六)目标代码生成
生成最终的机器语言指令。
参考资料:
[1]gcc Compilation Process and Steps of C Program in Linux
[2]