本帖最后由 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 上传
这个编译器是运行于单片机上的,但对RAM要求较多,估计只有STM32才能胜任,
虚拟机对RAM和ROM的要求很低,普通51单片机也能运行,所以,可以在STM32上运行编译器,编译后的机器码传给51单片机,让51去执行。
如果只想让单片机运行C语言程序,对执行速度要求不高,可以看看我的另一个帖子里的解释器,可以在一些RAM相对较大的51单片机上运行:
https://www.amobbs.com/thread-5492296-1-1.html
这里有个关于编译器与解释器的对比,很形象:
http://blog.csdn.net/touzani/article/details/1625760
我好像误会你的意思了,你是说直接编译成51单片机的机器码?
估计这工作量有点大,我没考虑过,只是想做个像JAVA那样能跨平台运行的东东。
曾经看到有坛友做了51、AVR的编译器,暂时没找到帖子。
----------
找到了,就是这位大神:
https://www.amobbs.com/forum.php?mod=viewthread&tid=5482772
一周热门 更多>