分类

  • 软件天地

  • GCC 编译流程及中间 RTL 的探索 ‖
      编译器的工作是将源代
    码或者机器语言),在现代
    码(通常使用高级语言编写)翻译
    编译器的实现中,这个工作一般
    成目标代码(通常是低级的目标代
    是分为两个阶段来实现的:

      第一阶段,编译器的前
    的某种中间表示方式。
    端接受输入的源代码,经过词法

    、语法和语义分析等等得到源程序


      第二阶段,编译器的后
    标机器上可运行的代码。
    端将前端处理生成的中间表示方

    式进行一些优化,并最终生成在目


      GCC(GNU Compiler Collection) 是
    ,它能够支持多种语言前端,包括 C, C+
    treelang 等。
    在 UNIX 以及类 UNIX 平台上广泛使用的编译器集合
    +, Objective-C, Ada, Fortran, Java 和


      GCC 设计中有两个重要的目标,其中
    码能够最大程度的被复用,所以 GCC 必
    质量的可执行代码,这就需要对代码进行
    一种硬件平台无关的语言,它能对实际的
    RTL(Register Transfer Language)。
    一个是在构建支持不同硬件平台的编译器时,它的代
    须要做到一定程度的硬件无关性;另一个是要生成高
    集中的优化。为了实现这两个目标,GCC 内部褂昧?br>体系结构做一种抽象,这个中间语言就是


      虽然关于 GCC 的研究
    目标是在 GCC 的编译过程
    和开发工作侧重于 GCC 后端代
    中前端是如何工作的。
    码优化方面,但本文中我们关注的


      把 GCC 的前端独立出来研究目的在
    何设计新编译器的前端,而将代码优化和
    设计的重复性劳动。
    于,在设计新的编译器的时候,我们仅仅需要关注如
    目标代码的生成留给 GCC 后端去完成,避免了后端


      本文将以 C 语言为例
    行处理并得到一个中间表示
    下作者尝试在 gcc 内部的R
    有兴趣研究和开发 gcc 的
    ,介绍 gcc[2] 在接受一个 .c
    并转交给蠖舜怼H缓螅诹?br>TL表示层中 hack gcc 的过程,
    读者有所帮助。
    文件的输入之后,其前端是如何进
    解了 gcc 的工作流程后,介绍一
    与大家分享一些经验,希望能给对


      2. gcc 的工作流程

      gcc 是一个驱动程序,
    步动作,gcc 提供了多种选
    查找这些编译选项的详细信
    它接受并解释命令行参数,根据
    项以达到控制 gcc 编译过程的
    息。
    对命令行参数分析的结果决定下一
    目的,我们可以在 GCC 的手册中


      gcc 的使用是比较简单
    对庞大的[3] gcc,我们只
    的详尽文档[4] ,这主要是
    只有通过其它途径来了解 g
    以跟踪过去看一看,阅读代
    们的阅读变得更简单一些,
    用调试器来跟踪 gcc 的编
    兴趣的细节部分。我们先从
    之间的调用关系,然后在 h
    且可以通过调试来发现和修
    的,但是要深入到其内部去了解
    能选择感兴趣的部分来分析。但
    由于 gcc 本身过于繁杂,而且
    cc。有两个比较好的方法:一是
    码看起来可怕,但其实代码中会
    这种方法便于从整体上把握 gcc
    译过程,这样可以看清 gcc 编
    大处着眼,从 source 中看看 g
    ack gcc 的时候,对 gcc 进行
    改 patch 中的错误。
    编译流程,情况就比较复杂了。面
    我们无法获得关于 gcc 编译流程
    它处于不断的变化当中,所以我们
    阅读 source,对感兴趣的函数可
    有很多注释说明它的功能,使得我
    ;另外一个是 debug gcc,就是使
    译的实际流程,也可以追踪我们感
    cc 一些比较重要的函数以及它们
    debug 来追踪我们关心的细节,并


      在开始阅读 gcc 的代码之前,推荐
    of the compiler 一章——如果您以前没
    一个大概的映像。
    您阅读一下 GCC internals 中 passes and files
    有看过的话,这段内容会帮助您对 gcc 的结构建立


      好了,我们以 gcc 中的函数为单位
    调用关系。在 gcc 源码目录中,很容易
    这个main.c 文件中只有一个函数 main,
    toplev_main 函数。之所以单独用一个 m
    前端可以方便设计不同的 main 函数。
    ,希望能够尽量详细地描述 gcc 中自顶向下的函数
    就发现了一个文件 main.c,应该是 gcc 的入口了,
    而这个 main 函数中也只有一条语句,调用了一下
    ain 函数来调用 toplev_main,是为了让不同的语言


      toplev_main 函数是在
    来控制 gcc 最顶层的编译
    打开文件、以合适的顺序调
    toplev_main 首先对 gcc
    始解析命令行参数,我们对
    函数看从名字看就是做编译
    toplev.c 文件中定义的,从名
    流程的,在程序开始的注释中也
    用各个分析程序 [5] 并记录它
    做了一下初始化,主要是设置环
    这些并不感兴趣,重要的是接下
    工作的,而在此之后 toplev_ma
    字中就可以看出这个文件应该是用
    说明了它是用来处理命令行参数、
    们各自所用的处理时间。
    境变量和诊断信息等等,然后就开
    来调用了 do_compile 函数,这个
    in 函数就返回了。

      do_compile 函数也是
    比如对编译过程中计时器的
    ,同时它还对 toplev_main
    ,调用了 compile_file()
    在 tolev.c 中定义的,它调用
    初始化、针对特定程序设计语言
    函数中解析的命令行参数做了
    函数,这个函数应该是用来进行
    了一些函数来做进一步的初始化,
    的初始化以及对后端的初始化等等
    进一步处理。在完成了上述工作后
    真正的编译工作了。

      compile_file 函数还是在 toplev.c
    的do_compile 函数,它们是参数和返回
    数包括编译的文件名、编译参数以及 gcc
    表示的,当然,这些全局变量在前面各种
    compile_file 函数,它又做了一些我们
    钩子函数来分析(parse)整个输入文件了:
    中定义的,这里提一下 compile_file 函数和上面
    类型都为 void 的函数,在编译的时候需要的各种参
    内部使用的一些钩子函数等等都是采用全局变量来
    初始化函数中都已经被适当地初始化了。接着说
    并不太关心的初始化工作,之后,它终于调用了一个


      (*lang_hooks.parse_file)(set_yydebug);  

                                                                                   
      这里的 lang_hooks 是
    特有的分析程序,关于 lan
    、langhooks.c 和 langhoo
    条语句相当于调用了 c-opt
    一个全局变量,不同语言的前端
    g_hooks 结构的定义和初始化等
    ks-def.h 等文件,这里就不详
    s.c 中的 c_common_parse_file
    对此赋以不同的值,以便调用各自
    等可以参见源码中的 langhooks.h
    细追究了。对于 C 语言来说,这
    函数。

      c_common_parse_file
    同样位于c-parse.c中的yyp
    [6] 从c-parse.y中得到的
    BNF(Backus Naur Form)来
    中调用了c-parse.c中的c_parse
    arse函数。有必要介绍一下c-pa
    一个语法解析器。c-parse.y则
    描述了某种程序设计语言的语法
    _file函数,在此函数中又调用了
    rse.c文件,它是由GNU bison
    是一个YACC文件,它使用
    。 [7]

      至此,我们对gcc中主要的函数调用
    c-parse.c中的yyparse函数。前面提到过
    文件作用后自动生成的,这导致这段代码
    中有很多条goto语句以及超过500个case
    gcc的函数调用带来了极大的困难,我们
    关系还是相当清楚的,从main函数层层深入,进入了
    c-parse.c文件是由GNU bison对c-parse.y这个YACC
    阅读起来比较困难,因为bison生成的c-parse.c文件
    的switch语句,如此多的选择和跳转语句无疑给追踪
    不可能再继续下去了。

      再回过头去看看前面那
    rest_of_compilation,这
    些代码和注释以及一些文档,注
    似乎是一个很重要的函数,我们
    意到多次提到过一个函数――
    可以过去看看。
                                                                                   

      在toplev.c中我们找到了这个函数,
    或者变量的定义处理以后,接着对这些函
    数返回后,gcc内部使用的tree结构就消
    序对应的汇编代码生成了,并且把对应的
    部分是gcc编译过程中内部使用RTL表示的
    个函数返回之前做的。
    注释中说明它的作用是:在对程序中顶层的函数定义
    数或者变量进行编译并输出相应的汇编代码,在此函
    亡了。看来这个函数的功能比较复杂,它已经把源程
    tree结构占用的空间已经释放了,而我们所感兴趣的
    情况,这部分处理应该是在rest_of_compilation这


      前面我们从main函数跟
    rest_of_compilation函数
    的有关RTL的处理就在其中
    踪到了yyparse函数,这里又发
    ,但中间这段过程gcc做了些什

    现了一个很重要的
    么我们还不清楚,也许我们所关心


      现在我们只有对gcc进
    绍一下调试gcc的方法:
    行调试才能确切的看清进入yypa

    rse后函数调用的情况了,这里介


      对gcc进行调试,其实是对编译gcc源
    运行命令:
    代码所得到的cc1程序调试,进入到cc1所在的目录,



      $ gdb cc1                                                            
      $ break main                                                      
      $ run -dr /PATH/test.c                                  

                                                                                   
      这样就是以-dr为编译
    了一个断点,-dr作为编译
    件中去。接下来在rest_of_
    点,用backtrace命令查看
    参数运行gcc来编译test.c文件
    参数就是要求在RTL表示生成以
    compilation之前再设置一个断
    此时函数栈帧的情况:
    了,并且在main函数的入口处设置
    后将其dump到一个以.rtl结尾的文
    点,并用continue命令运行到该断


      $ break rest_of_compilation                        
      $ continue                                                          
      $ backtrace                                                        

      下表1给出了使用gdb调试时显示出的
    从main到rest_of_compilation的函数调用情况:
      

      
    表1. 部分函数调用栈帧列表


      调试的结果证实我们前面的分析是正
    阅读代码时所分析得到的结果是吻合的。
    rest_of_compilation之间的一系列函数
    中去看看这些函数的功能。
    确的,从main函数到yyparse函数的调用顺序与我们
    现在我们得到了gcc编译时从yypare到
    调用,这些都是值得关注的目标,让我们返回到源码


      时刻记得我们的目标:
    中间表示层RTL生成汇编代
    层做一些修改,以达到我们
    函数的分析,直接跳转到RT
    对于gcc如何生成tree结构我们
    码的,我们感兴趣的是RTL表示
    的目的。为了省去一些篇幅,本
    L生成和处理相关的部分。
    并不关心,也不关心gcc是如何由
    是如何生成的,并希望在RTL表示
    文中略去了对那些我们不太关心的


      终于,在tree-optimiz
    是与RTL生成有关的函数调
    e.c中的tree_rest_of_compilat
    用,特别引起我们注意的又是一
    ion中,我们发现了一系列看起来
    个钩子函数:

      (*lang_hooks.rtl_expand.stmt) (D
    ECL_SAVED_TREE (fndecl));
                                                                                   

      这行代码的注释说这个钩子函数用来
    个函数来进行RTL生成阶段的最后处理(包
    调用了rest_of_compilation了。前面已
    做优化并且生成汇编代码输出,至此我们
    调用了一系列生成RTL表示的函数之后,
    一个原始的、未优化的RTL中间表示。如
    入代码做改动应该是一个不错的选择。
    生成一个被编译函数的RTL表示,接下来还调用了几
    括调用gcc编译时内部使用的垃圾收集函数),然后就
    经提到了,rest_of_compilation的作用是对RTL表示
    可以做出这样的推断:在tree_rest_of_compilation
    到调用rest_of_compilation之前,gcc的内部保存了
    果我们希望对函数的RTL表示做一些修改,在这里插


      到这里,我们所关心的
    的,我们应该有一定的信心
    gcc编译流程基本已经结束了,
    在RTL表示层上对gcc进行hack了
    也搞清了RTL表示在什么地方生成


      3. RTL简介

      我们的目标是在RTL表
    中有专门的一章描述RTL,
    插入RTL语句的时候,这份
    示层上hack gcc,所以有必要对
    如果对RTL没有任何了解,那么
    文档也可以作为比较详尽的手册
    RTL做一些介绍。在gcc internals
    它很值得您一看;同时,在理解和
    来参照。

      在gcc的编译过程中,有三次比较重要的转换:                                  

      待编译的源代码―

      gcc抽象语法树―

      RTL表示-<汇编代码输出。

      RTL是gcc内部使用的中
    来看一看。使用
    间表示语言,为了对其有一个直

    观点的印象,我们可以把它dump出


      $ gcc -dr test.c                                              
                                                                                   

      就可以得到test.c的RTL表示,文件名一般为test.c.00.rtl。  

      RTL的设计据说是从LIS
    个LISP程序,每条RTL语句
    以及上面提到的文档来深入
    来hack cc,必须阅读gcc源
    参考,唯一有帮助的就是已
    验,作者在尝试自己的补丁
    maillist上看到有些hacker
    有裨益的。
    P语言得到了灵感,所以我们dum
    都是用来描述需要输出的指令的
    学习RTL。但我们的要求不仅如
    代码提供的RTL操作的接口,这
    有的在RTL表示层上对gcc做的补
    时曾经参考过StackGuard [8]
    提供的patch,这些已有的工作

    p出来的.rtl文件看起来也像是一
    ,可以对照我们dump出的.rtl文件
    此,我们需要插入自己的RTL语句
    个过程比较繁琐而且没有文档可以
    丁,以吸取其他gcc hackers的经
    的代码,另外可以在gcc的
    对于gcc hacker newbie来说是很

                                                                                   

      仅仅这么多文字来介绍
    另外一篇文章来完成了,本
    RTL还远远不够,但是如果希望
    文就不再详述了。
    把RTL描述得十分清楚,那应该由


      4. Let's hack gcc

      下面进入hack gcc的实战阶段了,先
    的时候,能够在每个函数的开始和结束的
    的第一条指令之前,由编译器强制插入一
    入一个函数调用。下面用两段C语言代码
    说一下我的目的:我希望使用修改过的gcc编译程序
    地方插入一个函数调用语句,也就是说,在每个函数
    个函数调用,在函数最后一条指令结束之后,也要插
    来表达这个补丁的效果:

      int foo()                                                            
      {                                                                            
        first statement;                                          
        …                                                                        
        …                                                                        
        …                                                                        
        last statement;                                            
      }                                                                            
       int foo()                                                          
      {                                                                            
        my_function_begin;                                      
        first statement;                                          
        …                                                                        
        last statement;                                            
        my_function_end;                                          
      }                                                                            
                                                                                   

      左边一列是程序员正常
    到相当于编译右边这段函数
    一条语句之后自动插入两个
    个函数具体实现什么功能可
    一句话表示该函数确实被调
    编写的普通函数,我希望使用修
    的结果,就是对程序员透明地在
    函数调用:my_function_begin
    以由程序员来编写,最简单的实
    用了即可。
    改过的gcc编译该函数后,能够得
    每个函数的第一条语句之前和最后
    和my_function_end。当然,这两
    现可以仅仅在标准输出上分别打印


      gcc中生成抽象语法树
    为单位的,这也就意味着在
    示的函数之后,我们所得到
    RTL表示,这正好方便我们
    表示和RTL表示都是以一个完整
    tree_rest_of_compilation这个
    的只是当前正在被编译的函数的
    以函数为单位来进行修改。
    的函数定义或者top level的声明
    函数调用了一系列用于生成RTL表
    RTL表示,而并不是整个源程序的


      我们在tree_rest_of_compilation函
    调用一个新函数modify_rtl来对gcc生成
    function.c文件中,这是因为gcc在生成R
    ,我们的补丁也可以看作是gcc生成RTL表
    定义是最合适的。
    数中调用rest_of_compilation之前插入一条语句,
    的RTL表示做一些处理。函数modify_rtl的定义放在
    TL表示时需要的相关函数大部分都定义在这个文件中
    示的一部分工作,所以把modify_rtl放到这个文件中


      接下来工作的关键就集
    RTL表示,我们可以对这个R
    my_function_end函数即可
    一个insn [9] ,有的insn
    者其它各种声明信息。为了
    函数列表,并给出它们的功
    中到如何定义modify_rtl函数了
    TL单元进行扫描,找到合适的位
    。函数的RTL表示是一个双向连
    可能表示一条真实的汇编指令,
    简便起见,这里直接给出一个常
    能:
    。现在我们得到了当前编译函数的
    置分别调用my_function_begin和
    接的链表结构,其中每个节点称为
    有的则表示jump指令跳转的标签或
    用的gcc所提供的访问insn的宏和


      

      
    表2. 部分gcc提供的insn操作接口列表


      一个函数完整的、未被优化的RTL表
    定义了两个全局变量NOTE_INSN_FUNCTION
    note insn的行数。这样我们就可以扫描
    以插入相应的函数调用语句了。
    示中会有两个note insn表示函数的开始和结束,gcc
    _BEGIN和NOTE_INSN_FUNCTION_END来表示这两个
    当前RTL单元,当碰到这两个note insn的时候,就可


      gcc提供了emit_librar
    调用的RTL表达式,并默认
    如果直接调用emit_library
    而不是我们所希望的函数开
    数,它们产生一个相对独立
    y_call函数来插入一个函数调用
    地把这个RTL表达式插入到当前R
    _call,就会把函数调用语句插
    始和结束的地方,我们可以使用
    的sequence并把函数调用语句保
    ,这个函数返回的是一个表示函数
    TL单元的最后一个insn之后。所以
    入到RTL单元最后一个insn之后,
    start_sequence和end_sequence函
    存到一个RTL表达式中以备后用。

      我们已经找到插入函数
    用gcc提供的emit_insn_bef
    调用的点,并且也生成了表示函
    ore和emit_insn_after函数来插
    数调用的RTL语句,现在就可以使
    入RTL语句了。

      到这里,modify_rtl函数的实现基本
    数的开始处插入RTL语句的功能:
    已经成型了,下面这段示例代码就可以完成在每个函



      int modify_rtl()                                              
      {                                                                            
       rtx insn;                                                          
       rtx seq;                                                            

       //emit my_function_
    begin at the beginnig of eac
    h function
       start_sequence();                                          
       emit_libarary_call(gen_rtx(SYMB
    OL_REF, Pmode, my_function_begin),
       0, VOIDmode, 0);                            
       seq = get_insns();                                        
       end_sequence();                                              

       for(insn = get_insns(); ; insn
    = NEXT_INSN(insn))
       if((GET_CODE(insn) == NOTE)                    
       && (NOTE_LINE_NUMBER(insn) ==              
       NOTE_INSN_FUNCTION_BEGIN))                  
       break;                                                              

       emit_insn_after(seq, insn);                      

       …                                                                          
      }                                                                            

                                                                                   

      这段代码中所使用数据
    描述清楚,请读者参考gcc
    结构、函数的具体功能和用法,
    源代码。
    属于十分细节的内容,无须在这里


      对于在函数结束的地方插入my_funct
    到RTL单元的最后一个insn,然后使用PRE
    NOTE_INSN_FUNCTION_END的note insn时
    插入到这个insn之前即可。
    ion_end函数同样如此,我们可以用get_last_insn得
    V_INSN(insn)开始向前扫描,遇到行号为
    ,用emit_insn_before把相应的函数调用RTL表达式


      现在这个patch的基本功能已经完成
    用一些,比如加入一个编译选项(比如-fi
    译的命令行参数中没有提供这个编译选项
    选项,我们可以参考opts.c中的decode-o
    了,我们还可以再做一些工作使得它功能更强大和实
    nsert-function)来指定是否启用这个patch的,当编
    时,我们所作的补丁就不起作用。关于如何增加编译
    ptions函数,在此就不详细分析了。

      在modify_rtl中调用current_functi
    们可以把这些函数名写到一个文件中去,
    一个过滤器,在启用了patch的情况下,
    处理,这些功能也是很容易实现的。
    on_name函数可以得到当前正在被编译的函数名,我
    这样可以记录我们对哪些函数做了修改;还可以实现
    对于指定的函数,我们还可以将其过滤掉,不对其做


      我们还可以再实现一些功能,比如在
    条call指令所调用的函数名记录下来,这
    调用关系图,这就可以描绘程序的实际运
    扫描RTL的时候,如果发现一条call_insn,可以把这
    样我们甚至可以得到一个程序运行时刻的动态的函数
    行轨迹。

      最后,还需要把my_function_begin
    的功能扩展一下,不是仅仅输出一条语句
    以得到一个以函数为粒度的运行时刻日志
    一些特殊的检查工作等等,这样就使得我
    mylib.c中实现,编译成一鰏hared obje
    和my_function_end两个函数实现一下,可以把它们
    到标准输出,而是记录一些信息到文件中,这样就可
    ,甚至可以使这两个函数与linux内核联系起来,做
    们的patch有一些实用性了。这两个函数我们可以在
    ct,使用如下命令编译:


      $ gcc mylib.c -c -fPIC                                  
      $ gcc mylib.o -shared -o libmylib.so      

                                                                                   

      把libmylib.so放到/us
    用这个shared object中的
    r/lib目录下,那么在编译的时
    函数了。
    候只需加上-lmylib参数就可以使


      剩下的工作就是进行调
    完美的运行起来的时候,也
    试和测试了,当我们解决了各种
    许我们就能体会到gcc hacker的
    问题,使这个修改过的编译器能够
    那种成就感和喜悦之情了。

      5. 经验总结

      先说一下我自己尝试的结果,我是基
    选项以选择是否启用添加的补丁,可以在
    在函数调用之前和返回之后插入函数调用
    理,并且可以在运行时将一些信息记录到
    了,实现方法可能和本文中的方法有所不
    在则进行了一些改动,这里就不详加介绍
    lynx等实用软件,运行正常,补丁功能也
    空间可以上载我的补丁,有兴趣的读者可
    于gcc version 3.4.0工作的,给gcc加入了一个编译
    每个函数的开始和结束的时候插入函数调用,也可以
    ,实现了一个过滤器,可以忽略一些函数不对其做处
    文件中去留待分析。这个补丁的功能基本上就是这些
    同,文中描述的方法是较早的时候我采用的方法,现
    了。我已经成功的使用“我的”gcc编译了emacs和
    正常,可以说是取得了一个小小的成功。但是我没有
    以通过e-mail向我索取。

      最后谈谈我的经验:                                                            

      在理解gcc的编译流程以及试图找到
    有的工作是怎么做的。不要贸然尝试,不
    方法,在确立了一个基本思路之后,可以
    好的思路,在确信自己思路的可行性之后
    做补丁的思路的时候,需要多阅读文档,包括学习已
    要奢望可以凭运气达成目的,尽量找到最合适的实现
    在gcc的maillist上咨询一下,看看有没有人提供更
    再开始具体的工作。

      在做具体实现的时候,肯定会遇到各
    错,或者用patch过的gcc编译程序时出错
    心地检查代码和进行debug,尽量自己解
    讨论。我记得在maillist上曾经有人严厉
    a question each time you get an erro
    实在不明白的问题必须拿到maillist上去
    才能够得到有效的帮助。
    种各样的问题,比如在编译自己修改过的gcc时会出
    ,或者是编译通过运行时刻出错等等,这时候需要耐
    决问题,不要把一些特别细节地问题拿到maillist上
    地告诫我:“you won't go very far if you ask
    r”,自己debug才是解决问题的最好方法,当然如果
    讨论,这时候要尽量详细的描述自己的目的和问题,


      <全文完>



    上一页 下一页




    map