zoukankan      html  css  js  c++  java
  • C-编译器的实现

      写这个编译器的目的,是为了完成编译原理课上老师布置的大作业,实际上该大作业并不是真的实现一个编译器,而我选择硬刚,是为了完成我的小愿望--手写内核,编译器和CPU。我花了整个上半学期,写完了WeiOS,为了让它支持更多的用户态程序,甚至是基本的程序开发,必须给它量身打造一个编译器。于是这个编译器被提上日程。

      因为我要复习考研和专业课过多,我打消了手写词法分析和语法分析的念头,转而使用FLEXYACC,等到有时间再完成手工的版本--我认为也不是很难,如果用递归下降的话。

      全部代码都在该处 https://github.com/mynamevhinf/CMinusCompiler

    词法分析

      因为是精简的C语言,所以只支持基本的符号(Token),像”++”, “--”和位操作都不予考虑。另外,只支持intvoid类型。于是便构成了以下符号集:

      number   [1-9][0-9]*

      letter     [a-zA-Z]

      Identifier  {letter}({letter}|{number})*

      Newline   

      whitespace [ ]+

    保留字:

      If else while int void return

    运算和界限符号:

      <= >= != == + - * / < > ; , ( ) { } [ ]

    主体函数是getToken(),这个函数封装了FLEX原生的yylex函数。而yyparser也将直接调用该函数。它的主要工作是在开始第一次词的检索前,初始化相关变量。然后在每次被调用的时候,返回符号的类型给yyparser,并且把构成符号的字符串临时保存在tokenString字符数组中。所以这函数相当于什么事情都没有干。

    另外注意的是注释的两个符号,我直接在词法分析处理注释了。行注释是”//”,利用FLEX自带的input()函数(如果有的话,没有就写一个)一直读到’ ’出现。然后就是段注释符”/*”和”*/”,相似的做法。

    语法分析

      以下是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联合体不够用,所以我引入了第二个联合体分别来存储两个值。分别叫attrAattrB。有些时候这样做也无法解决结构上的问题,才不得不用两个枚举类型来解决。现在我也不确定这样做是否多余,毕竟我也是第一次写编译器,但它确实解决了当下的问题。

      我删除了封装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结构并没有维护参数个数和局部变量大小的信息,所以我做了一些更改,添加了nparamsnlocalv域。

      中间代码

      本来我都不打算写中间代码,直接生成x86的汇编代码,但老师说不给我分,我就写了。然后我看了一下vsl的中间代码,我很郁闷遂决定自己创造一套新的三地址码,我觉得还挺有意思的,我甚至都考虑用这套三地址码写一个虚拟机,直接在虚拟机上面跑。。。

      折中了一下,我的这套三地址码已经非常像x86的汇编代码了,除了一些没用的临时寄存器。其对于我来说,最大的作用就是把树形的信息,转换成了链状的,接下来顺着三地址码构成的链表翻译就好了。相关代码见irgen.c而数据结构等在irgen.h.下面是一个源代码文件(source2.c)以及其生成的三地址码(midcode.f)的部分。

    source2.c:

    int jb;
    int x[10];
    
    /*
    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 + 1;
        }
        return k;
    }   
    
    void sort(int a[], int low, int high)
    {
        int i;
        int k;

    midcode.f:

     1 program:
     2     glob_var jb 4
     3     glob_var x 40
     4 
     5 _minloc:
     6     loc_var 16
     7     k = low 
     8     mull t0 low 4 
     9     addl t1 &a t0 
    10     x = t1 
    11     addl t2 low i 
    12     i = t2 
    13 L0:
    14     lt t3 i high 
    15     if_false t3 goto L1
    16     mull t4 i 4 
    17     addl t5 &a t4 
    18     lt t6 t5 x 
    19     if_false t6 goto L2
    20     mull t7 i 4 
    21     addl t8 &a t7 
    22     x = t8 
    23     k = i 
    24 L2:
    25     addl t9 i 1 
    26     i = t9 
    27     goto L0
    28 L1:
    29     ret k 
    30 
    31 _sort:
    32     loc_var 16
    33     i = low 
    34 L3:
    35     subl t10 high 1 
    36     lt t11 i t10 
    37     if_false t11 goto L4
    38     loc_var 16
    39     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.实现循环代码的语义正确(其实就是保证了可重入性)。

      当然还有一个问题就是,栈内容初始化。因为物理页面分配的不确定性,因此会留下上一个进程的数据,所以必须在程序一开始对可能用到的栈空间进行初始化。严格来说,这并不是我的问题。

      最后附上一张成功运行的图:

      

  • 相关阅读:
    feign远程调用问题
    java8--stream
    feign业务组件远程请求 /oauth/token
    redis实现自增序列
    MySQL数据库 相关知识点
    netty
    spring的启动流程及bean的生命周期
    MethodHandleVS反射
    并发与并行
    关于注解的思考
  • 原文地址:https://www.cnblogs.com/yzwqf/p/9114135.html
Copyright © 2011-2022 走看看