GNU/GCC 编译过程可以被细分为四个阶段:
◆ 预处理(Pre-Processing)
◆ 编译(Compiling)
◆ 汇编(Assembling)
◆ 链接(Linking)
Linux程序员可以根据自己的需要让GCC在编译的任何阶段结束,以便检查或使用编译器在该阶段的输出信息,或者对最后生成的二进制文件 进行控制,以便通过加入不同数量和种类的调试代码来为今后的调试做好准备。和其它常用的编译器一样,GCC也提供了灵活而强大的代码 优化功能,利用它可以生成执行效率更高的代码。
GCC编译器由编译预处理组件 cpp 和针对特定语言的编译器 cc1 组成
[xxxx@ test_paag]$gcc –S -O2 main.c -o main.s
此编译命令解释:命令行中的GCC是一个驱动程序,它接受命令行参数并解释,然后根据命令行参数解释的结果选择下一步的步骤,最后调用编译组件cc1进行编译产生汇编指令*.s文件。
gcc编译器的工作过程:
GCC编译器的前端将高级语言源码经过词法分析、语法分析生成与高级语言无关的低级中间层表示GENERIC[36],然后经过单一化赋值转化为另一种中间表示层GIMPLE[36],在中间层GIMPLE组建控制流程图(Control Flow Graph,CFG),并在GIMPLE上进行一系列的优化,然后将其转换为更加便于优化的RTL(Register Transfer Language,RTL)中间表示层[37],在RTL层做很多遍优化(pass),最后对于每条RTL通过模板匹配(Pattern Match)调用对应的汇编模板生成汇编代码。GCC编译流程图如下图所示。
前端
GCC编译器的前端非常强大,经过多年的发展,GCC 编译器前端已经变得相当成熟,它能编译现在计算机界主流的高级编程语言,前端支持多种编程语言,如 C、Fortran、Pascal、Objective-C、Java、Ada、Go等,同样支持这些语言的标准库libstdc++、 libgcj 等。
GCC编译的前端将不同的高级编程语言经过词法分析、语法分析转化为与前端语言无关的统一的中间表示。有了与前端无关的中间表示,GCC的前端将不同的高级编程语言转换成这种中间表示,这就是GCC处理器支持多种编程语言的根本原因。
后端
编译器分为前端和后端,在GCC的组成结构中,后端负责将RTL表示的中间形式进行优化并最终生成对应平台的汇编代码。GCC后端是在RTL的中间形式上进行的,在RTL上做大量的优化,包括机器有关的优化和机器无关的优化。
RTL是从特定机器平台中抽象出的模型表示的,实际上它是一种抽象出来的机器模型。和所有实际存在的平台一样,这个抽象出来的机器模型也有它自己的“指令系统”。机器模型的“指令系统”在GCC中定义为标准指令模板的一系列指令,这些指令是GCC内部使用的。GCC的后端为不同的平台提供了描述这些指令模板的接口文件,这些指令模板在不同平台上的描述将GCC映射到不同的平台上去,这样就可以把GCC的后端移植到不同的平台上去。通过机器描述支持多种体系结构,机器描述文件给出机器各种参数的宏定义和指令集。这使得GCC移植非常方便,针对目前广泛使用的目标机器的体系结构提供了大部分的支持,后端支持的平台有x86、mips、Alpha、ARM、AVR、IA-64、SPARC、PowerPC等30多种平台。
GCC编译器的前端将高级语言源码经过词法分析、语法分析生成与高级语言无关的低级中间层表示,后端负责将RTL表示的中间形式进行优化并最终生成对应平台的汇编代码。GCC的前端到后端一共用了三种中间表示,GCC用遍管理器将其连成一个整体。GCC在编译和优化中,对编译对象(一般以函数或文件为处理对象)的一次编译处理,称为pass(遍)。GCC的整个编译处理过程组成了pass_list,这个pass_list包含的所有遍就是整个GCC编译时所经过的过程。遍管理器在GCC编译流程中的作用如下图所示:
GCC编译器前端将源语言进行词法分析、语法分析后得到抽象语法树(AST),该过程为GENERIC。GENERIC之后简化为GIMPLE的中间表示,在GIMPLE层上加入控制流程图(CFG),在这个中间表示层上进行一系列的前期优化(例如,过程间优化IPO,静态单一化赋值SSA等),再将GIMPLE转换成RTL中间表示,同样在RTL上进行一系列的优化,最终生成汇编代码,整个过程在图1中用带箭头的实线表示。从GENERIC到GIMPLE再到RTL,在GCC中是用一系列的遍(pass)连起来的(在图1中用带箭头的虚线表示)形成一个 pass_list。
中间表示( intermediate representation, IR) 指编译器对于源程序进行扫描后生成的内部表示,代表源程序的语义和语法结构,编译器的各个阶段都在IR上进行分析或优化变换,因而它对编译器的整体结构、效率和健壮性都有着极大的影响[38]。中间表示对提高编译器的可移植性以及代码生成起到关键作用,在编译器的研究中,应该设计一种结构良好的中间表示,这种中间表示应在适当的抽象层次上,向上能支持多语言的映射,向下能适应多平台转换且易于进行各种优化。
对于现代编译器来说,编译器前端和后端分别指分析输入源语言和生成目标平台汇编代码的两编译阶段,大部分现代编译器在前端和后端之间会有个中间表示层。有了一个设计良好的中间表示,有m种前端编程语言(C\C++、Fortran、Ada、Java等)和n种后端平台(x86、MIPS、Sparc、ARM等)的编译器设计,这样就减少了为了设计m中语言和n个平台从而设计m*n个编译器设计。
中间语言的使用可以大大减低开发各个语言和各个平台所需编译器的工作量。支持多种编程语言和多种后端平台的编译器可以通过使用一种中间表示来实现。总的来说,中间表示层在减低编译器开发方面的开销和复杂度上功不可没。有些中间表示语言的设计是专门为了支持一种特定的语言,例如Jvm的设计仅仅是为了支持java语言,而大多数中间表示语言的设计是用来将不同的前端语言和后端平台连接起来。
GCC编译器有三种中间表示语言,一种AST/GENERIC比较完善的表示了前端语言的信息,一种GIMPLE用来在相对比较高的层次来表示源语言程序。而另外一种rtl用来高度抽象的表示从特定平台抽象出来的机器指令。
一、GENERIC形式与前端的编程语言是相关的,每种前端语言词法语法分析后形成的AST/GENERIC是异构的,GENERIC包含了前端语言所有的信息。
GCC编译器的前端将高级语言源码经过词法分析、语法分析生成GENERIC。GENERIC是一棵抽象语法树(Abstract Syntax Tree,AST),用数据结构中的树(tree)结构表示。GCC编译器的抽象语法树是源程序的一种中间表示形式,比较直观的表示出源程序的语法结构,并含有源程序结构显示所需要的全部静态信息。GCC 格式的 AST 文件是 GCC 编译源程序时产生的,以文本方式记录源程序抽象语法树的文件。
C语言源码和对应的GENERIC表示如下所示:
C语言源码:
int sum(int a,int b)
{
int c;
c=a+b;
return c;
}
GENERIC表示(部分):
GCC的前端将源语言解析生成GENERIC。不同的前端高级编程语言,GCC前端生成的GENERIC形式不尽相同。每种前端语言词法语法分析后形成的AST/GENERIC是异构的,需要转换成一种统一的中间形式进行后续的处理,这种统一的中间表示形式就是GIMPLE形式。
(关于 GIMPLE 命名是从编译器 SIMPLE IL 改造合成而来)
二、中间表示GIMPLE:GIMPLE是一种三地址码的中间表示形式。
GIMPLE中间形式由GENERIC表达式变换而来,与GENERIC相比,主要有如下的转换:
(1)、通过引入临时变量保存中间结果,并将GENERIC表达式拆分成不超过三个操作数的元组(tuples)。
(2)、GENERIC中的控制结构,例如if-else,for,while等也被转换成条件转移。
(3)、词法作用域(lexical scopes)被取消了。
(4)、异常区域(lexical scopes)被转换成一个单独的异常区域树(exception region tree)。
GIMPLE是通过简化GENERIC得来的,这样做的好处通过下面的例子比较来说明。
xxx@localhost$ cat compare_generic_to_gimple.c
int func(int j, int k)
{
int i;
for (i=0;i>=10;i++)
{
j = i + 1;
k = j + i + 5;
}
return k;
}
int func(int j, int k)
{
int i;
for (i=0;i>=10;i++)
{
j = i + 1;
k = j + i + 5;
}
return k;
}
使用如下命令进行编译,其中-fdump-tree-gimple选项打印出GCC处理的中间过程,包括列出GENERIC的中间形式和GIMPLE中间形式。
xxx@localhost$ paag-linux-gcc -fdump-tree-gimple -S -o compare_generic_to_gimple.s compare_generic_to_gimple.c
xxx@localhost$ paag-linux-gcc -fdump-tree-gimple -S -o compare_generic_to_gimple.s compare_generic_to_gimple.c
下表分别给出其GENERIC形式和GIMPLE表示,并进行对比分析。
从上图可以看出,GIMPLE相比较GENERIC而言的优势,也就是为什么要进行GENERIC形式到GIMPLE形式的转化,主要有以下几个方面的原因:
(1) GENERIC是树结构表示和存储的,对于分析和处理非常不便。
(2) GENERIC形式与前端的编程语言是相关的,而GIMPLE是与前端编程语言无关的,所有的语言并不能够表示成统一的GNERIC,这就使得GENERIC并不适合做优化
(3) GIMPLE形式从本质上讲就是线性的代码序列,所有的计算表达式都表示成一系列的基本操作。这样可以更方便有效地进行后续的编译优化,优化算法很容易在GIMPLE形式上实现。在GIMPLE的表示中,控制结构都分解为直接或间接跳转语句。
(4) GIMPLE表达式的表现形式更加严谨,除了函数表达式以外,每个表达式的操作数不超过3个。
(5) GIMPLE表达式将GENERIC分解为3地址格式的表达式,用临时变量来存储计算中的中间值。同样在GIMPLE的表示中,GIMPLE节点并不表示其节点的值,类似CON_EXPR或是BIND_EXPR这样的节点如果有值,那么GIMPLE格式的表示会将其值存储在临时变量中。
GIMPLE形式的中间表示由于其与前端无关的特性,将多种前端高级编程语言统一到一种中间表示上来,并且在GIMPLE层上做了部分优化。随后GCC将GIMPLE转换成RTL形式的中间表示。
三、中间表示RTL:RTL叫做“寄存器转移语言”(Register Transfer Language),它是以一种虚拟寄存器(pseudo register)的方式来叙述计算机行为的语言。RTL 是一种接近机器指令的语言,既有指令序列组成的内部形式,又有机器描述和调试信息组成的文本形式。
RTL的表示形式:RTL的产生受到了LISP表的启发,要描述的输出指令基本上都是以字母表顺序一个接一个地描述指令完成的工作。它既有内部(内存)形式,是由指向其他结构的指针组成;也有文本形式,用于机器描述和调试输出打印。
RTL的对象类型:表达式,整数,宽整数,字符串和向量。其中表达式是最重要的一类对象。一个RTL表达式类似于一个c的结构体,通常用指针来引用它,指针类型定义为RTX,每个RTX都具有自己的内部数据结构与外部语法。
RTL在一个指令序列(INSN)中如下图 所示。
在GCC中,函数代码的RTL表示被称为INSNs,所有的INSNs被一个双向链表所连接(注意,不是循环双向链表)。有些INSN表示实际的指令,有些代表switch语句的跳转表,有些表示程序跳转所对应的标号,还有一些可以表示各种不同的声明信息。INSN的格式为:
(insn 第0个操作数 第1个操作数 第2个操作数 第3个操作数 第4个操作数 第5个操作数 第6个操作数 第7操作数)
经过上面对 GCC 编译器的整体结构分析可以总结得出 GCC 编译器中主要包含三种中间语言表示形式,分别为 GENERIC层,GIMPLE层(包含高级 GIMPLE 和低级 GIMPLE 以及 SSA)和 RTL表示。这三种中间语言都是与前端语言无关的,其中 GENERIC 树是将前端语言直接翻译过来后形成的中间树,而GIMPLE 则是简化了的 GENERIC 树的集合,在将 GENERIC 转化为 GIMPLE 的过程中将 GENERIC 树中比较复杂的语句都转化为了多个比较简单的语句,其中的计算结果用临时变量来保存RTL是通过低级GIMPLE转化而来的,将低级GIMPLE 树转化为 RTL 树的主要目的是进行优化。
参考:
若想了解 gcc 编译器细节可参考官方文档:
GCC online documentation- GNU Project
Top (GNU Compiler Collection (GCC) Internals)
Top (The GNU C Preprocessor Internals)
https://Blog.csdn.net/u012491514/article/details/24736041