GCC源码分析(摘)

摘自https://blog.csdn.net/sonicling/article/details/6702031https://blog.csdn.net/sonicling/article/details/670615https://blog.csdn.net/sonicling/article/details/7915301https://blog.csdn.net/sonicling/article/details/7916931https://blog.csdn.net/sonicling/article/details/8246231,感谢分享。

gcc-4.5.2为例。

GCC是一个编译驱动器,驱动cc1、as和ld三个部件完成编译、汇编和连接的工作。cc1将C语言源文件编译为汇编文件(.s)。而将汇编代码转换为二进制指令的工作由AS完成,生成大家都很熟悉的对象文件(.o);生成的这些对象文件再由AR程序打包成静态库(.a),或者由LD程序连接成可执行程序(elf、.so或其他格式)。而LD就是所谓的连接器。AS、AR、LD是属于另外一个叫做binutils的软件包的程序,所以要让GCC能够有效运作起来,除了在系统中安装GCC外,还要安装binutils才行。

以下是cc1、as、ld各司其责的配合完成一个编译过程。

gcc test.c -S -o test.S  
as test.S -o test.o  
ld test.o -o test  

通常所用的“gcc -c”就相当于“gcc -S” + as,而对于编译单个源文件一步到位生成可执行“gcc test.c -o test”相当于上面三个步骤的组合,中间文件被放置在临时目录下。

从这一篇开始,我们将从源代码的角度来分析GCC如何完成对C语言源文件的处理。GCC的内部构架在GCC Internals(搜“gccint.pdf”,GNU Compiler Collection Internals)里已经讲述得很详细了,但是如果你只看了gccint就来看代码,还是觉得一头雾水,无法下手,因为你很难把gccint所讲的概念同gcc代码里真实的数据结构联系起来。

GCC的源代码文件非常多,总数大约有好几万。但是很多都是testsuite和lib。首先我们除去所有的testsuite目录,然后lib打头的目录也可以基本上不看,那是各程序语言的gcc版标准库和专为某种语言的编译而设计的库。我们只分析C语言的话,只用看其中的libcpp,它包含了C/C++的词法分析和预处理。剩下的GCC源代码大多集中在config、gcc两个目录下。

config目录是Makefile为各跨平台编译准备的配置目录。

gcc目录下除去gcc/config目录外的其他文件是各个语言的编译器前端源文件,一般放在各自语言命名的目录下,例如cp(C++)、java、fortran等。唯一例外的是C语言,它的前端源文件同GCC的通用文件(包括中间表示、中间优化等)一起,散放在gcc目录下。gcc/config目录是gcc在各种硬件或操作系统平台下的后端源文件,负责把GCC生成的中间表示转换为各平台相关的机器码、字节码或其他目标语言。那我们可以从gcc的源代码组织上大致看出gcc之所以能支持众多前端和后端的原因,它将各种语言的源文件按照各自的方法分析完之后,表示为由GENERIC、GIMPLE、RTL组成的统一的中间结构,再由各种后端将统一的结构转换为各自平台对应的目标语言。

词法分析,通俗讲,就是给源文件断词。我们将源文件看作一个字符流,并交由词法分析器进行断词,词法分析器必须能够输出一个一个的词,也叫做记号(token),每个记号至少有三个属性:

1.值:即断出的那一段字符串

2.类型:关键字、标识符、文字常量、符号等

3.位置:这个记号在当前文件的第几行,用于报错。

  在《编译原理》里面,词法分析是和NFA、DFA、正则表达式联系起来的,他们属于III型语言。根据词法定义,我们手头已经有很多工具可以实现词法分析器的自动构造,这些自动构造的代码无一例外的使用了DFA的概念,即构造出来的词法分析器一定是一个DFA,里面包含了初始状态、终结状态和状态的转移,而这些状态都是自动构造中抽象出来的符号或者数字,一般人很难看出这些状态在词法定义中的位置。所以这也是自动构造的缺点——贪图构造的方便,一定带来修改的成本。

  而GCC的词法分析是手工构造的,实现在libcpp/lex.c文件中,其中最重要的那个函数是_cpp_lex_direct,他反应了GCC词法分析器的核心结构。

