3.语法分析器
语法分析器是整个环节比较主要的一环,大部分的任务都是由其承担。在前面的基础知识提到过语法分析的基本方法,由于递归子程序手工构造比较简便,故本设计使用的是递归子程序法。由于在实现递归时,要考虑是否有递归的出口,因此需要提取左公共因子,降低递归深度。在语法分析的时候,不仅需要考虑到语法分析程序的高效性和简洁性,在降低耦合性的同时需要注意函数的之间的联系。
语法分析时,可能文法的描述很简便,但不利于实现,需要对其文法上的等价变换才能更好地编写。这里将介绍函数逻辑和文法等价上的变换,介绍的书籍已经有很清楚的介绍,不再进行赘述。下面开始该过程实现之路。
3.1解析语句
Via-C语言由于有很多语句种类,下面介绍几种常见语句的解析过程。
例如for循环语句解析过程如下图3.1:
3.1 for 循环语句的解析过程
从括号解析,进行循环变量的初始化,再进行for循环的条件的判断,然后进行for循环体的代码的运行。
选择语句,这个都比较熟悉。下图3.2是语句的解析过程:
3.2 选择语句的解析过程
虽然跳转语句有 continue语句等好几种语句,但是它们解析的过程基本相似。解析过程如图3.3。
3.3 跳转语句的解析过程
语句还有其他几种分类,这里不再详细介绍。语句的解析关系着整个语法分析器的逻辑部分,具体可见文件grammar.c。
3.2解析定义和声明
解析类型符过程如图3.4:
3.4 类型解析过程
先经过词法分析器的解析后,对传递进来的标识符进行识别,上图解析了Via-C语言的所有的数据类型。
图3.5是声明符的解析过程:
3.5 声明符解析
声明符主要是解析函数定义和变量的定义,实现详情见grammar.c文件。
3.3解析表达式
Via-C表达式有好几类,虽然有在表现形式上有区别,但是表达式在解析过程中基本上是差不多的结构。另外解析表达式时,还需要考虑到运算符的优先级高低。在前面的表格中已经列出了优先级高低。图3.6是乘除表达式的解析过程。
3.6 乘除表达式解析过程
3.4语法缩进
在语法的分析中,有些程序没有进行缩进的处理,可能需要对其缩进,以便让 语法分析解析后产生的输出,更加直观。
以下是对语法状态的枚举类型定义
enum e_SynTaxState
{
SNTX_NUL, //空状态
SNTX_SP, //空格
SNTX_LF_HT, //换行缩进
SNTX_DELAY //延迟输出
};
具体的缩进操作函数和缩进动作实现见grammar.c,另外在Via-C中采用的缩进是4个空格。
3.5语法分析的测试
输入程序(图3.7)没有缩进,经过语法分析之后,得到结果(见图4.13),可以看到程序已经根据了声明和定义、函数定义等规则进行了缩进,证明了语法分析程序已经成功。
3.7 语法分析的示例程序
可以看到示例程序是杂乱无章的,经过语法分析器分析后输出整齐一致,如图3.8。
3.8 语法分析结果图
4.符号表和语义分析器
源代码经过上一层次分析后,只能说明代码在语法和词法上符合相关的规则,并不能说代码已经是正确。例如在函数内部并没有循环结构,出现了非法的跳转语句,这是符合语法规则的,然而在逻辑上这并不是正确的。语义分析程序需要抓住这类的漏网之鱼。这一阶段的工作比较繁杂,需要先做好符号表的准备,再进行语义上的剖析,以便下阶段的代码生成准备。
4.1符号表
符号表主要是储存符号的相关特征信息,其需要在编译的过程不断收集信息的同时不断更新信息,通过对属性信息的查询可以进行语义检查。
符号表主要用栈来实现。由于变量存在作用域的区别,因此需要两个栈分别进行信息的收集。分别定义如下:
Stack G_Sym; //全局符号栈
Stack L_Sym; //局部符号栈
接下来编写符号表的操作函数,此后就可以在上阶段的基础上进行符号表的构造。
在构造字符表时需要注意的是字节对齐,尤其在自定义类型的成员上。字节对齐可以提高访问速度,但是如果一味地追求字节上的对齐,会导致许多空间没有储存数据,导致性能上的浪费。
4.2语义分析器
前面提到过静态缺陷和运行异常,语义分析器就是处理静态语义缺陷。语义分析器主要是限制目标程序和源程序在逻辑结构上完全一致,因此在以前语法分析器的基础上进行语义上限制。对语义的分析有两个层次,首先是正确性分析,即需要根据语言的定义规则去判断代码语义的正确性,其次是优化分析,也就是用于分析提高目的代码执行的性能。
语义分析器基本是在前面的基础上实现,实现详情见grammar.c文件。
5.代码生成器和错误处理
本设计的适用平台为Windows,所以可执行文件的格式为COFF,采用的机器语言也是Intel x86机器语言。Intel x86机器语言有七种指令,跳转和算术指令等等,以及各种的寄存空间的操作。代码生成器需要生成对应的指令和对应的寄存器的操作。具体函数见gencode.c文件。
代码生成器效果图如5.1所示,这一阶段任务主要是生成了中间代码文件。中间代码文件中储存了程序的相对地址,生成可运行程序需要链接器和库文件进行链接。下图展示了生成的效果。
5.1 中间代码文件生成
在编写代码的过程中,思维上的遗漏或者是语法上不正确的情形不可避免会出现,因此错误控制程序是有必要存在的。一般来说程序出错的等级有词法警告、语法错误以及运行异常等。
图5.2是Via-C的错误控制程序的处理流程,具体实现见error.c。
5.2 错误控制程序
6.链接器
前面已经介绍了链接器的有关信息。下面开始介绍链接器相关实现的信息。
首先介绍应用平台的有关知识。Windows平台使用的可执行文件为PE格式,而PE文件主要由DOS部分、NT头、节点表、节数据等部分构成,DOS部首由两部分组成:DOS的MZ文件标志和DOS存根程序,NT头包含了PE文件签名、COFF文件头、COFF可选文件头[8,16-19]。
这个阶段要加载相关文件和解析相关文件中引用的外部符号,并且计算RVA地址,然后进行目的文件的生成。
下面开始这阶段链接器逻辑的编写,流程顺序图见图5.3。
5.3 链接器的编写流程图
中间代码的优化,是可以在代码生成时采取优化措施直接进行优化,但这样仅仅是优化了一个文件,无法顾及到整个程序的优化。因此一些重要的优化就需要在链接器链接时才能进行。编译器有很多的优化方法,浅易如折叠不变量,高深同重排指令等等。但是有些要紧的优化是超过当今世面上编译器性能,例如使用高性能的算法替代低性能的算法,抑或是优化指令的排列,提高代码的运行性能。
图5.4是链接器的成果展示:
5.4链接器成果图
到这里已经完成了链接器的编写,同时编译内核的工作基本上完成了。后续将进入周边功能的实现。