接下来的线路很清楚,就是一个一个的分析这些个回调函数啦。
对用户源文件进行语法分析这个 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 变量的定义如下。
1
| 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 内核模块的作者人数一样多。我们的座右铭是:每一个人都是程序员;每一个人都能加载自己编写的内核模块;每一个人都能使用自己实现的编程语言!(不要害怕,这只是一句玩笑话。呵呵。) |