C语言的语法分析器实现在gcc/c-parser.c中。该文件的起始函数是实现在文件末尾的void c_parse_file(void)。它调用了c_parser_translation_unit(),然后按照文法定义一直递归调用下去。因此这是一个典型的递归下降分析法。C语言的语法分析从c_parser_translation_unit开始,往下调用c_parser_declaration_or_fndef,这是一个关键函数。

在c_parser_declaration_or_fndef函数里有两个分支,一个处理非函数声明,最后总是调用到了finish_decl函数,而另一个分支用来处理函数声明,最后总是调用到了finish_function函数,这两个函数都实现在c-decl.c文件中。这两个函数开启了接下来的工作:中间层翻译。

在语法分析过程中,所有识别出来的语言部件都用一个叫TREE的变量保存着。这个TREE就是gcc语法树,叫做GENERIC。实际上它也是gcc的符号表,因为变量名、类型等等这些信息都由TREE关联起来。GENERIC的节点都定义在gcc/tree.h头文件里。

每个函数翻译为GENERIC的语法树之后,会进行gimplification(gimple化),在这一过程中函数的语法树被翻译为了控制流图的形式。每个函数对应一个控制流图。控制流由基本块(Basic Block)组成。每个基本块具有一串指令序列,并且只能有一个入口和一个出口,因此在这个序列内部不允许存在跳转。gcc对基本块的操作主要定义在gcc/basic-block.h里。basic block在控制流中以链表的形式存放,它们由edge组成逻辑意义上的图。gcc提供了对每个基本块相关的边进行遍历的宏FOR_EACH_EDGE。每个edge有flags标志位,用来判别边的类型,它决定了跳转的方式(条件、无条件等等)。

gimple和RTL是gcc用来表示指令的两种形式。因此每个基本块都包含有两组指令序列,一组是gimple指令,一组是RTL指令。每个函数将首先被gimple化,此时基本块里只包含gimple指令,之后由gimple生成RTL。

  gimple是一种包含最多三个操作数的中间指令,也就是编译原理里讲的四元码(三个操作数,一个操作符),基本上也就是 dst = src1 @ src2 的这种形式。由于gimple最多只能对两个操作数进行计算,因此一个复杂的表达式会展开为一系列的gimple指令,这一过程就是gimple化。gimple化的代码实现在gcc/gimplify.c中,核心的思想就是对语法树进行后序遍历,对每个非叶子节点生成一条gimple指令,自动生成必要的中间变量,并正确识别出基本块,从而生成完整的控制流。

  从源码来看,语法分析中,每分析完一个函数,就会调用finish_function,它又会调用cgraph_finalize_function将函数添加到cgraph里,只有这个函数被调用才会继续处理它。分析整个文件后,compile_file()函数会调用一个hook:lang_hooks.decls.final_write_globals ()。这个hook实际上是write_global_declarations() (in gcc/langhooks.c),它会调用注释中提到的 cgraph_finalize_compilation_unit() 函数,接下来就是这样的调用关系:

write_global_declarations()
    cgraph_finalize_compilation_unit()
        cgraph_analyze_function()
            gimplify_function_tree() -> gimplification。
            cgraph_lower_function() -> lowering
        cgraph_optimize() -> 优化
  在所有针对gimple的优化完成后,有一个叫做pass_expand的步骤,它将gimple展开为RTL。RTL是一种相对底层的指令,如果说gimple的重点在于控制流和数据流这种逻辑结构的话,那么RTL的重点就在数据和控制的精确描述。通过RTL可以将操作数的长度、对齐、操作的类型、副作用等信息表述出来,从而有利于自动化地进行最后的指令生成。

  RTL的指令在gcc中称之为insn,insn是有语法和语义的,它被gcc的生成工具所识别和处理,并生成对应的.c文件作为gcc的一部分一同编译到gcc的执行文件中。

GENERIC、GIMPLE和RTL三者构成了gcc中间语言的全部,它们以GIMPLE为核心,由GENERIC承上,由RTL启下,在源文件和目标指令之间的鸿沟之上构建了一个三层的过渡。接下来,gcc的工作就是对中间语言进行平台无关优化。

