GCC编译过程分解
GCC 原名为 GNU C 语言编译器(GNU C Compiler),因为它原本只能处理 C语言。GCC 很快地扩展,变得可处理 C++。后来又扩展能够支持更多编程语言,如Fortran、Pascal、Objective-C、Java、Ada、Go以及各类处理器架构上的汇编语言等,所以改名GNU编译器套件(GNU Compiler Collection)。
编译器的工作是将源代码(通常使用高级语言编写)翻译成目标代码(通常是低级的目标代码或者机器语言),在现代编译器的实现中,这个工作一般是分为两个阶段来实现:
- 编译器的前端接受输入的源代码,经过词法、语法和语义分析等等得到源程序的某种中间表示方式。
- 编译器的后端将前端处理生成的中间表示方式进行一些优化,并最终生成在目标机器上可运行的代码。
GCC 设计中有两个重要的目标:
- 硬件无关性:在构建支持不同硬件平台的编译器时,它的代码能够最大程度的被复用。
- 要生成高质量的可执行代码,这就需要对代码进行集中的优化。
为了实现这两个目标,GCC 内部使用了一种硬件平台无关的语言,它能对实际的体系结构做一种抽象,这个中间语言为 RTL(Register Transfer Language)。
GCC的编译过程
GCC的编译过程可以分为以下四个阶段:预处理(或预编译)、编译、汇编、链接,如下图所示:
以下面代码为例:
int main()
{
printf("Hello World\n");
return 0;
}
include两种方式:
#include<> 引用的是编译器的类库路径里面的头文件。
#include” “ 引用的是程序目录的相对路径中的头文件。
gcc命令只是后台程序的包装,它会根据不同的参数要求去调用预编译编译程序cc1、汇编器as、链接器ld。
预编译
# gcc –E test.c –o test.i
以下为test.i部分内容:
# 1 "test.c"
{
printf("Hello World"\n);
return 0;
}
将所有的#define删除,并且展开所有的宏定义;预处理过程主要处理那些源代码中以#开始的预处理指令,主要处理规则如下:
- 处理所有条件编译指令,如#if,#ifdef等;
- 处理#include预处理指令,将被包含的文件插入到该预处理指令的位置。该过程递归进行,及被包含的文件可能还包含其他文件。
- 删除所有的注释//和 /**/;
- 添加行号和文件标识,如#2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号信息;
- 保留所有的#pragma编译器指令,因为编译器须要使用它们;
经过预编译后的.i文件不包含任何宏定义,因为所有的宏都已经被展开,并且包含的文件也已经被插入到.i文件中。所以当无法判断宏定义是否正确或头文件包含是否正确使,可以查看预编译后的文件来确定问题。
编译
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件。这个过程是整个程序构建的核心部分,也是最复杂的部分之一。
以下为test.s部分内容:
汇编
汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可。
test.o的内容为机器码,不能以文本形式方便的呈现(不过可以利用 objdump -S file 查看源码反汇编)。利用hexdump 查看如下:
链接
链接器ld将各个目标文件组装在一起,解决符号依赖,库依赖关系,并生成可执行文件。如下形式:
ld –static crt1.o crti.o crtbeginT.o test.o –start-group –lgcc –lgcc_eh –lc-end-group crtend.o crtn.o (省略了文件的路径名)
# gcc -o test test.o或者# ld -o test test.o
test程序调用了printf 函数,这个函数是标准C库中的一个函数,它保存在一个名为printf.o 的文件中,这个文件必须以某种方式合并到test.o的程序中。
链接器ld负责处理这种合并。结果得到test可执行文件,可以被加载到内存中由系统执行。
小结
以上过程可以参考下图:
转载于:http://chengqian90.com/2017/C%E8%AF%AD%E8%A8%80/GCC%E7%BC%96%E8%AF%91%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3/