Board logo

标题: 探索 GCC 前端的内部结构(1) [打印本页]

作者: look_w    时间: 2018-4-21 12:50     标题: 探索 GCC 前端的内部结构(1)

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 结构为例。
1
2
3
4
5
struct sample {
        int member_int;
        char *member_str;
        void (*member_fun)(void);
};




在 C99 中,初始化一个 struct 结构数据,使用下面这样的语法。
1
2
3
4
5
struct sample inst_c99 = {
        .member_int = 78,
        .member_str = "iloveqhq",
        .member_fun = real_fun,
};




在 C99 标准出现之前,GCC 定义了自己的扩展,下面的例子就是按照这个 GCC 对 C 语言的扩展,来初始化一个 struct 结构数据。
1
2
3
4
5
struct sample inst_gcc = {
        member_int: 76,
        member_str: "zhaoway",
        member_fun: real_fun,
};




在 GCC 的源代码中没有使用上面的两种办法,而是大量使用了宏定义。这个办法首先要申明一份辅助的宏定义。这些个辅助的宏定义,在一个软件项目里面,针对一个 struct 结构,只需要一份即可。
1
2
3
4
5
6
7
8
9
#define MEMBER_INT 0
#define MEMBER_STR ""
#define MEMBER_FUN NULL
                                                                                       
#define SAMPLE_INITIALIZER {   \
        MEMBER_INT,            \
        MEMBER_STR,            \
        MEMBER_FUN,            \
}




按照上面这样的办法申明了这些关于这个 struct sample 的辅助宏定义以后,在每次要初始化一个 struct sample 数据结构的时候,只需要按照如下操作即可。除了要稍微多打一些字以外,这个方法的方便程度和以上两种方法是差不多的。
1
2
3
4
5
6
7
#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 语言的发展方向来走的。
:::::注释结束




欢迎光临 电子技术论坛_中国专业的电子工程师学习交流社区-中电网技术论坛 (http://bbs.eccn.com/) Powered by Discuz! 7.0.0