编译的流程
对于c语言这种,需要经历四个过程:
- 预处理
- (狭义的)编译
- 汇编
- 链接
第一步,由预处理器对一些宏进行展开和一些包括的头文件进行填充,不考虑其他语法含义.第二步,需要生成汇编语言,也就是扩展名为”.s”的文件,内容是接近机器语言的汇编代码.第三步,由汇编器转换为机器语言,内容是机器可执行的二进制文件,后缀名为”.o”,也称为目标文件.还需要进行一步链接,有些需要用库,这一步可以对程序库进行加载.
在jvm中,使用了JIT编译器,能够在程序运行时将程序转换为机器语言的编译器.
编译的阶段
词法分析
对代码进行每个词的词法分析,用一些结构体将每个合法的词解析出来.比如”x=1+2”,分割成五个单词,并且识别出是单词的语义.
语法分析
词法分析后需要进行语法分析,转换为”语法树”.
语义分析
通过解析代码获得语法树后,需要对树进行处理生成抽象语法树(AST),语义分析包括:
- 区分变量为局部变量还是全局变量
- 解析变量的声明和引用
- 变量和表达式的类型检查
- 检查在引用变量之前是否进行了初始化
- 检查函数是否按照定义返回了结果
上述处理结果都会反应到抽象语法树中,语法分析生成的语法树只是将代码的构造搬了过来.
生成中间代码
将抽象语法树转化为只在编译器内部使用的中间代码,为了支持多种编程语言或者机器语言.
代码生成
中间代码转化为汇编语言,称为代码生成,负责这个模块的称为代码生成器.比如说汇编语言中局部变量只能通过寄存器来存放,寄存器数量非常有限而c或java都可以随意定义局部变量.
cmm概要
仿c语言语法的编程语言c–,cmm相对c语言除去的功能:
- 预处理器,改用java中的import
- 浮点数
- enum枚举
- 结构体的位域
- 结构体和联合体的赋值和返回值
- 逗号表达式
- const,volatile,auto,register
- 添加了boolean关键字
- 对于语义规则更加严格.
import关键字
语法类似如下:12
后缀名为’.cmm’的类似于c语言中的源文件,后缀名为’.hm’的为头文件,一般包含下述声明:
- 函数声明
- 变量声明(不可包含初始值的定义)
- 常量定义(这里必须有初始值)
- 结构体定义
- 联合体定义
- typedef
比如stdio.hm
的内容:12345import stddef;import stdarg;typefef unsigned long FILE;// dummyextern FILE* stdin;...
cmc编译器的代码构成
包 | 包中的类 |
---|---|
asm | 汇编对象的类 |
ast | 抽象语法树的类 |
compiler | Compiler类等编译器的核心类 |
entity | 表示函数和变量等实体的类 |
exception | 异常的类 |
ir | 中间代码的类 |
parser | 解析器类 |
sysdep | 包含依赖于OS的代码的类(汇编器和链接器) |
sysdep.x86 | 包含依赖于OS和CPU代码的类(代码生成器) |
type | 表示cb的类型的类 |
utils | 小的工具类 |
一个大致的流程:
- 首先看Compiler包,其中包含了一个Compiler类,有main入口,也就是编译的入口
- 由传入的命令行参数得到所有源文件的名字.
- 调用build方法对源文件进行编译,首先用compile得到汇编文件.然后用assemble运行汇编器,转换为目标文件.
- 其中compile完成编译器前端部分,首先用对源文件用parser得到AST,然后语义分析完成抽象语法树.最后一个IR类生成中间代码.
解析器javaCC
解析器的作用是,规定好语法,自动生成解析相应语法的代码.否则手动编写NFA转DFA,状态转移表,LL算法,手动生成语法树.
javaCC是LL解析器,
语法规则用一个”.jj”的文件来描述,称为语法描述文件.首先,文件的大致格式如下:
举个加法的例子:
注意,要运行javaCC生成的解析器类,需要下面两个步骤:
- 生成解析器类的对象实例(上例Adder就是一个解析器类)
- 用生成的对象调用和需要解析的语句同名的方法.(也就是调用expr())
- JavaCC生成的解析器中默认定义有如下4个类型的构造函数:
- Parser(inputStream s)
- Parser(inputStream s,String encoding)//如果要解析中文必须用这个或者下面的构造函数,并且在设置里将UNICODE_INPUT设为true.
- Parser(Reader r)
- Parser(xxxxTokenManager tm)