C1编译器的实现

总览
词法、语法分析
分析方案
词法
语法
符号表
类型系统
AST
语义检查
EIR代码生成器
MIPS代码生成器
寄存器分配
体系结构相关特性优化
使用说明
编译
运行

总览

C1语言编译器及流程

C1 语言是一个类 C 的语言。语言的特征为:

  • 包含 int、float 和 bool 简单类型以及以这些类型为基本类型的多维数组类型。
  • 一个 C1 程序包含多个函数、全局变量声明和常量声明,其中必须有一个 void main(void)主函数。
  • 函数可以带参数,也可以不带参数,参数的类型是简单类型。
  • 函数的返回类型可以是void,或者是某简单类型。
  • 函数体中可以有常量定义、变量声明和函数声明,包含表达式语句、条件语句、循环语句、函数调用语句、复合语句和空语句。

本文实现的C1编译器,其编译流程由词法语法分析、语义检查和代码生成三个阶段组成。其主要的特点是:

  • 多目标:对C1源代码,可以生成MIPS汇编码、EIR二进制码和C代码;
  • 强大的类型系统:可以识别C语言语法的类型定义,输出其类型表达式;
  • 实现绝大部分C1语言特征
  • 带有扩展语法:如continue、for等;
  • 较详细的错误报告

下面根据编译器的阶段,逐一介绍其实现细节。

词法、语法分析

分析方案

本阶段的分析是把字符串流转换为抽象语法树。

词法、语法分析分别使用Flex和Bison构造。

分析时,只对语句建立树结构。对于符号的定义(变量定义、函数定义等),并不对其语法成分建树,而是顺着分析流程建立符号表,并把符号放在符号表中。

这样,就可以 避免语法树中出现大量的字符串,使得树的结构、结点的类型得到了简化。缺点是 造成复杂的类型分析比较困难,将类型系统的设计大大复杂化了。

翻译完成后,得到的总入口为全局符号表,从此符号表开始检索,可以得到程序的所有信息。

词法

与C的词法类似,其主要区别为:

  • read和write是保留字,用于在C1中进行输入输出;
  • bool、true和false是保留字,用于实现布尔类型;

其余还有一些区别,如sizeof不是单词等,但并不重要。

语法

本实现的语法与C1的语法基本相同,其主要区别是:

  • 没有逗号表达式;
  • 包含for语句;
  • 函数的参数可以是数组类型(值传递语义);
  • 变量初始化语法只能有一层括号,且不能有多余逗号。
  • 下列不是运算符
 ++、--、+=、-=

符号表

符号表实现在src/sym_tab.c中。采用多层结构,图示如下:

        +->指向上一层sym_tab
 +------|---->+---------+<-----------+
 |      |     | sym_tab |            |
 |      |     +---------+            |
 |      +-----| uplink  |            |
 |            +---------+            |
 |        <-->|  order  |<-->        |
 |            +---------+            |
 | +--------->|entry[i] |<-----------|----+
 | |          +---------+            |    |
 | |  +---------+     +---------+    |    |
 | |  |sym_entry|     |sym_entry|    |    |
 | |  +---------+     +---------+    |    |
 | +->|  list   |<--->|  list   |<->.|..<-+
 |    +---------+     +---------+    |
 +--->|   tab   |     |   tab   |----+
      +---------+     +---------+

上图中,sym_tab是符号表,sym_entry是表项。

表项串接在符号表中,有list和order两个线索。表项的list链条是哈希链,order链条为顺序链。

查找符号时,先在本层的符号表查找。若找不到,则顺着uplink向上一层再查,直到找到或到达顶层。

符号表记录符号的名字和类型。根据不同的类型有不同的记录,如函数有函数局部符号表地址、函数语句AST指针、函数地址、函数类型等信息。

类型系统

表示

类型系统实现在src/type.c中,其基本结构类似符号表,也是一个哈希链条将所有类型串起来。

每个类型的定义如下:

