C-编译器的实现
写这个编译器的目的,是为了完成编译原理课上老师布置的大作业,实际上该大作业并不是真的实现一个编译器,而我选择硬刚,是为了完成我的小愿望--手写内核,编译器和CPU。我花了整个上半学期,写完了WeiOS,为了让它支持更多的用户态程序,甚至是基本的程序开发,必须给它量身打造一个编译器。于是这个编译器被提上日程。
因为我要复习考研和专业课过多,我打消了手写词法分析和语法分析的念头,转而使用FLEX和YACC,等到有时间再完成手工的版本--我认为也不是很难,如果用递归下降的话。
全部代码都在该处 https://github.com/mynamevhinf/CMinusCompiler
词法分析
因为是精简的C语言,所以只支持基本的符号(Token),像”++”, “--”和位操作都不予考虑。另外,只支持int和void类型。于是便构成了以下符号集:
number [1-9][0-9]*
letter [a-zA-Z]
Identifier {letter}({letter}|{number})*
Newline \n
whitespace [ \t\r]+
保留字:
If else while int void return
运算和界限符号:
<= >= != == + - * / < > ; , ( ) { } [ ]
主体函数是getToken(),这个函数封装了FLEX原生的yylex函数。而yyparser也将直接调用该函数。它的主要工作是在开始第一次词的检索前,初始化相关变量。然后在每次被调用的时候,返回符号的类型给yyparser,并且把构成符号的字符串临时保存在tokenString字符数组中。所以这函数相当于什么事情都没有干。
另外注意的是注释的两个符号,我直接在词法分析处理注释了。行注释是”//”,利用FLEX自带的input()函数(如果有的话,没有就写一个)一直读到’\n’出现。然后就是段注释符”/*”和”*/”,相似的做法。
语法分析
以下是BNF格式的语法规则:
Program -> declaration_list
declaration_list -> declaration_list declaration | declaration
declaration -> var_declaration | func_declaration
type_specifier -> INT | VOID
var_declaration -> type_specifier VARIABLE ; | type_specifier VARIABLE [ NUM ] ;
func_declaration -> type_specifier VARIABLE ( params ) compound_stmt
Params -> params_list | VOID
params_list -> params_list , param | param
Param -> type_specifier VARIABLE | type_specifier VARIABLE [ ]
compound_stmt -> { local_declarations stmt_list }
local_declarations -> local_declarations var_declaration | /* empty */
stmt_list ->stmt_list stmt | stmt
Stmt -> expr_stmt | if_stmt | return_stmt | compound_stmt | iteration_stmt
expr_stmt -> expr ; | ;
if_stmt ->IF ( expr ) stmt | IF ( expr ) stmt ELSE stmt
iteration_stmt -> WHILE ( expr ) stmt
return_stmt ->RET ; | RET expr ;
Expr -> var = expr | simple_expr
Var -> VARIABLE | VARIABLE [ NUM ]
Call -> VARIABLE ( args )
Args -> arg_list | /* empty */
arg_list -> arg_list , expr | expr
simple_expr -> NUM | var | call | ( expr ) | - simple_expr
| simple_expr + simple_expr
| simple_expr - simple_expr
| simple_expr * simple_expr
| simple_expr / simple_expr
| simple_expr < simple_expr
| simple_expr > simple_expr
| simple_expr >=simple_expr
| simple_expr <= simple_expr
| simple_expr != simple_expr
| simple_expr == simple_expr
我用globl.h中的TreeNode结构来保存语法树中的每一个节点。而一些为空的转换,我打算还是用一个该结构来表示,但是类型标记为None(也许有点浪费内存).
我实现的C-还算是个比较完整的程序语言,所以很有必要生成AST(抽象语法树),那么语法树中共有几种类型的节点呢?按理说应每种语法规则对应一种类型,例如参数列表,声明语句,普通语句和表达式等都对应一个节点类型,详细可以参见NodeType枚举类型。Parser.c文件是处理与语法树相关的函数,目前来说当中几个函数还没写清楚,TreeNode需要大改一下我估计,过几天也许就明了了。--2018/05/29
2018/05/30
没想到只过了一天不到,就完成了语法分析部分,总体上来说还是很简单的。
有些语法规则导出两种相似的子规则,用专业术语来讲就是就是要做左因子消除,但好像yacc已经代替我们做了这个工作--我猜测它在底下优化了我们混乱的语法规则,包括左递归也解决了。据来说就是if_stmt导出的两种情况,我并不打算在StmtType枚举中添加新的枚举类型来处理,而是利用原有的结构,用nkids来辨别是哪种情况。而在处理var_declaration第二种导出的规则时,原有的结构不够用了,因为我要存储VARIABLE和NUM,很显然一个attr联合体不够用,所以我引入了第二个联合体分别来存储两个值。分别叫attrA和attrB。有些时候这样做也无法解决结构上的问题,才不得不用两个枚举类型来解决。现在我也不确定这样做是否多余,毕竟我也是第一次写编译器,但它确实解决了当下的问题。
我删除了封装yylex()的getToken()函数,并解决了注释代码的问题,现在可以支持/**/的段注释和//行注释了。另外,我不想让我代码变得冗余,所以我把构建符号表(Symbol table)的任务放在了语义分析,直接放在语法分析中虽然节约了时间,但实在是难以维护。
最后根目录下source*.c都成功地进行了语法分析,可能也只是表面上...
语义分析还没学,翻书去了...
2018/06/02
经过两天的学习,我知道了符号表基本的构建方式和运行时环境的搭建。但是还来不及完全转换成代码。今天我抽了一点时间写了符号表的管理函数。我的编译器有多级符号表:一个全局符号表以及每个函数一个局部符号表。在全局符号表中,每个属于函数的变量名,都有一个指针指向其局部符号表。而局部符号表通过Parent字段与父符号表相连,在当前符号表无法检索到符号信息的时候,就会去搜寻父符号表,层层递增。由于C语言本身的规定,和我的懒惰,我不支持动态作用域,也就是说对一切外部(相对与当前代码块)的符号引用而言,其偏移和效果都是固定的;也不支持函数声明嵌套,也就是说对于任何局部符号表,其父符号表都指向全局符号表。因此我觉得这样简单的实现方式是可行的。
符号表的基本数据结构是哈希表。具体实现不做阐述,我只写了插入和寻找的函数,因为这个烂编译器暂时不支持类型声明和类型别名,所以我没有写删除。
这里留下一个问题,除了全局声明函数,代码块--也就是被{}包围起来的部分,也需要局部符号表,而且是支持嵌套的。那么我该如何解决这种相当于“匿名”函数的符号表问题,我还没有思路。或许可以给它们各自赋予决不会引起冲突的名字?又或者是留到代码生成阶段再处理,这样就不必缓存“匿名”符号表了,直接生成代码就完事了。
主要代码都在symbol.c 和 symbol.h里面。
2018/06/07
经过几天的咸鱼和跟进,我完成了类型检查和符号表生成,中间代码生成。
关于类型检查,是完全仿造gcc的,检查顺序是后序遍历,例如表达式:
a = test() + b;
我们必须先检查test()是否有返回值,然后b是什么类型的数据,如果test()返回值是void,那么这个语句就可以直接判断为错误了。若两个加数通过检查,而a是数组类型的话,也会报错。
由上所述,类型检查必须是后序遍历,而我生成符号表的时候采取先序遍历,这里就产生了矛盾。解决的方法是,我对语法树中的表达式(expr)类型的节点,通通执行后续遍历,而其他如If语句,while语句等采取常规做法。原因是我在表达式的语句中,不可能出现声明新函数或者变量的语句,反而是要检查所用的变量和函数在之前是否声明,如果没有就报错,因此后序遍历是可行的。
也就是说我的buildSymtab()函数不是安装树状递归展开的,而且名不副实,因为它除开生成符号表,还同时完成了类型检查工作。其实我是想把这两个步骤分开的,用2-pass来完成,代码也比较精简,想砍掉哪部分也很方便。但由于当时写的太爽了,不想再写类似于buildSymtab()这样结构的函数了,所以直接做完了。
上一篇遗留的问题,匿名代码块怎么办?我的处理方法是为它们生成单独的符号表,并且在语法分析阶段,调用randomFuncName()函数为每一个匿名代码块生成一个名字,类似真正的函数那样管理,这个名字不是真正随机的,因为它是AAAnonymous*的形式,但无所谓了,反正这编译器也烂。
具体的代码在analyzer.c里,相关结构的定义在analyzer.h中。
接下来是一张类型检查的贴图: 左边是我的fcc,右边是gcc,错误和警告都检测出来了,但是他们比我提示信息更加多...
运行时环境(run-time):
当然是选择和C语言一样啊,没有什么好说的。
要提的一点还是匿名代码块的问题,对于这种代码块中声明和定义的“局部变量”,我观察了gcc的做法,他们把这些“局部变量”当作了其所属的函数的局部变量,举个例子:
void test()
{
int j;
{
int i;
}
}
那么在函数test()为局部变量分配空间的时候,不是分配4个字节,而是8个字节。所以我也这么干。因为语法的原因,函数体本身声明局部变量肯定在匿名代码块之前,故当我们处理到匿名代码块的时候,必须要返回去更改所属函数的局部变量信息(用于代码生辰的时候分配具体空间)。之前我的FuncProperty结构并没有维护参数个数和局部变量大小的信息,所以我做了一些更改,添加了nparams和nlocalv域。
中间代码
本来我都不打算写中间代码,直接生成x86的汇编代码,但老师说不给我分,我就写了。然后我看了一下vsl的中间代码,我很郁闷遂决定自己创造一套新的三地址码,我觉得还挺有意思的,我甚至都考虑用这套三地址码写一个虚拟机,直接在虚拟机上面跑。。。
折中了一下,我的这套三地址码已经非常像x86的汇编代码了,除了一些没用的临时寄存器。其对于我来说,最大的作用就是把树形的信息,转换成了链状的,接下来顺着三地址码构成的链表翻译就好了。相关代码见irgen.c而数据结构等在irgen.h.下面是一个源代码文件(source2.c)以及其生成的三地址码(midcode.f)的部分。
source2.c:
int jb;
int x[]; /*
void test(void)
{
}
*/ int minloc(int a[], int low, int high)
{
int i; int x; int k; k = low;
x = a[low];
i = low + i; /*
k = a;
ok = 10;
i = a[test()];
*/
while (i < high) {
if (a[i] < x) {
x = a[i];
k = i;
}
i = i + ;
}
return k;
} void sort(int a[], int low, int high)
{
int i;
int k;
midcode.f:
program:
glob_var jb
glob_var x _minloc:
loc_var
k = low
mull t0 low
addl t1 &a t0
x = t1
addl t2 low i
i = t2
L0:
lt t3 i high
if_false t3 goto L1
mull t4 i
addl t5 &a t4
lt t6 t5 x
if_false t6 goto L2
mull t7 i
addl t8 &a t7
x = t8
k = i
L2:
addl t9 i
i = t9
goto L0
L1:
ret k _sort:
loc_var
i = low
L3:
subl t10 high
lt t11 i t10
if_false t11 goto L4
loc_var
begin_args
可以看到真的是一一对应的,虽然还有个小Bug,但是我懒得管了。
2018/06/11
我的寄存器分配函数写得有点混乱,当然如果我不想做那些奇奇怪怪的优化,就不会存在这个问题。我写得很难受,但也比较享受。我现在是考虑把对中间变量的处理也统一起来,但是还没有思路...
isReside():判断一个地址是否驻留在寄存器中,如果该地址类型为TempPtr或TempReg,
regWriteBack(RegPtr Rp, RegPtr Rbp,FILE *fp): 把Rp寄存器的内容写会所属变量的内存地址。一定要先判断Hp是否为空,若为空就表示当前存了一个中间变量!!!! 如果不为空,但是没被修改过--就是dirty为0,也没必要写回去!!!
regReadIn(RegPtr Address *addr, FILE *fp): 把地址addr所属的变量的值读到到Rp指向的寄存器中,并更新相关内容.如果该变量/中间变量已存在寄存器中,则调用regMoveReg()直接从原寄存器移动.如果不在,则根据addr的类型采取不同的操作.我暂时处理了一般变量和常数的读取。
regBindVar(RegPtr Rp, Address *addr, HashList Hp, int dirty):如函数名,把addr,Hp,dirty赋给寄存器Rp.函数体内检测是否有其他寄存器已保存了该变量,调用FreeReg()把其addr和Hp域清空.
RegPtr regSearchOne(HashList Hp, FILE *fp):为第一个操作数搜寻合适的寄存器.我判断的是addr这个域,但感觉好像不够圆满...
2018/06/14
懒得写了,终于完成了代码生成,我自己写的那套寄存器分配,也很好用,甚至超过了vsl作者们写的算法= =
但是遗留了一个问题,就是我在代码生成的时候,忘记考虑到代码重入性的问题了,在生成循环代码块的时候,根据前文的数据流生成了代码,本意是尽量减少内存存取的次数,但是我没有考虑到,在真正运行的过程中,每次循环开始的时候,寄存器中存储的信息不一定是“第一次"进入循环块时的信息,因此我生成的代码有语义错误,只有偶尔情况下才是正确的。我不打算改这个bug了,老师说这个涉及到龙书上讲解的数据流分析,但我还要考研,所以推迟了吧,等真正需要的时候再搞这个。
下面是测试代码,就是一个简单的选择排序 source2.c
/* A program to perform selection sort on a 10
element array. */ /*
void test(void)
{
}
*/ int minloc(int a[], int low, int high)
{
int i; int x; int k; k = low;
x = a[low];
i = low + i; while (i < high) {
if (a[i] < x) {
x = a[i];
k = i;
}
i = i + 1;
}
return k;
} void sort(int a[], int low, int high)
{
int i;
int k; i = low;
while (i < high - 1) {
int t;
k = minloc(a, i, high);
t = a[k];
a[k] = a[i];
a[i] = t;
i = i + 1;
}
}
我生成的汇编代码 source2.s:
.file "testfile/source2.c"
.text
.globl minloc
.type minloc, @function
minloc:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 12(%ebp), %eax
movl %eax, -12(%ebp)
movl 12(%ebp), %edx
leal 0(,%edx,4), %ebx
movl 8(%ebp), %ecx
addl %ecx, %ebx
movl (%ebx), %ebx
movl %ebx, -8(%ebp)
movl -4(%ebp), %eax
addl %eax, %edx
movl %edx, -4(%ebp)
.L0:
cmpl 16(%ebp), %edx
jge .L1
leal 0(,%edx,4), %eax
addl %ecx, %eax
movl (%eax), %eax
cmpl -8(%ebp), %eax
jge .L2
leal 0(,%edx,4), %ebx
addl %ecx, %ebx
movl (%ebx), %ebx
movl %ebx, -8(%ebp)
movl %edx, -12(%ebp)
.L2:
movl -4(%ebp), %edx
addl $1, %edx
movl %edx, -4(%ebp)
jmp .L0
.L1:
movl -12(%ebp), %eax
leave
ret .globl sort
.type sort, @function
sort:
pushl %ebp
movl %esp, %ebp
subl $16, %esp
movl 12(%ebp), %eax
movl %eax, -4(%ebp)
.L3:
movl 16(%ebp), %edx
subl $1, %edx
cmpl %edx, %eax
jge .L4
movl 16(%ebp), %ebx
pushl %ebx
pushl %eax
movl 8(%ebp), %ecx
pushl %ecx
call minloc
addl $12, %esp
movl %eax, -8(%ebp)
leal 0(,%eax,4), %edx
addl %ecx, %edx
movl (%edx), %edx
movl %edx, -12(%ebp)
movl -4(%ebp), %eax
leal 0(,%eax,4), %edx
addl %ecx, %edx
movl -8(%ebp), %eax
leal 0(,%eax,4), %ebx
addl %ecx, %ebx
movl (%edx), %edx
movl %edx, (%ebx)
movl -4(%ebp), %eax
leal 0(,%eax,4), %ecx
movl 8(%ebp), %eax
addl %eax, %ecx
movl -12(%ebp), %eax
movl %eax, (%ecx)
movl -4(%ebp), %eax
addl $1, %eax
movl %eax, -4(%ebp)
jmp .L3
.L4:
leave
ret
最后上测试结果,难得的正确了一次:
2018/06/24
本来不打算写了,但是在满分的激励下我做了以下修改,并最终解决了上面的Bug
1.被调用函数保存可能改变的所有寄存器。
2.实现循环代码的语义正确(其实就是保证了可重入性)。
当然还有一个问题就是,栈内容初始化。因为物理页面分配的不确定性,因此会留下上一个进程的数据,所以必须在程序一开始对可能用到的栈空间进行初始化。严格来说,这并不是我的问题。
最后附上一张成功运行的图:
C-编译器的实现的更多相关文章
- python开发编译器
引言 最近刚刚用python写完了一个解析protobuf文件的简单编译器,深感ply实现词法分析和语法分析的简洁方便.乘着余热未过,头脑清醒,记下一点总结和心得,方便各位pythoner参考使用. ...
- 使用 Roslyn 编译器服务
.NET Core和 .NET 4.6中 的C# 6/7 中的编译器Roslyn 一个重要的特性就是"Compiler as a Service",简单的讲,就是就是将编译器开放为 ...
- 编译器开发系列--Ocelot语言7.中间代码
Ocelot的中间代码是仿照国外编译器相关图书Modern Compiler Implementation 中所使用的名为Tree 的中间代码设计的.顾名思义,Tree 是一种树形结构,其特征是简单, ...
- 编译器开发系列--Ocelot语言1.抽象语法树
从今天开始研究开发自己的编程语言Ocelot,从<自制编译器>出发,然后再自己不断完善功能并优化. 编译器前端简单,就不深入研究了,直接用现成的一款工具叫JavaCC,它可以生成抽象语法树 ...
- 从Unity3D编译器升级聊起Mono
接前篇Unity 5.3.5p8 C#编译器升级,本文侧重了解一些Mono的知识. Unity3D的编译器升级 新升级的Mono C#编译器(对应Mono 4.4) Unity编辑器及播放器所使用的M ...
- 基于虎书实现LALR(1)分析并生成GLSL编译器前端代码(C#)
基于虎书实现LALR(1)分析并生成GLSL编译器前端代码(C#) 为了完美解析GLSL源码,获取其中的信息(都有哪些in/out/uniform等),我决定做个GLSL编译器的前端(以后简称编译器或 ...
- 在 CSS 预编译器之后:PostCSS
提到css预编译器(css preprocessor),你可能想到Sass.Less以及Stylus.而本文要介绍的PostCSS,正是一个这样的工具:css预编译器可以做到的事,它同样可以做到. “ ...
- 匹夫细说C#:庖丁解牛聊委托,那些编译器藏的和U3D给的
0x00 前言 由于工作繁忙所以距离上一篇博客已经过去一个多月的时间了,因此决心这个周末无论如何也得写点东西出来,既是总结也是分享.那么本文主要的内容集中在了委托的使用以及内部结构(当然还有事件了,但 ...
- Keil> 编译器特有的功能 > 关键字和运算符 > __weak
__weak 此关键字指示编译器弱导出符号. 可以将 __weak 关键字应用于函数和变量声明以及函数定义. 用法 函数和变量声明 对于声明,此存储类指定一个 extern 对象声明,即使不存在,也不 ...
- 明显调用的表达式前的括号必须具有(指针)函数类型 编译器错误 C2064
看到“明显调用的表达式前的括号必须具有(指针)函数类型”这句时我才发现我的语文水平有多烂,怎么看都看不懂,折腾了半天才知道是哪里出了问题. 举个简单的例子 class CTest { void (CT ...
随机推荐
- Java实例---简单的上课管理系统
源码分析 Course.java package com.ftl.many2many; import java.util.*; public class Course { private int cr ...
- 第六次作业——Excel制作工资表
- UNIX crontab自动执行脚本
crontab 是不会加载环境变量的你手动执行可以是因为登录了oracle用户加载了环境变量,你可以在脚本里添加,echo $ORACLE_HOME echo $ORACLE_SID 等等试试,看看 ...
- 为OS X增加环境变量
1.创建并以 TextEdit 的方式打开 ~/.bash_profile 文件 touch ~/.bash_profile; open -t ~/.bash_profile 2.新增环境变量 exp ...
- JavaScript的DOM_通过元素的class属性操作样式
使用 style 属性可以设置行内的 CSS 样式,而通过 id 和 class 调用是最常用的方法. <script type="text/javascript"> ...
- 安装TA-Lib时报错:ubuntu****, Command "/usr/bin/python -u -c "import setuptools, tokenize;__file__='
使用pip install TA-Lib 时报错: ERROR: Complete output from command /usr/bin/python3 -u -c 'import setupto ...
- 初始化列表initializer_list
初始化列表定义在<initializer_list>,初始化列表简化了参数数量可变的函数的编写,初始化列表的所有的元素都应该是同一种数据类型 由于定义了列表中允许的类型,所以初始化列表是安 ...
- Nexus修改admin密码及其添加用户
Nexus之所以修改密码,是为了安全起见,个人学习的话,本地windows或者虚拟机即可,外网服务器建议将密码修改复杂点,而且强烈建议端口不要8081,最好将其改为其他的.同样也是为了安全起见. 添加 ...
- Dubbo实践(十六)集群容错
Dubbo作为一个分布式的服务治理框架,提供了集群部署,路由,软负载均衡及容错机制.下图描述了Dubbo调用过程中的对于集群,负载等的调用关系: 集群 Cluster 将Directory中的多个In ...
- LeetCode40.组合总和|| JavaScript
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合. candidates 中的每个数字在每个组合中只能使用一次. ...