打造自己的编译器

来源:互联网 发布:电视盒软件破解版 编辑:程序博客网 时间:2024/05/05 00:39
打造自己的编译器
汉字最好 08.12
http://writeblog.csdn.net/PostEdit.aspx?entryId=3451935
源码下载页面http://download.csdn.net/source/839828
为了更深入的了解编程的底层,就想研究下编译。
找了几本编译原理看,什么正则表达式、自动机把我都搞糊涂了。
把复杂的问题弄得更复杂,这不是我希望的。
还是自己做个东西模拟下编译过程。
任务:读入源码,输出汇编。
为什么不直接生成机器码?汇编可以直接看出编译是否错误。
xx
先设定一个语言模型:
只有运算符、标识符、数字、分号四种语法元素。
标识符都定为全局变量。
运算符只有= + - * /代表赋值、加、减、乘、整除五种运算。
数字仅仅是32位整数。
分号表示一条语句结束。
如:
aa02=15/kl+kh*kl/51;

问题一:建立一个符号表,按顺序写入必要的语法元素,
可以查询或修改符号的类型等信息。
把符号表做成一个指针数组,指向什么呢?
pp=^tx
tx=record
next:pp    //指向下个符号
lx:integer;//类型
wz:integer;//在该类型中的位置
end;
fhb:array of pp;//符号表
js:integer;      //符号表记数
原来是指向一个记录,它可以构建链表(为什么用链表?可以方便去掉
已经处理的符号),一个符号一个pp。

lx(类型)预定义四种:
1:运算符  2:标识符 3:数值 4:寄存器(储存中间结果)

运算符的wz(位置):
1:=  3:+  4:-  6:*  7:/

寄存器的位置:与sjcq对应
sjcq:array[0..5]of string=('eax','ebx','ecx','edi','esi','edx');
edx怎么放到后面去了?除法要用这个寄存器啊。
是不是还要记录寄存器被哪个符号使用?
bjcq:array [0..5]of pp; //对应的寄存器使用者

至于标识符和数值我把他放到bsf中,wz就是它们在bsf中的index。
bsf:tstringlist;
(若是fhb:array of tx 一次性申请内存,可以免去频繁的new(pp),源码里忘了释放了)

好象还缺点什么,读入源码是为了输出代码服务的,编译是一条一条语句
编译的,所以还要记录一条语句是从符号表哪里开始哪里结束。
yj=record     //语句
qd,zd:integer;//起点,终点
end;
yjz:array[0..100]of yj; //语句组
yjzjs:integer;          //语句组记数

这样读取源码建立符号表、语句组应该没有难度了,详见procedure dq;

问题二、判断运算优先级
现在尝试处理一条语句,也就是fhb[qd]到fhb[zd]的一段链表。
我们要做的就是遍历这段链表,找出最先运算的运算符,
实际上运算符的位置就是优先级;
var
xp,xq,last,qm,hm:pp;
x,y,z1,z2:integer;

xp:=fhb[qd]; x:=-1;xq:=nil;
while xq<>fhb[zd].next do //不能超过一条语句终点
begin
  if xp^.lx=1 then  //如果是运算符
     begin
      if xp^.wz<=x+1 then break;  //判断运算优先
          qm:=last;      //运算符前面一个符号
         hm:=xp^.next;   //运算符后面一个符号
        x:=xp^.wz;       //x是具体哪个运算符
    end;//endif
  last:=xp;
  xq:=xp^.next;
  xp:=xq;
end;//endwhile
x+1是为了同级别运算按顺序来;
应该看明白了吧,测试下 aa=55-hk*jj/51;
x=6(乘法),qm(前面)指向hk,hm(后面)指向jj,
这样二元运算的三个关键符号都到手了。

问题三、前后都是数值编译器应该先计算
这个简单,看源码。

问题四、处理qm(前面的符号)
先看一些汇编:
sub eax,10
add eax,[s]
mul eax,ecx,7
idiv ecx
如果你对汇编有点了解的话就该知道,qm必须转成寄存器类型。
如果qm^.lx=4(寄存器)那不用处理.
如果qm^.lx=2(标识符)就要转成寄存器类型.
   qm^.wz:=newjcq(qm);//申请寄存器
   qm^.lx:=4;     //改为寄存器类型
   当然x=1(赋值运算)是不用转的
如果qm^.lx=3(数值)最麻烦,要分两种情况:
  如果x是乘,加运算,qm和hm的内容要交换;(为了优化代码)
  如果x是除,减运算,qm要转成寄存器类型;(必须这样啦)

问题五、乘法的处理
要按hm的类型分三种情况。
hm是寄存器: 生成类似代码 imul ebx
hm是数值:   生成类似代码 imul eax,ecx,5
               可以优化成 lea eax,[ecx+ecx*4](麻烦)
hm是标识符:这也分两种情况
      qm是eax,  生成类似代码 imul dword ptr [b]
      qm不是eax,生成类似代码 imul ebx,[b]

问题六、除法最麻烦(俺还是在用整除呢)
除法要求,被除数在eax中,edx不能用。(所以在sjcq中俺把edx弄到最后)
如果qm不是eax怎么办?交换。
假如qm是ecx,生成类似代码 xchg eax,ecx
eax和qm的寄存器相关也要交换,不然会出错的。
   //原eax的与qm 交换
           tp:=bjcq[0];   //取得原来使用eax的符号 
           bjcq[qm^.wz]:=tp;
           tp^.wz:=qm^.wz;
           qm^.wz:=0;
           bjcq[0]:=qm;
现在可以生成代码了。
首先 cdq (用xor edx,edx也可以,但是指令多一个字节)
hm是数值:   首先要把hm转成寄存器类型
hm是寄存器: 生成类似代码 idiv ebx
hm是标识符: 生成类似代码 idiv dword ptr [b]

问题七、赋值运算
hm是数值:   生成类似代码 mov [a],18
hm是寄存器: 生成类似代码 mov [a],eax
hm是标识符: 生成类似代码 mov eax,[b]
                          mov [a],eax

问题八、收尾工作
if hm^.lx=4 then bjcq[hm^.wz]:=nil ; // 不要的寄存器要归还
if qm=fhb[qd] then exit;    //一条语句弄完了
qm^.next:=hm^.next;      //把已经运算的从链表中删除
yx(qd,zd); //再来
用链表删除数据就是简单

测试:与delphi生成的代码对比
设 
 a:=8;
 b:=4;
 d:=10;
编译
 a:=b*5-d div 5;
当然在我的编译器中写做a=b*5-d/5;
delphi代码:
mov eax,[d]
mov ecx,$00000005
cdq
idiv ecx
mov edx,[b]
lea edx,[edx+edx*4]
sub edx,eax
mov [a],edx

我生成的代码:
mov eax ,[b]
imul eax,eax,5
mov ebx ,[d]
xchg eax,ebx
mov ecx ,5
cdq
idiv  ecx
sub ebx,eax
mov [a],ebx
经过1千万次循环delphi的用时250毫秒,我的200毫秒。
但是delphi的代码占30字节,我的占35字节
如果你在delphi中用for循环测试要小心,
for循环的循环变量会占用 ebx。
怎么处理,改下sjcq中成员的次序。

最后的问题:扩展
括号是最容易扩展的,你可以把括号当成语句。
比较运算最后要test或cmp。
……

               
      


原创粉丝点击