GNU 编译器家族 GCC 内部探密
内容:
GNU 编译器家族 GCC 介绍
关于代码分析
Treelang 的代码框架
对用户源文件进行语法分析
语法分析的细节
GCC 前端的全景图
小结
参考资料
关于作者
在 Linux 专区还有:
教程
工具与产品
代码与组件
项目
文章
探索 GCC 前端的内部结构
赵蔚 (
[email protected])
Linux 和自由软件技术独立顾问
2003 年 7 月
我们在本文中说明 GCC 源码包中的例子编程语言 Treelang 的实现细节。主要目的在于辑此说明所谓 GCC 前端的编程方法。限于篇幅,本文只能略略讲一下 GCC 前端的内部结构的框架部分。本文中所涉及到的源程序均位于 GCC 源码包中的 gcc/ 目录和 gcc/treelang/ 目录下。本文的代码分析基于 GCC CVS 中的最新(2003 年六月)的开发版本。
GNU 编译器家族 GCC 介绍
作为自由软件的旗舰项目,Richard Stallman 在十多年前刚开始写作 GCC 的时候,还只是把它当作仅仅一个 C 程序语言的编译器;GCC 的意思也只是 GNU C Compiler 而已。经过了这么多年的发展,GCC 已经不仅仅能支持 C 语言;它现在还支持 Ada 语言,C++ 语言,Java 语言,Objective C 语言,Pascal 语言,COBOL 语言,以及支持函数式编程和逻辑编程的 Mercury 语言,等等。而 GCC 也不再单只是 GNU C 语言编译器的意思了,而是变成了 GNU Compiler Collection 也即是 GNU 编译器家族的意思了。
另一方面,说到 GCC 对于各种硬件平台的支持,概括起来就是一句话:无所不在。几乎所有有点实际用途的硬件平台,甚至包括有些不那么有实际用途的硬件平台,比如 Don Knuth 设计的 MMIX 计算机,GCC 都提供了完善的支持。
我们在这篇文章中要弄清楚的就是 GCC 是如何做到能够支持这么多种程序语言的。所谓的 GCC 的程序语言前端到底是怎么回事。如果我们要设计实现自己的编程语言的话,应该从何入手。回答这些问题的第一步,就是分析清楚 GCC 源码包中,为了说明 GCC 的程序语言前端的编写方法,而写作的 Treelang 编程语言在 GCC 中的实现细节。
如果把我们自己的程序语言的实现建立于 GCC 之上,也立刻使得我们的程序语言的实现版本可以运行在几乎所有有用的硬件平台之上。这对于程序语言的作者来说,也是一个确实的有极大诱惑力的好处。
关于代码分析
在这一小节里面我们着重说明两个问题:第一、为什么要阅读源代码;第二、代码分析应该怎么写。
阅读源代码对提高自己的编程水平是非常有帮助的。这个帮助至少体现在两个方面。第一个方面是学会大型软件项目设计的模式。这样的模式是真实可靠的第一手资料,这样学来的模式要比从书本上,用日常语言陈述的模式,更能深入到你的脑海中去。而且它的真实性和可靠性都是有保证的。并且这样的模式还非常的具体。我曾经看到计算机系的同学推荐去读亚历山大的建筑学方面的经典著作;个人以为这是走的太远了。与其去读建筑学的书,不如去分析一下成功的自由软件项目的源代码。具体的用代码说明的模式,无论如何要比虚无飘渺的美学概念,或者模棱两可的工程纪律,都要更加容易学习吧?
阅读源代码的第二个好处,是增加自己的自信心。就象学习英语,要和别人谈话,要看看别人的文章,不能只是看教科书上的简单的例子。教科书上的例子限于篇幅,不可能做到像真实、完整的英文小说那样,把一个完整的设计呈现在你的面前。只有当你硬着头皮,抛开字典,把一本英文小说生生啃下来之后,你才能有把握说:我的确能做到。类似的,只有当我们看过大型软件项目的源代码,作过修改,摸爬滚打之后,我们才能有把握的说:我也能写出来。
上面说了阅读源代码至少有这么两个好处。那么在阅读源代码的时候,我们必然要做代码分析笔记。这个代码分析笔记如何写,这就是我们关心的一个问题了。在这里,我提出一些我自己的也许不太成熟的看法,也请读者朋友们不吝指教。
我总觉得,与其作一行一行的代码注释,说明每一行代码的作用;不如设计一个故事,把代码的框架说清楚。这也是我前面提到的,所谓模式一说。因为阅读源代码,最关键的是要了解大型软件项目设计的模式,而不是要把每一次读者分析每一行代码细节的乐趣从此剥夺掉。
另一方面,代码分析的写作风格,可以是参考手册似的;也可以是航海日志似的。我个人觉得参考手册似的代码分析是比较乏味的,读起来乏味,写起来也不免乏味,虽然它可能更有用。对于一个急着要快点结束加班工作的软件工程师来说,也许参考手册更加实用。但是对于一个想要了解这一份成功的软件背后的工作奥秘的探索者来说,一个航海日志似的代码分析,也许读起来更有味道,更能让一个程序员在键盘与屏幕之间,体会到那地理大发现的激动与乐趣。
本文后面的代码分析,就是希望能写成这样的风格。可是作者笔力有限,如有不足之处,还请读者朋友们不吝指教。
Treelang 的代码框架
读者朋友们在阅读这一部分代码分析的时候,手边最好能准备上一份 GCC 3.3 的源代码。这个源代码可以从 GCC 的站点
http://gcc.gnu.org 上获得。本文作者力图做到把整个情况像说故事一样娓娓道来,但是读者朋友们如果在适当的时候能够查阅一下源代码,可能更能把问题了解的清楚透彻。
这个 treelang 语言的实现,主要有两个 C 语言文件,把整个代码框架分成两个部分。第一部分以 tree1.c 为主,带上 parse.y 这个 YACC 源程序,组成了和 GCC 前端的接口;第二部分以 treetree.c 为主,组成了和 GCC 后端的接口。
这里首先说明一下 tree1.c 这个文件。它和上级目录中的 GCC 框架文件 toplev.c 交互作用,实现 tree1 这个执行程序的主体部分。这个 tree1 就相当于 GCC 的 C 语言前端中的 cc1 执行程序,该程序是 C 语言编译器前端的主体。
我们首先试图说明从 toplev.c 到 tree1.c 的路径。这样我们就注意到 toplev.c 中这个引人注目的 lang_hooks 变量。当然,接下来就注意到在 toplev.c 同一目录下的 langhooks.c 这个文件。我们希望在其中发现一点有趣的东西。这一共是三个文件:langhooks.[ch] 和 langhooks-def.h 其中在 langhooks.h 中定义了一堆各式各样的 struct lang_hooks_for_xxx 结构,以及最后还有一个 struct lang_hooks 结构把前面的那些 for_xxx 的结构都总括了起来。这每一个结构都是若干个至少看上去像是回调函数的函数指针。看来这就是我们要寻找的东西。那么大概就是这样了,编译器前端向 GCC 主体部分注册自己的 lang_hooks 来完成各样的任务。接下来一个自然的问题就是这个注册是如何进行的;另外一个问题就是要对这些回调函数指针进行分析了。
这个 langhooks.h 文件中关于 struct lang_hooks 结构字段的注释很详细,这里我们暂时先跳过去。等到 treelang 中具体的注册回调函数出现的时候,我们根据需要再做仔细说明。在 langhooks-def.h 文件中定义了一些这个 struct lang_hooks 结构的默认值。
现在我们进入 treelang 目录下的 treetree.c 这个文件。来察看一下在 treelang 中对 struct lang_hooks 这个结构的初始化过程。这个过程不是按照我们通常所熟悉的 C 语言的 C99 标准或者是 GCC 扩展语法来进行的。而是采用了大量的 #define 和 #undef 并结合上层目录中的 langhooks-def.h 来进行。细想一下,这是理所当然的事情,因为这是在编译 C 语言编译器本身嘛。当然就不好用到 C 语言的新的东西或者是自己做的扩展的东西。
注释开始:::::
我们以初始化如下定义的 struct sample 结构为例。
struct sample {
int member_int;
char *member_str;
void (*member_fun)(void);
};
在 C99 中,初始化一个 struct 结构数据,使用下面这样的语法。
struct sample inst_c99 = {
.member_int = 78,
.member_str = "iloveqhq",
.member_fun = real_fun,
};
在 C99 标准出现之前,GCC 定义了自己的扩展,下面的例子就是按照这个 GCC 对 C 语言的扩展,来初始化一个 struct 结构数据。
struct sample inst_gcc = {
member_int: 76,
member_str: "zhaoway",
member_fun: real_fun,
};
在 GCC 的源代码中没有使用上面的两种办法,而是大量使用了宏定义。这个办法首先要申明一份辅助的宏定义。这些个辅助的宏定义,在一个软件项目里面,针对一个 struct 结构,只需要一份即可。
#define MEMBER_INT 0
#define MEMBER_STR ""
#define MEMBER_FUN NULL
#define SAMPLE_INITIALIZER { \
MEMBER_INT, \
MEMBER_STR, \
MEMBER_FUN, \
}
按照上面这样的办法申明了这些关于这个 struct sample 的辅助宏定义以后,在每次要初始化一个 struct sample 数据结构的时候,只需要按照如下操作即可。除了要稍微多打一些字以外,这个方法的方便程度和以上两种方法是差不多的。
#undef MEMBER_INT
#define MEMBER_INT 12
#undef MEMBER_STR
#define MEMBER_STR "trtr"
#undef MEMBER_FUN
#define MEMBER_FUN real_fun
struct sample inst_def = SAMPLE_INITIALIZER;
这样就也可以像 C99 标准或者 GCC 的扩展一样,按照成员变量的名称来初始化一个 struct 类型的数据结构了。不过话又说回来,在我们一般的软件项目中,还是应该沿着 C99 标准这个 C 语言的发展方向来走的。
:::::注释结束
接下来的线路很清楚,就是一个一个的分析这些个回调函数啦。
对用户源文件进行语法分析
这个 treelang 注册的这些回调函数在 GCC 主框架那里被调用的顺序,我们暂时还不想深入。拣有意思的先看看吧。首先关注的是 treelang_parse_file 这个函数。在 langhooks.h 里面关于这个回调函数所作的注释说明,是要它对用户的整个源文件进行语法分析。因为这个函数的返回值是 void 所以我们预期它是通过设置某一个全局变量来完成任务的;但是也有另外一种可能,就是它会把所有要做的事情都给做完,这样它也就自然不需要返回值了。这两种可能我们现在还不能确定。让我们往下看吧。
这个 treelang_parse_file 函数在 tree1.c 中定义,这是属于到 GCC 前端的接口。它直接就跑去调用 yyparse 这个 YACC 主函数了。这倒是简单,呵呵。可是要我们从 parse.y 文件中理出个头绪来,这个文件有超过 900 行的 YACC 代码,未免有点麻烦。最关键的是这中间数据的交流不大容易看清楚,不像回调函数指针这样显而易见。如果程序果真是通过设置一些全局变量来完成任务的话,我们的分析任务就有点棘手了。
注释开始:::::
在这里先说一下 tree 这个数据结构。这是 GCC 围绕着 C 和 C++ 语言的语法分析,用到的主要数据结构。所有其它语言的编译器前端,也都需要在语法分析阶段结束以后,为 GCC 生成相应的 tree 结构的数据。然后 GCC 的后端就可以从 tree 生成独立于平台的 RTL 数据结构,并随后生成相应平台上的机器语言代码。所以作为 GCC 的编译器前端,这里的主要工作就是从一个文本文件,也就是源代码,生成这个 tree 结构的数据,喂给编译器的后端。我们看到,前端是依赖于编程语言的;后端是依赖于机器平台的;中间的 tree 和 RTL 则独立于编程语言和机器平台。但是话虽如此说,这个 tree 和 RTL 数据结构也还是主要以 C 和 C++ 语言为考虑问题的中心。这是不可避免的事情。
:::::注释结束
好啦,没办法啦。我们这就开始从 treelang 目录下的 parse.y 一行一行的往下瞅吧。这个 Treelang 程序语言的语法很简单,我们看到哪儿,说到哪儿。
注释开始:::::
在看 GCC 的源代码的时候经常会遇到 GTY 这个东西。这是 GCC 内部的内存管理机制所需要的,在 C 语言代码上添加的一些类型信息,这些类型信息在 GCC 内部做垃圾收集的时候会用到。这个细节我们这里先忽略过去,以后讲到相关内容的时候再做说明。
:::::注释结束
在 parse.y 中的一些主要的产生式上所匹配的 C 函数,它们所做的工作大体上都是首先根据语法分析的结果,把自己定义的结构 struct prod_token_parm_item 里面的数据先给设置好;然后根据情况调用在 treetree.c 中定义的相关函数,生成 tree 结构的数据;这之后再把返回来的 tree 结构数据记录在 struct prod_token_parm_item 里面,并把整个结构的数据放到 symbol_table 这个单向链表上。这样看来,似乎这个 symbol_table 就是我们前面所要寻找的全局变量了。是不是在语法分析任务完成以后,就获得了这个全局变量;然后依赖于这个全局变量,后续任务才得以获得输入数据,继续往下执行呢?
我们来仔细看一看 tree1.c 中这个 symbol_table 变量的定义如下。
static GTY(()) struct prod_token_parm_item *symbol_table = NULL;
注意到这是被申明为 static 的变量。在 Samuel P. Harbison III 和 Guy L. Steele, Jr 所合著的 C: A Reference Manual 的英文版的第五版第八十三页上,关于 static 变量有如下说明:"On data declarations, it always signifies a defining declaration that is not exported to the linker."换句话说,这个 static 的 symbol_table 变量,在 tree1.o 之外是看不见的。这不可能是我们所要寻找的全局变量。
可是,另一方面,除了这个变量有点像是那么一回事之外,其它的就再也没有什么有趣的变量了。这是怎么一回事呢?我们先不管它,往下看了再说吧。
那么这个 parse.y 文件大体如是啦。其它的一些具体的细节问题,牵涉到 Treelang 程序语言的具体定义,暂且不是我们的兴趣所在。粗粗的看一遍下来,这个语法分析的过程,从 GCC 的主体结构上,经由 lang_hooks 进入 treelang 部分的 yyparse 函数,这个函数按照语法定义,把编译器用户输入的 Treelang 语言的源程序分解成若干类型的小块,加以分析,生成自己定义的 struct prod_token_parm_item 结构的数据,再把这些数据一个一个串到 symbol_table 这个链表上面;这样就算完成任务了。线索从 lang_hooks 中定义的这个回调函数撤出,再度回到 GCC 的主体框架。
对了,上面还忘了说,在把用户输入的 Treelang 语言的源程序进行分解以后,在分析的过程中,按照各种类型的小块,还生成了相应的 tree 结构的数据,一起记录在各自的 struct prod_token_parm_item 结构里面,这样就一并把这个 tree 结构的数据也都放在了 symbol_table 这个链表里了。
接下来回到 GCC 的主体框架上的 toplev.c 文件。可是迷惑人的事情出现了,在函数 compile_file 对回调函数 treelang_parse_file 进行调用之后,无论是在 toplev.c 文件中,还是说在哪一个其它的回调函数里也好,似乎都并没有什么有趣的事情发生了。这让我们如何是好?看来我们只有回过头去仔细跟踪 treelang 目录下的 treetree.c 文件中的那些函数,看看它们在被 parse.y 中的产生式调用执行的时候,到底干了些什么。
语法分析的细节
根据从 parse.y 这个 YACC 文件中的产生式得来的线索,我们首先关注 treetree.c 文件中的 tree_code_create_variable 这个函数。从那个 YACC 产生式,我们估计这个函数是为一个变量申明而构造必要的 tree 数据结构。这个函数有 100 行不到的源代码。我们来仔细的看一看。这个函数使用了从 GCC 的框架结构里面来的关于 tree 数据结构的一些 API 接口。我们目前所最感兴趣的,就是这个函数在利用这些接口函数构造一个和所对应的 YACC 产生式相当的 tree 结构数据以外,还干了些什么。我们之所以关心这个"以外",是因为目前我们最想了解的,是这个从 Treelang 语言的源程序开始,到一连串的 tree 结构数据,然后是怎么变成 RTL 结构的数据的。只有在有了这样一个概观以后,我们对 GCC 前端的编写方法才能算有了一个初步的大概的了解。
根据这样的思路,我们很快就看清楚,在这个 tree_code_create_variable 函数中,在设置好若干个局部的 tree 结构的数据以后,引人注目的在一个 if 语句的分支中调用了 rest_of_decl_compilation 这个函数。而且在这个函数被调用返回以后,似乎不再有重要的事情发生了。这个函数来自于 GCC 框架结构上的 toplev.c 文件。这样的话,根据我们前面的分析,这个函数里面应该会隐藏有我们的主要问题的答案。也就是说,在 YACC 文件 parse.y 把用户提供的 Treelang 语言的源文件肢解以后,在 treetree.c 中的相应的函数,为之生成了相应的 tree 结构数据,而在现在我们所关注的这个 rest_of_decl_compilation 函数(以及在这个 if 语句的另一个分支中出现的一系列相应的函数)中,应该会完成从 tree 结构的数据到 RTL 数据的翻译。
从另一个角度补充一点,程序的执行线索是如何从 GCC 主框架进入 parse.y 中的呢?这一段我们前面分析过了,现在再来提醒一下。这是从 GCC 的框架结构,进入到 treelang 这个 GCC 的语言前端模块注册的 lang_hooks 结构的数据,找到相应的回调函数,最终找到 parse.y 这个 YACC 程序的入口 yyparse 函数的。在 yyparse 之后,我们看到程序的主线索进入了 treelang 目录下的 treetree.c 文件中的函数。最后,我们重新又追踪到 GCC 主体部分的 toplev.c 文件中的函数。现在我们的整个图景的大轮廓就快要完全弄清楚了。
GCC 前端的全景图
终于,我们在 rest_of_decl_compilation 函数中,看到了一系列的和 RTL 相关的函数调用。稍微仔细的看了一遍之后,我们有把握得出这个结论了。我们在本文的开头部分,曾经猜想 GCC 的主体部分在要求 GCC 这个 Treelang 语言前端从用户提供的 Treelang 语言的源程序文本,经过语法分析,得出相应的 tree 结构数据以后,会把这个数据通过函数返回值传回给 GCC 的主体程序,或者设置一个全局变量,这样就算完成任务了。但是事实上,经过我们上面的分析,发现不是这么一回事。
相反的,在 Treelang 这个语言前端得到需要的 tree 结构的数据以后,继续往下的运行,这完全是 Treelang 前端必须自己负责的任务。这个 GCC 前端必须自己调用 GCC 主体部分提供的,用来从 tree 结构数据生成 RTL 结构数据的函数接口,以完成从 tree 结构数据到 RTL 结构数据的翻译过程。这样,这个 GCC 的语言前端的任务才算完成。换句话说,GCC 的这个语言前端承担的角色是非常的主动的。很明显,这样的设计提供给我们极大的灵活性。关于这一点,我们以后会逐渐看到。
小结
本文限于篇幅,只大略讲述了 GCC 前端的框架结构,给出了一个粗略的全景图。在以后的几篇文章中,我们将进一步探索 GCC 的主体部分为 GCC 前端所提供的 API 函数和数据结构。并利用这些知识,探索一下为 GCC 编写一个 Scheme 语言前端的可能性。在这一系列文章结束的时候,希望能使得读者朋友们对 GCC 以及程序语言的本质有一个更加深刻的了解。也希望 GCC 的前端的作者人数,就能和 Linux 内核模块的作者人数一样多。我们的座右铭是:每一个人都是程序员;每一个人都能加载自己编写的内核模块;每一个人都能使用自己实现的编程语言!(不要害怕,这只是一句玩笑话。呵呵。)
在技术内容以外,本文也探索了开放源码运动所需要的技术文档的一种写作模式。开放源码运动为我们带来了大量的自由软件的源程序。对于用户来说,需要文档讲述如何使用这些自由软件;对于程序员来说,则需要文档讲述如何才能理解并真正的掌握这些自由软件的源程序。这第二种文档的写作,不是一件容易的事情。作者本人在经常阅读解释自由软件的源程序的内部运作机理的文档的过程中,总是觉得这件事情应该可以有办法做的更好。本文就是作者的一个尝试。希望读者朋友们给我来信,不仅仅讨论 GCC 的技术问题,也欢迎对作者的写作方式提出批评与指教!
参考资料
Internals of the GNU Compiler Collection
http://gcc.gnu.org/onlinedocs/gccint/ 这其实也就是 info gccint 的内容。这份文档是除了源代码以外最权威的资料了。不过它的可读性恐怕不是那么好。初上手阅读的时候,恐怕会非常困惑的。
The GNU Treelang Compiler 手册,这其实也就是 info treelang 的内容。这个作为例子的 Treelang 程序语言基于 Tim Josling(见下)为了阐明 GCC 前端的编写方法而发明的一个玩具编程语言。
Sreejith K Menon 编写的 GCC Frontend HOWTO
http://www.tldp.org/HOWTO/GCC-Frontend-HOWTO.html 被收录在 The Linux Documentation Project 的 HOWTO 文集中。这份资料可能是可读性最强的了吧?当然,这是说除了本文之外啦。:-)
Using, Maintaining and Enhancing COBOL for the GNU Compiler Collection
http://cobolforgcc.sourceforge.net/cobol_toc.html 这是由 Tim Josling 领导的 COBOL for GCC 项目的文档,其中原先由 Joachim Nadler 所编写的第十四章,后来由 Tim Josling 从德文翻译成英文;这一章讲述了 GCC 前端的编写方法。
Using and Porting GNU Fortran 手册
http://gcc.gnu.org/onlinedocs/g77/ 中关于 Front End 的一章也讲述了我们感兴趣的内容。
自由大百科全书 Wikipedia 中关于 GCC 编译器家族的条目
http://www.wikipedia.org/wiki/GNU_Compiler_Collection 对 GCC 有个概括介绍。关于 GCC 内的 RTL 数据结构的条目
http://www.wikipedia.org/wiki/Register_Transfer_Language 以及关于 GCC 的前端使用的 Tree 数据结构的条目
http://www.wikipedia.org/wiki/GCC_Abstract_Syntax_Tree 也都值得一看。
用 Doxygen 文档生成工具制作的 GCC Source Documentation
http://www.nondot.org/gcc/ 有兴趣的话也可以看一看。
在阅读大型的 C 语言项目的源代码的时候,手头有一本好的、全的 C 语言参考手册也是很重要的。
关于 C 语言的一本比较好的书是 Samuel P. Harbison III 和 Guy L. Steele, Jr. 合著的 C: A Reference Manual 第五版。这本书的英文影印版最近在国内出版了。作者之一 Guy L. Steele, Jr. 是 Scheme 编程语言的发明人之一,也是 Java 语言规范的作者之一,更是 ACM 的 Grace Murray Hopper 奖 1988 年的获得者。
关于作者
赵蔚,南京的 Linux 和自由软件技术独立顾问。他的网络日记http://www.advogato.org/person/zhaoway的前言部分列有他在网络上发表的技术文章的一份清单。他还有另外一处网络日记http://www.kuro5hin.org/user/zhaoway/diary您可以在上面发表评论。他也经常以 iloveqhq 的网名登录南京大学小百合 BBS 站
http://bbs.nju.edu.cn 的 LinuxUnix 版和 CompLang 版参加讨论。您可能通过
[email protected] 与他联系。
http://www-900.ibm.com/developerWorks/cn/linux/l-gcc/part1/index.shtml