声明
这一系列“编译原理学习日记”中的所有文章为作者个人的准Weblog,所有文章版权均属作者所有。转载请注明出处(CSDN文档中心)、作者(lover_P)及作者联系方式(lyb_dotNET@hotmail.com),并在转载前与作者联系。
其实,要到下学期才开“编译原理”这门课程,可漫长的假期让人觉得有点寂寞。我终于忍不住对编译原理的向往,翻开了《现代编译程序设计》一书。为什么是这本书?因为我对理论实在是不感兴趣,这本书恰恰是偏重实践的一本书,同样的还有《编译原理及实践》一书。我也是到现在才发现这两本书的,从打算好好学习编译技术开始,我就一直在为自己物色好的教程,很多教材我看了目录,就开始沮丧起来,觉得应该先把计算机原理和汇编语言什么的补一补。甚至一度想过放弃,但幸好我看到了这两本书。粗略的浏览一下,我就豁然开朗了——学些编译技术难道一定要精通x86么?不!两本书不约而同地,都用了“虚拟机”这个强大的武器。是啊,自己做个小的虚拟机,在这个虚拟机上开发自己的语言和编译器!于是我打算从一个小的虚拟机入手,开始自己的实践和学习。我选择《现代编译程序设计》一书没什么原因,就是因为我先发现的这本书。当然,两本书我都要好好地学习的。我也推荐对编译技术感兴趣的同胞们好好地看看这两本书。
我发现,现在国内网站上的一些技术文章,与“编译原理”有关的多以“表达式分析”居多,甚至我曾一度错误地认为编译原理就是表达式分析,后来才知道,这不过是编译过程中的一部分而以。不过,至少可以说,表达式分析是学习编译原理很好的入门步骤。所以,我也想从表达式分析入手。《CSDN开发高手》新增加了一个编译技术的专栏,不知道大家都看了没有,这两期都集中在“计算器”的话题上;无独有偶,《现代编译程序设计》一书也在第一章介绍了一个指令型的计算器。呵呵,那我就也作指令型计算器。《现代编译程序设计》书上介绍的指令性计算器只有个四指令——ADD, MUL, PUSH, PRINT,我就来一个能实现各种四则数值运算的计算器吧。(注:本计算器是在参考了《现代编译程序设计》的思想上自主研制出来的,极具收藏价值哦:P)
一、虚拟指令型计算器架构
首先,我接受了《现代编译程序设计》一书中的思想,将这个“虚拟机”(也就是我的“指令型计算器”)设计为基于堆栈的。该架构相当简单:
所有的指令均作用于堆栈,我们将该堆栈称为“运算栈”;
指令的基本执行流程:从运算栈中弹出操作数->执行相应操作(运算或输出或其他)[->将运算结果压入运算栈];
如果指令需要两个或更多操作数,操作数出战的顺序规定为:从右至左,即对于操作“operand1 op operand 2”,其过程为“pop operand2, pop operand1, add operand1 operand2”。这也规定了调用某项操作的压栈顺序,即push operand1, push operand2。
在某些无操作结果的指令(如OUT),无须[->将运算结果压入运算栈]这一步骤。
有了整体架构,我们就可以开始设计这个虚拟计算器的指令系统了。
二、虚拟指令型计算器指令系统
我为这个计算器设计了两组指令(详细的指令说明见附录):
1、运算栈控制指令
这一组指令用于对运算栈进行控制,包括下面这些指令:PUSH、DUP和OUT。
2、运算指令
这一组指令用于进行具体的运算,其基本运行机理是按照从右到左的规则从运算栈中推出各个操作数,再将运算结果压入运算栈。这一组指令中又分为以下一些类型:
a. 四则运算指令,包括:ADD、SUB、MUL、DIV
b. 指数运算指令,包括:EXP、SQR、CUB、POW、SQRT、CBRT
c. 对数运算指令,包括:LN、LG、LOG
d. 三角函数运算指令,包括:SIN、COS、TAN、CTN
e. 反三角函数运算指令,包括:ASIN、ACOS、ATAN、ACTN
f. 双曲函数运算指令,包括:SINH、COSH、TANH、CTNH
g. 反双曲函数运算指令,包括:ASINH、ACOSH、ATANH、ACTNH
三、虚拟指令型计算器汇编语言编译器
首先介绍一下我为这个虚拟指令型计算器设计的汇编语言,简单得让你掉下巴。就是把指令一条一条写出来就成了:P。譬如,计算3+2,可以写:
PUSH 3
PUSH 2
ADD
OUT
注:这种指令的想法参考了《现代编译程序设计》一书,你可以在该书的第一张找到相应的例子。
而sin(1.57)*cos(1.57)可以写作:
PUSH 1.57
SIN
PUSH 1.57
COS
MUL
OUT
再来一个,5*asin(cos(2)):
PUSH 2
COS
ASIN
PUSH 5
MUL
OUT
怎么样?容易吧?
那么,我们的汇编程序都做什么呢?再掉一次下巴吧,就是把这些指令写成字节,指令占一个字节、操作数用double,按机器的不同和double类型占用同样的字节数。
为什么要写这样一个汇编程序呢?让计算器直接读取指令字符串不是就可以了么?
呵呵,假装是为了提高效率和减轻对存贮器的负担吧。其实,最重要的还是回忆一下C语言。弄了这么长时间.NET和C#,把C语言都忘了。其实,最初我是想用C#作这个东西的,但最后考虑了一下,还是决定用C做。所以,险些这么个小东西熟悉一下。当然,写了这么一个编译器后,就可以尽早发现指令错误,而可以减轻计算器本身的编写难度。
废话不说了,来看看这个简单的汇编编译器吧。
其基本原理刚刚说过了,很简单,从文本文件中一次读取每一条指令然后翻译成字节,再写入到二进制文件中作为指令文件(instruction file)。
程序中的重点在一个循环中:
while(fscanf(sfile, " %s ", token) != EOF) {
if(strcmpi(token, "PUSH") == 0) {
cinst = PUSH;
fwrite((void*)&((char)cinst), sizeof(char), 1, dfile));
/* Need an operand: */
fscanf(sfile, " %lf ", &coperand));
fwrite((void*)&coperand, sizeof(coperand), 1, dfile));
}
else {
if(0 == strcmpi(token, "DUP" )) cinst = DUP ;
else if(0 == strcmpi(token, "OUT" )) cinst = OUT ;
...... /* Other instruction */
else { /* Unknown token */
cerror(lno, "Invalid instruction!");
}
/* If no error, write the instruction into temp file: */
fwrite((void*)&((char)cinst), sizeof(char), 1, dfile));
}
lno++;
} /* while */
其中,sfile是一个已打开的汇编程序源文件,token用来存贮我从文本文件中获得的指令文本。在这里,我使用了fscanf()函数来取得指令文本,而没有使用fgets(),是因为PUSH指令带有操作数,但没有必要一次把一行都读进来再分离,只需读入一个记号,如果是PUSH就再读一个操作数就可以了。这样做比较方便。所以后面我把PUSH指令单独提出来,在后面又加了一些读取操作数的代码(/* Need an operand: */注释下面的两行)。其它指令则在那一大堆if-else中进行判断,并存放在Instruction枚举类型的cinst(表示current instruction即当前指令)中。最后,将指令代码转型为char(在C中只占用一字节)并写入二进制文件(见fwrite()语句)。另外,还有个lno,这是用来存贮当前行号的,用来显示发生错误的行。
刚才提到,cinst是Instruction枚举类型。现在来介绍一下这个枚举。Instruction枚举了该指令型计算机所支持的所有指令,在inst.h中定义如下:
typedef enum tag_instruction {
PUSH, DUP, OUT,
ADD, SUB, MUL, DIV,
EXP, SQR, CUB, POW, SQRT, CBRT,
LN, LG, LOG,
SIN, COS, TAN, CTN,
ASIN, ACOS, ATAN, ACTN,
SINH, COSH, TANH, CTNH,
ASINH, ACOSH, ATANH, ACTNH
} Instruction;
没什么好解释的,一目了然。如果对指令不太熟,请看附录1。
还有文件处理方面。我为虚拟指令型计算器的汇编语言源文件起了扩展名“.scs”,意为“Simple Calculator Sourcefile”;指令文件扩展名为“.sci”,意为“Simple Calculator Instructionfile”。还有,我首先将指令写入到了一个临时文件中,也就是fwrite()语句中的dfile,原来这个变量表示的是目标文件(dest file),但后来我发现,一旦编译过程中发生错误,原来的文件就会被覆盖,而留下一个没有写完的.sci垃圾文件。所以我使用了临时文件,当编译成功后再将原有的.sci文件删除并将临时文件改名为目的文件名。而如果编译不成功,则清除临时文件,原来的.sci文件不会被损坏。我不知道别的编译器是怎样实现这一目的的,我的这种方法应该不算笨。(我还想过一种方法,就是把所有指令写入内存中,如一个void*型的buffer,不过这种方法很不方便)你在源代码中会看到一个tfn变量,这就是我用tmpnam()函数得到的临时文件名。
这个编译器真的很简单,也没什么好说的了。这一次主要也就是介绍了这个虚拟指令型计算器的构思和指令系统。至于编译器的源代码,本来打算附在附录中,但我没想到这么容易的一个东西竟被我写得乱七八糟(那里面的这个主循环可没这么好看,因为夹杂了大量的错误处理语句)。为了保留点面子,源代码就不列出来了。不过,如果你耐心地看到这了,并且对我这个计算器感兴趣,可以和我联系,我把代码悄悄地发给你,你觉得不爽就悄悄地骂我。当然,为了表示答谢,我还“附赠”一个“虚拟指令型计算器汇编语言反汇编程序”:P。其实这个反汇编程序是我用来查错的,不过我想以后也会有用的。原理和编译器差不多,就是从二进制的指令文件中获得文本方式的指令代码。
好了,这一次就贫到这里了(真没想到又贫了这么多,下次改)。以后我的编译原理就实践于这个计算器上了。当然,下一次的话题就是如何实现这个虚拟的计算器了。
=====未完待续=====