阅读lcc(2)

4

1 编译器的一般结构

1.1 规范流程

编译技术之所以在计算机科学领域内成为一个实践相对成熟的技术,很大程度上是因为几乎所有的编译器都遵循着一个严格的流程,而且在流程的每个部分都有完整的理论框架和足够多的实现工具.这使得每个编译器在设计的时候考虑的仅仅是在每个步骤中选择什么样的技术来实现.这给编译器的设计者留出了足够多的时间去关注语言特性本身的设计.

就现代编译器来说,一段源代码最终转换为一段目标机器代码需要经历如下流程:

                [ 词法分析]                        [语法分析]                     [ 语义分析 ]                          [代码生成] 

源代码 -------|---------->    符号流  --------|-------> 语法树  -----------|---------> 中间表示 ---------|------> 目标代码

                       |___________________|__________________|___________________|

                                                                               |

                                                                         [符号表]

1.2 前端和后端

在以上过程中,   [ 词法分析]  [语法分析]  [ 语义分析 ]  到生成中间表示的阶段  通常称为编译器的前端,而[代码生成] 通常还有[代码优化]通常称为编译器的后端.

编译器的前端读入程序源代码, 将一个一个的字符流用[ 词法分析]  归整为一个一个 [语法分析器](parser)能识别的符号(token)流,去除其中的空白和注释, [语法分析器]接收到符号流后,根据事先设计的语言文法(grammar), 建立语法分析树, 尔后经过[ 语义分析 ] , 生成中间表示,实际上大多数编译器在进行[语法分析]的同时就进行了[ 语义分析 ]  ,而每种编译器所选择的中间表示也有所不同,比如dag, tree, 三地址码, 或是各种专有表示,例如GCC所采用的RTL, 一般来说,将中间表示做的越为通用,越标准化越好,因为这样可以通过为一种不同语言写一个独立的前端,产生标准的中间表示,可以实现一种语言的编译器,同样,也可以基于标准的中间表示,写一个独立的后端,来产生另一个平台上的目标代码. GCC就是这么干的.

有了一个包含源代码完整语义的情况下,[代码生成]负责生成目标代码,通常是目标机器的汇编语言;一个实际应用的编译器会将大量的工作花费在[代码优化]上,好的代码优化器会产生质量高而且占用空间小的代码,这也是编译器最为复杂的部分之一.

1.3 符号表

还有一个整个流程中都会用到的符号表(Symbol table), 符号表是用来存贮程序中定义或声明的各种标示符(identifier), 字面常量(constant),编译器生成的临时变量,标号(labels). 编译器在[词法分析]期间会将识别作为声明(或定义)的符号安装安装到符号表里去,在 [ 语义分析 ]阶段会查找需要的符号,找出未定义的符号等错误, 在代码生成期间,会为符号生成临时地址,分配寄存器.

1.4 中间表示?

为什么要一个中间表示?或许基于上面所说的复用代码的理由.但这使我想到另外一个有趣的例子:传说中有这么一个测试,让一个人先快速阅读目标文字T,拿走这段文字,然后给他看另外两段文字A和B,文字A和B有如下特证:A文字与T多数单词类似,而意义大不相同, B文字与T单词大不相同,而意义类似, 现在让这个人从A和B中选一段和T最为接近的,几乎所有的人都会选B.  这似乎说明大脑在处理文本信息的时候倾向于把文本转化为一个意义上的中间表示,这种表示是与文本的字面形式无关的,只与其所蕴涵的含义有关; 而编译器是要在保证程序语义不变的情况下将一种形式转为为另一种形式, 或许,将其先转化为一种形式无关的中间表示是个更符合理性的选择?

2 rcc 的结构

2.1 rcc的结构差异

rcc与上述的一般语言编译器的流程没有什么差别,关键在于其实现方法与其他编译器有些许不同.相对一般编译器的前端通常采用目前成熟的工具(如lex, yacc)来生成的做法,rcc的词法分析器和语法分析器是采用纯手工打造; 而其后端却是用自己写的工具lburg来读入指令匹配模板生成的.这样的做法至少基于如下的几个理由:

1) 虽然利用lex生成词法分析器是一种非常简单的做法,只需要根据语言的规范写一些正则表达式来描述符号的形式就足够,但是lex生成的代码多是利用DFA算法根据很多的跳转表来匹配符号的,其运行效率比不上手写的专用词法分析器. 看看rcc的gettok函数(src/lex.c下), 就是一个大的switch/case语句,实现非常紧凑,对于高频率调用的gettok函数来讲,速度的提高是比较明显的.

2) rcc的语法分析是用的最简单直白的递归下降算法,这是由于C文法是简单的LL(1)文法,用此种方法已足够分析,同样的,rcc也在语法分析算法的局部使用了更为优化的方法,比如在识别运算表达式时,利用运算符优先级循环解析大大减少了递归调用的次数.

3)利用lburg来生成[代码生成]模块. rcc的中间表示是一个种基于公共表达式的无环有向图(dag), rcc先利用事先写好的规则自底向下的 规约树并利用规则标注树节点,然后自上而下用动态规划算法选择发射指令. lburg的输入文件中是规约树的规则和相应的目标代码模板,lburg将其转换为选择发射指令的代码.如果要为另一种目标平台生成代码,只需要修改这些规则和相应的目标代码模版. 因为lcc是为了在不同的目标平台上运行并生成不同的目标代码,这样做会尽可能少的修改代码.

2.1 rcc的符号表和类型系统

rcc的符号表是一个hash 链表. 每个符号用一个symbol类型(见src/c.h的定义)来表示.符号按类型来分,分为几种:标示符(identifiers), 字面常量(constants),标号(labels);分别存在不同的符号表中,而按其作用域来分,又分为全局符号(globals),局部符号(locals),外部符号(externals),在以函数为单位的编译过程中,每个函数定义体都会有自己的局部符号表,而所有的函数都会共用一个全局的符号表,而局部符号表里又可以嵌套很多个内部作用域.

如果作用域越深,其符号表的级别(level)越高, 那么在查找一个符号的时候,由level高的符号表往level低的符号表里查,就能实现C的作用域规则.

rcc的每个符号都有一个相关类型(type,见src/c.h的定义), rcc中所有的类型都存贮在一个types的类型hash表中.

下一节详细描述这两个结构symbol和type.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值