struct type {
      struct list_head list;
      enum {
            TYPE_VOID = 0,
            TYPE_INT,
            TYPE_FLOAT,
            TYPE_BOOL,
            TYPE_ARRAY,
            TYPE_FUNC,
            TYPE_LABEL,
            TYPE_TYPE,
      } type;
      int n;
      int is_const;
      union {
            struct sym_entry *e;
            struct type *t1;
      };
      struct type *t2;
};

有上述定义可见,这个类型的定义是树状的,因而可以表达非常复杂的结构,如函数数组,数组函数等。

名字

上面类型都是匿名的,当需要给类型取名(包括内置类型和用户自定义类型)时,可以构造一个TYPE_TPYE类型的类型。其中上述结构体的e指向符号表,给出类型名字,t2指向真实的类型。

在编译器初始化时,默认给内置类型命名:

symtab_enter_t(symtab, "int", get_type(TYPE_INT, 0, 0, NULL, NULL));
symtab_enter_t(symtab, "float", get_type(TYPE_FLOAT, 0, 0, NULL, NULL));
symtab_enter_t(symtab, "bool", get_type(TYPE_BOOL, 0, 0, NULL, NULL));
symtab_enter_t(symtab, "void", get_type(TYPE_VOID, 0, 0, NULL, NULL));

当用户用typedef定义新类型时,可以类似上述方法,在符号表中记录相应类型。

等价

类型等价可以按结构和按名字。

从类型的表示可见,当类型需要按名字等价时,只要比类型指针就可以了。若指针不等,则不是同一类型(匿名的类型总是不等的):

static inline int type_is_equal_byname(struct type *t1, struct type *t2)
{
      return t1 == t2;
}

当按结构等价时,则需要递归地比较两个类型树的所有属性:

static inline int type_is_equal_bystru(struct type *ty1, struct type *ty2)
{
.....
      if(ty1->type == TYPE_FUNC)
            return type_is_equal_bystru(ty1->t1, ty2->t1) &&
                  type_is_equal_bystru(ty1->t2, ty2->t2);
.....
}

解析

C语言中的类型定义 并非是书写类型表达式,而是声明其用法。这造成了这一部分实现的极端复杂。

如类型表达是为int->array(10,int)的类型用C语法写出为:

int type(int a)[10];

为了分析这种类型,在rule/c1.y中有两个函数来处理之。

AST

AST实现在include/ast_node.h中。

由于语义的要求,树结点的分叉数是不一样的的,故采用链表 将儿子和兄弟组成一个双向链表(从Linux内核取出,而非bison-example),增强通用性。

定义如下:

struct ast_node {
	unsigned short type;
	unsigned short id;
	struct list_head sibling;
	int first_line;
	int first_column;
	union {
		void *pval;
		int ival;
		float fval;
		struct list_head chlds;
	};
};

各个域含义为:

  • type:结点类型(exp、block等,详见node_type.h)
  • id: 结点子类型(‘+‘、‘-‘等)
  • sibling:兄弟组成的链表
  • first_:位置追踪信息
  • chlds: 儿子组成的链表
  • val: 结点属性值

图示如下:

   +--------------------------------------+
   |  +---------+     +---------+         |
   |  |  types  |     |  types  |         |
   |  +---------+     +---------+         |
   +->| sibling |<--->| sibling |<->....<-+
      +---------+     +---------+
   +->|  chlds  |<-+  |   val   |
   |  +---------+  |  +---------+
   |               |
   |  +---------+  |
   |  |  types  |  +----+
   |  +---------+       |
   +->| sibling |<->..<-+
      +---------+
  ..->|  chlds  |<--..
      +---------+

基本操作只有三种: ast_node_new 新建 ast_node_delete 删除 ast_node_add_chld 增加儿子

其余遍历兄弟和儿子的操作使用list.h中的list_for_each_entry实现。

语义检查

此遍较简单,主要要做的检查为:

  1. 类型检查和提升
  2. continue、break在while或for中
  3. 变量不能是void
  4. const变量不能被赋值

EIR代码生成器

EIR指令模拟的是一种栈式机器,指令类型和意义可见eir/interp_dbg.c。

此指令集的特点是: 已经将所有的策略定好,因此指令生成并没有太多灵活的空间,只要对树进行一次遍历,就可以生成代码。

