分类
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的函数调用情况: |
![]() |
| |
| 调试的结果证实我们前面的分析是正 阅读代码时所分析得到的结果是吻合的。 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的宏和 |
![]() |
| |
| 一个函数完整的、未被优化的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才是解决问题的最好方法,当然如果 讨论,这时候要尽量详细的描述自己的目的和问题, |
| <全文完> |

