发布一个简单的C语言编译器(含虚拟机)

2020-01-13 18:33发布

本帖最后由 lcw_swust 于 2013-6-1 11:13 编辑

本程序由一个解释器修改而来,由于需要较大内存,目前只是在KEIL下软件仿真运行
上程序:
TC编译器V1.1.rar (158.74 KB, 下载次数: 344) 2013-6-1 11:12 上传 点击文件名下载附件
/*--------------------------------------------------
TC编译器说明

目标:
                单片机中运行编译器,能编译简单的C语言程序,生成汇编
        代码,再将汇编代码转为虚拟机的机器码,再让虚拟机执行机器码。
        关于简单的C语言程序:
                实现if,while,一维数组,API函数调用。       
                数据类型 (signed/unsigned)charintlong (*)
--------------------------------------------------
虚拟机结构:
        寄存器(全32位):r0~rf(共16个)
        内存: 自定义
寄存器使用规则:
        函数入口用r0、r1、r2、r3传递
        函数返回用r4,计算结果也用r4
        计算使用寄存器r4、r5、r6、r7
        Rc用于进入函数时保存Re,便于计算局部变量的偏移量
        Rd用于指针查表(相当于x指针,用于ld、st指令)
        Re用于申请变量空间(相当于y指针)
        Rf用于堆栈(相当于sp,用于push、pop、call、ret指令)
        其余寄存器备用

虚拟机指令集:
        算术指令:
                (全是寄存器作为操作数)
                neg(~),not(!),add(+),sub(-),mul(*),div(/),mod(%)
                and(&),or(|),xor(^),grt(>),les(<),sl(<<),sr(>>)
                land(&&),lor(||),nless(>=),ngrt(<=),neq(!=),equ(==)
       
        跳转指令:
                ret,jmp,call,
                je        (r4=0则跳转)
                jne        (r4!=0则跳转)
        数据传送:
                mov         r,r        (寄存器复制)
                ldi                r,i        (加载立即数)
                //ld系列
                ldu8        r         (r=*(U8*)rd,从RAM或ROM加载无符号8位数)
                lds8        r         (r=*(S8*)rd,从RAM或ROM加载有符号8位数)
                ldu16        r         (r=*(U16*)rd,从RAM或ROM加载无符号16位数)
                lds16        r         (r=*(S16*)rd,从RAM或ROM加载有符号16位数)
                ldu32        r         (r=*(U32*)rd,从RAM或ROM加载无符号32位数)
                lds32        r         (r=*(S32*)rd,从RAM或ROM加载有符号32位数)
                //st系列
                stu8        r         (*(U8*)rd=r,向内存写入无符号8位数)
                sts8        r         (*(S8*)rd=r,向内存写入有符号8位数)
                stu16        r         (*(U16*)rd=r,向内存写入无符号16位数)
                sts16        r         (*(S16*)rd=r,向内存写入有符号16位数)
                stu32        r         (*(U32*)rd=r,向内存写入无符号32位数)
                sts32        r         (*(S32*)rd=r,向内存写入有符号32位数)
                push        r        (sp-=4,r=*sp,sp即rf)
                pop                r        (r=*sp,sp+=4,)
--------------------------------------------------
C程序转为汇编代码的过程:
(参考TCCMP.C文件)
1.编译中使用的缓冲区
  缓冲区先是被分为独立的3段,普通的代码放入第1段,定义数组时,
  数组首地址(标号)赋给第1段中的变量,数组元素的值放入第2段,
  如果数组元素是双引号字符串,则将双引号字符串首地址(标号)
  放入第2段,字符串放入第3段。
  这些参考AddAsmCode1、AddAsmCode2、AddAsmCode3
  当然,在写入时有可能出现第1段装不下,写到第2段去的情况,
  这时就需要,加大第1段,将第2、3段往右移动(参考moveasm2、moveasm3)。
  当然,如果编译器运行环境的缓冲区足够,则可以不用这么麻烦,
  直接定义三个缓冲区就是了。
  最后,将3段连接起来变一个字符串,用于Link

2.变量定义
  (参考addvar函数)
  假如一个变量占两字节,则只需让re增加2:

  ldi r4,2
  add re,r4        //re=re+2

3.读取变量
  (参考getvar_id函数)
  mov rd,rc        //rc=rd=进入函数时保存的局部变量的RAM区首地址
  ldi r4,2        //r4=该局部变量在局部变量缓冲区中的偏移地址
  add rd,r4 //rd=该变量的绝对地址
  ldu8 r4        //r4=*(U8*)rd,读到该变量的值,根据变量的类型,ldu8可能为lds8ldu16lds16等

4.写变量
  (参考setvar函数)
  mov rd,rc
  ldi r5,2
  add rd,r5 //rd=该变量的绝对地址
  stu8 r4   //*(U8*)rd=r4

5.数组
  (参考vardef1内state==10的处理)
  定义数组赋初值时,则生成一个标号,标号的值赋给该数组变量,然后将数组元素放入另一code段
  char s[]="123";
  可能被编译成
          code第1段----------
          ......
          ldi r4,lab001  //取数组地址
          stu32 r4                //变量赋值
          code第2段----------
          lab001:
          arrs8 049,050,051,
  long s[]={"123","456"};

  可能被编译成
          code第1段----------
          ......
          ldi r4,lab001  //取数组地址
          sts32 r4                //变量赋值
          code第2段----------
          lab001:
          arru32 lab002,lab003,
          code第3段----------
          lab002:
          arrs8 049,050,051,
          lab003:
          arrs8 052,053,054,

6.算式
  (参考getexp及getexp1)
        getexp是将整个算式的值求出,x结果放入r4,
                例如 a+b*c
        getexp1只是求出算式中的一个参数,并放入r4,就像上面的ac

        举个例子,要计算 1+2
        调用getexp(),
        getexp()内先调用getexp1,
        得到代码:
                ldi r4,1    //r4=1
        getexp()再调用GetToken,取得+号
        得到代码:
                push r4                //保存r4

                调用getexp1得到:
                ldi r4,2   //r4=加号右方的2

                mov r5,r4  //r5=r4=2
                pop r4           //r4=加号左方的1

                调用cacu得到:
                add r4,r5  //r4=r4+r5=1+2

简单总结:
    本编译器将C转成汇编代码基本上是按照固定规律来的,
        所以可能会有些冗余,目前未作优化
......
--------------------------------------------------
汇编转为机器码的过程:
(参考Link.C文件)

调用 Link_GetToken,
如果得到的不是以冒号结尾的标号,则当成是指令,
然后查表将Token字符串转为指令码,再根据指令码的
类型获取指令之后的参数,如果参数中有标号,则查找
标号(Link_getcst)。
--------------------------------------------------
机器码的执行过程:
(参考VM.C文件)
这个相对简单,读取一字节的机器码之后,根据指令类
型进行处理,对寄存器、RAM等进行读写,以及调用某些
API函数。
--------------------------------------------------
一些注意事项及BUG:
  对于运算式的处理,是按从左到右来的,没有优先级之分
  有括号的地方会作为单独的运算式处理
  例如
          1+2*3        等价于(1+2)*3
          1+(2*3)        等价于1+(2*3)

  不要在循环内定义参数
  函数传递参数的个数以及返回参数没有严格检查
  定义变量赋初值时不能取变量值,可以取变量地址
  比如int a=&b;是可以的,int a=b;就不行


//--------------------------------------------------*/


程序运行截图
5.jpg (148.92 KB, 下载次数: 1) 下载附件 2013-6-1 11:11 上传
友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。
55条回答
ilikemcu
2020-01-16 02:47
ilikemcu 发表于 2013-6-2 09:47
很厉害了,只是看到那18KB多的XRAM需求,心里一下哇凉瓦良的了,这样的51不好找啊,外扩SRAM就更麻烦了。 ...

的确,编译器需要较大的RAM,可以将编译器程序移植到VC或VB下,只让单片机运行虚拟机。

要在小型单片机上运行编译器,我暂时想到两个办法:
(1)如果程序空间足够装下编译器代码,但内存不够,可以利用SD卡,将asmcode[]、mcode[]等存入SD卡(或其它存储设备)。
(2)如果程序空间只能装下虚拟机的代码,则可以将这个编译器的C代码用“简单C” 的语法重新写一下,(在电脑上)编译成机器码存入SD卡中,让单片机运行虚拟机,运行SD卡中的代码,就是说在虚拟机中运行编译器;
综上所述,只要单片机能运行虚拟机,配合外部存储设备(如SD卡),就能运行大型的程序(比如运行这个编译器)。(当然,速度上会受限制)

一周热门 更多>