值得一提的是短路运算的翻译方案。如and的翻译如下:

geni(lit, 0, 0);
gen_exp(l);
cj1 = cx;
geni(jpc, 0, 0);
gen_exp(r);
cj2 = cx;
geni(jpc, 0, 0);
geno(opr, 0, notnot);
code[cj1].v.i = cx;
code[cj2].v.i = cx;

这个翻译方案的特点是:

  • 若两个表达式有一个为假,最终栈顶留下数字0
  • 若第一个表达式为真,第二个表达式不求值
  • 两个表达式均真时,执行notnot操作,将栈顶翻转为1

因此这个方案是and操作的合法方案。这个方案 用较少的指令达到了准确的翻译,且翻译只需要局部的信息。缺点是条件较复杂时可能要连续经过多次跳转才能到达目标。

or的翻译类似可得。

MIPS代码生成器

寄存器分配

MIPS是基于寄存器的机器,因此相对于栈式机器,需要进行寄存器分配。

为了简单起见,本生成器基于基本块来分配。

寄存器分配器为每个寄存器维护如下的结构:

struct reg_struct {
	int dirty;
	int loaded;
	struct sym_entry *sym;
	struct list_head list;
	struct list_head avail_list;
};

由此可知,这里一个寄存器仅仅可以关联一个符号sym。符号表中同时有一项指向寄存器结构,表示当前此符号被关联到了哪个寄存器上。

当产生对sym对应寄存器修改的指令时,dirty位置1。

当到达基本块出口时,调用reg_wb_all函数产生指令将dirty为1的寄存器写回内存。同时将原来所有关联取消,以便下一个基本块分配。

分配函数的核心为get_reg函数。生成器将要使用的符号传递给get_reg。

get_reg函数首先查看是否符号已经关联,若是则直接返回寄存器号。否则,从avail_list链中取出一个可用寄存器,将符号关联到此。若avail_list为空,则产生溢出,将list上面的一个变量写回内存,在将符号关联到此。

体系结构相关特性优化

延迟槽的利用

由于这个生成器还十分简单,获取的全局信息也不够,因此 对一般生成的指令,延迟槽内仅仅填写空操作。但是 对于函数框架模板、短路翻译方案等地方,手工做了优化

叶子函数

叶子函数是指此函数体内没有进一步函数调用。根据MIPS体系结构特点,不需要将返回地址放入内存。

我们在语义检查阶段对函数调用情况进行统计,当生成时,发现可以进行叶子函数的优化时,就产生特殊的指令,提高效率。

使用说明

编译

输入make,得到c1c执行文件。

运行

从命令行读取参数,使用方法类似GCC:

编译生成EIR中间代码:
	c1c src_file [-o out_file]

编译生成C代码:
	c1c src_file [-o out_file] -m c

编译生成MIPS汇编代码:
	c1c src_file [-o out_file] -m spim

帮助:
	c1c -h

原文: http://home.ustc.edu.cn/~hchunhui/c1.html
时间: 2024-10-29 04:22:14

C1编译器的实现的相关文章

[Inside HotSpot] C1编译器工作流程及中间表示

1. C1编译器线程 C1编译器(aka Client Compiler)的代码位于hotspot\share\c1.C1编译线程(C1 CompilerThread)会阻塞在任务队列,当发现队列有编译任务即可CompileTask的时候,线程唤醒然后调用CompilerBroker,CompilerBroker再进一步选择合适编译器,以此进入JIT编译器的世界. CompilerBroker到C1编译器进行JIT编译的调用栈如下: CompileBroker::invoke_compiler_

[Inside HotSpot] C1编译器优化:条件表达式消除

1. 条件传送指令 日常编程中有很多根据某个条件对变量赋不同值这样的模式,比如: int cmov(int num) { int result = 10; if(num<10){ result = 1; }else{ result = 0; } return result; } 如果不进行编译优化会产出cmp-jump组合,即根据cmp比较的结果进行跳转.可以使用gcc -O0查看: cmov(int): push rbp mov rbp, rsp mov DWORD PTR [rbp-20],

[Inside HotSpot] C1编译器优化:全局值编号(GVN)