gcc 对中间语言的每一步处理叫做一个pass。从一个函数的GENERIC树刚被转换为GIMPLE之后,接下来的工作就由一连串的pass来完成。这些pass环环相扣,最终完成整个程序的优化工作,为目标代码生成做最后的准备。

GCC的pass结构定义在gcc/tree-pass.h头文件中。

大部分常用的pass都实现在gcc目录下的某些文件中,这些文件的特点是声明了一个全局的xxx_pass结构体变量,而这些变量在tree-pass.h中用extern声明一遍,并在passes.c中的 init_optimizations() 函数中串在一起。该函数通过使用NEXT_PASS()宏,初始化了5串pass:all_lowering_passes -> all_small_ipa_passes -> all_regular_ipa_passes -> all_lto_gen_passes -> all_passes。

三大类Pass:GIMPLE Pass、RTL Pass、IPA Pass。

Pass被Pass管理器执行。执行每一个pass的代码实现在gcc/passes.c里。在gcc/tree-optimize.c中定义了几个特殊的pass:pass_all_optimizations、pass_early_local_passes、pass_all_early_optimizations、pass_cleanup_cfg、pass_cleanup_cfg_post_optimizing、pass_fixup_cfg、pass_init_datastructures。

调用关系:

    cgraph_finalize_compilation_unit()

        cgraph_analyze_functions()
            cgraph_analyze_function()
                gimplify_function_tree() -> gimplification。
                cgraph_lower_function() -> lowering
            cgraph_optimize()
                ipa_passes()
                   if (!in_lto_p) execute_ipa_pass_list (all_small_ipa_passes); -> small IPA execute
                   if (!in_lto_p) execute_ipa_summary_passes(all_regular_ipa_passes) -> regular IPA summary
                   execute_ipa_summary_passes (all_lto_gen_passes); -> lto summary
                   if (!flag_ltrans) execute_ipa_pass_list (all_regular_ipa_passes); -> regular IPA (include LTO) execute
                cgraph_expand_all_functions()
                   cgraph_expand_all_function()
                      tree_rest_of_compilation()
                        execute_all_ipa_transforms() -> regular IPA transform (include LTO) transform
                        execute_pass_list (all_passes) -> 进程内优化

普通的pass由gcc/passes.c中的execute_one_pass()函数来负责调用。

执行完所有Pass之后,gcc就进入了最后的阶段:目标代码生成。

GCC的pass格局,它是GCC中间语言部分的核心架构,也是贯穿整个编译流程的核心。在完成优化处理之后,GCC必须做的最后一步就是生成最后的编译结果,通常情况下就是汇编文件(文本或者二进制并不重要)。

GCC中间语言的核心数据结构是GENERIC、GIMPLE和RTL。其中的RTL就是和指令紧密相关的一种结构,它是指令生成的起点。

RTL叫做寄存器转移语言(Register Transfering Language)。说是寄存器,其实也包含内存操作。RTL被设计成一种函数式语言,由表达式和对象构成。其中对象指的是寄存器、内存和值(常数或者表达式的值),表达式就是对对象和子表达式的操作。这些在gcc internal里面都有介绍。

  RTL对象和操作组成RTL表达式,子表达式加上操作组成复合RTL表达式。当一个RTL表达式表示一条中间语言指令时,这个RTL表达式叫做INSN。RTL表达式(RTL Expression)在gcc代码中缩写为RTX,代码中的rtx类型就是指向RTL表达式的指针。所以insn就是rtx,但是rtx不一定是insn。

RTL是由gimple生成的,从gimple到RTL的转换叫做“expand”。在整个优化的pass链中,这一步由pass_expand完成。该pass实现在gcc/cfgexpand.c中。

针对每个CPU平台,gcc有对应的Machine Description用指导指令生成。这些代码放在gcc/config/<平台名称>的目录下,比如intel平台的在gcc/config/i386/。一个Machine Description文件是对应平台的核心,比如gcc/config/i386/i386.md文件。

在优化的pass序列的最后,有一个叫做pass_final的RTL Pass,这个pass负责将RTL翻译为ASM。



猜你喜欢

转载自blog.csdn.net/eidolon_foot/article/details/80198824
今日推荐