1. 值编号 我们知道C1内部使用的是一种图结构的HIR,它由基本块构成一个图,然后每个基本块里面是SSA形式的指令,关于这点如可以参考[Inside HotSpot] C1编译器工作流程及中间表示.值编号(Value numbering)是指为每个计算得到的值分配一个独一无二的编号,然后遍历指令寻找可优化的机会.比如下面的代码: a = 1;b=4; c = a+b; d = a+b; e = b; 编译器可以在计算a的时候为它指定一个hash值(0x12a3e)然后放入hash表:b同理指定

JVM - JIT编译器

对效率的追求是程序的天生信仰 - JVM在不断的追求效率 1. 什么是Just In Time编译器? 在主流商用JVM(HotSpot.J9)中,Java程序一开始是通过解释器(Interpreter)进行解释执行的.当JVM发现某个方法或代码块运行特别频繁时,就会把这些代码认定为"热点代码(Hot Spot Code)",然后JVM会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为:即时编译器(Just In Time Compiler,JIT

浅谈对JIT编译器的理解。

1. 什么是Just In Time编译器? Hot Spot 编译 当 JVM 执行代码时,它并不立即开始编译代码.这主要有两个原因: 首先,如果这段代码本身在将来只会被执行一次,那么从本质上看,编译就是在浪费精力.因为将代码翻译成 java 字节码相对于编译这段代码并执行代码来说,要快很多. 当 然,如果一段代码频繁的调用方法,或是一个循环,也就是这段代码被多次执行,那么编译就非常值得了.因此,编译器具有的这种权衡能力会首先执行解释后的代 码,然后再去分辨哪些方法会被频繁调用来保证其本身的编

Java性能优化指南系列(三):理解JIT编译器

即时编译器概述 编译器在编译过程中通常会考虑很多因素.比如:汇编指令的顺序.假设我们要将两个寄存器的值进行相加,执行这个操作一般只需要一个CPU周期:但是在相加之前需要将数据从内存读到寄存器中,这个操作是需要多个CPU周期的.编译器一般可以做到,先启动数据加载操作,然后执行其它指令,等数据加载完成后,再执行相加操作.由于解释器在解释执行的过程中,每次只能看到一行代码,所以很难生成上述这样的高效指令序列.而编译器可以事先看到所有代码,因此,一般来说,解释性代码比编译性代码要慢.不过,解释性代码具有

JVM性能优化, Part 2 ―― 编译器

作为JVM性能优化系列文章的第2篇,本文将着重介绍Java编译器,此外还将对JIT编译器常用的一些优化措施进行讨论(参见"JVM性能优化,Part 1″中对JVM的介绍).Eva Andreasson将对不同种类的编译器做介绍,并比较客户端.服务器端和层次编译产生的编译结果在性能上的区别,此外将对通用的JVM优化做介绍,包括死代码剔除.内联以及循环优化. Java编译器存在是Java编程语言能独立于平台的根本原因.软件开发者可以尽全力编写程序,然后由Java编译器将源代码编译为针对于特定平台的高

Java调优

Java调优经验谈 对于调优这个事情来说,一般就是三个过程: 性能监控:问题没有发生,你并不知道你需要调优什么?此时需要一些系统.应用的监控工具来发现问题. 性能分析:问题已经发生,但是你并不知道问题到底出在哪里.此时就需要使用工具.经验对系统.应用进行瓶颈分析,以求定位到问题原因. 性能调优:经过上一步的分析定位到了问题所在,需要对问题进行解决,使用代码.配置等手段进行优化. Java调优也不外乎这三步. 此外,本文所讲的性能分析.调优等是抛开以下因素的: 系统底层环境:硬件.操作系统等 数据

JVM-程序编译与代码晚期(运行期)优化

晚期(运行期)优化 1.为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time,JIT编译器). 2.Hotspot虚拟机内的即时编译器 (1)解释器与编译器 主流的商用虚拟机,如Hotspot,J9等,都同时包含解释器和编译器. 解释器与编译器两者各有优势:当程序需要快速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行.在程序运行后,随着时间得推移,编译器组件发