一.摘要
最近在学习编译原理,所用的书籍以《编译原理及实践》,《flex与bison》这两本书为主,另外辅有《编译原理》(江湖人称编译“龙书”)和《编译器工程》(英文版为Engineering a Compiler Second Edition)等。其中,《编译原理及实践》这本书里头介绍了一种文法简单,适合编译原理初学者实现的语言,书中称之为TINY语言,本文主要讨论以《编译原理及实践》书中所讲各种方法,用附录里所给出的代码,简单拼凑,实现一个简单的递归下降TINY语言编译器前端(词法,语法部分)。
输入:由TINY语言编写的程序
输出:输入TINY代码的线性语法树
二.写在前面的一些话:
这只是作为初学编译原理初学者的我所做的一次尝试,试图用一些现成的原料来实现一个简单的编译器前端,并没有关于词法分析,语法分析等理论技术的具体实现(我也不会详细讲这些理论技术),这些具体实现C代码均是由我采自于Kenneth C. Louden所著的《编译原理及实践》这本书,另外TINY语言的定义也是出自于这本书。
刚刚开始学习时,老师曾经要求我们写过一个简单的词法分析器,我花了几个小时就写出来了,但那只是按照我自己的思路写出来的“残次品”,输出的结果根本不能作为语法分析器的输入(事实上连最基本的词素补充都没做到,那时还没有token的概念),原因是我当时连词法分析都没看完就开始写,完全忽略了编译器工程的线性与整体性,前端与后端,前面的输出作为后面的输入,更没有抽象语法树这类的概念,所以,这是一次过急的尝试,虽然失败了,但经验很重要,这使我能静下心来好好看一看理论。
在连续一个多月的理论学习后,我已经基本掌握了一些必要的词法、语法的前端分析技术(至少我自己这么认为,不知道老师怎么想),但就这些理论知识而言,我还是虚得很,为何而虚,就是因为缺少实践。让我用这些理论真刀真枪地从零开始做一个编译器出来?还是算了吧,我压根不知道从何处下手。但经过数天的思考,我选定了这样一种方式:逆向工程,就是我们军工界的路,二十年前沈飞引进SU-27,成飞学习SAAB-37,逆向工程了二十多年,于是我们有了歼-11,歼-10,到现在歼-16,歼-20都快出来了,青出于蓝而胜于蓝。这就是为什么我前面说,要用现有的代码拼凑出一个简单编译器前端(别管有多么简陋,再简单的编译器对我这个“小白”来说都很难,先解决从无到有),这就是逆向工程的第一步:进口SU-27后,把它拆了,拆了之后再拼起来,看看能不能飞,内部结构搞不懂没关系,照猫画虎先让它飞起来,飞起来第一步就成功了!其他的事,还是等飞起来后再说吧。
所以,本文的主要目的是:工程实践,对,就是以熟悉为目的初学者入门实践。真正从零开始的实践。
身为初学者的我希望本文也能对初学者们有所帮助,也希望能多多交流。
另外,如果是大神路过,欢迎指正我的错误与不足(如果能如您法眼的话)。
三.背景知识介绍
编译器的各个阶段可以根据其用途分成两个大阶段:词法分析、语法分析和语义分析重点在处理编程语言的符号系统上,统称为编译器的前端(front-end),而中间代码生成、规范化、指令选择、控制流分析、数据流分析、寄存器分配、指令流出、汇编、连结等着重处理代码计算逻辑的阶段统称为编译的后端(back-end)。
在本文中,由于我是初学者,所以只讨论前端的两个部分:词法分析和语法分析。
词法分析程序扫描输入文件,将文件中的各个语句的组成单词分离出来,称之为词素(lexeme),并为各个词素添加相关信息。将所生成的token信息作为语法分析器的输入。语法分析主要是从词法分析程序中获得token输入,对程序进行基本的语法分析,生成抽象语法树,作为下面语义分析的输入。语义分析及后端部分这里暂时不做介绍(主要是还没学到…)。
词法分析器所用的主要知识有:正则表达式,状态机等,实现方式可以手工实现,也可以用自动分析工具Flex来实现。
语法分析器所用的主要知识有:递归下降,LL(1),LR(0),SLR(1),LR(1),等分析方法。但是,LL,LR类的分析方法在实现时会用到及其复杂的方法,手工实现很麻烦(我觉得很麻烦),因此多采用自动工具如Bison来自动生成。而类似递归下降的方法就更适合手工实现,逻辑上也能更方便自己理解。
本文所采取的实现策略是:用Flex工具生成词法分析代码,然后用递归下降方法来手工构建语法分析器。一般Flex和Bison是成对使用的,但在此,我只用了Flex来构建词法分析器。
四.环境介绍与搭建
操作系统:Windows10
编程环境:Visual Studio 2013
所用工具:Flex&Bison
所用源代码:《编译原理及实践》--KennethC. Louden 附录带所带源码里的:
”globals.h” ,“parse.h”,“scan.h”,”util.h”,”parse.c”,”util.c “以及Flex的输入文件”TINY.L”
◎注:这些代码的作用已经在书本上讲的很详细了:
第2章词法分析P56-65(介绍用Lex构建词法分析程序)
第3章上下文无关文法及分析P97-101(介绍TINY语言的文法,语法树结构和一个TINY语言的示例程序)
第4章自顶向下的分析P136-137(介绍递归下降法构造的TINY语法分析器)
其中,由于自动工具 Flex和Bison本来是UNIX上的工具,虽然有win的版本,但还是得经过一系列的配置才能用,进一步的,如果想在 Visual Studio 2013 上使用的话,又得对VS进行一番配置,具体步骤如下:
1. 从网上下载flex&bison的windows版本,文件名为win_flex_bison-latest.zip。
2. 环境变量的配置:在环境变量Path后加上前面下载的win_flex_bison-latest.zip解压后的路径,这样可以直接从命令行中调用win_flex和win_Bison,添加好后记得用-V命令检测是否配置正确(在命令行下敲入:win_flex –V 以及win_bison –V,注意V是大写,显示出版本号就表示配置正确了)。
3. 在命令行下,可以自己弄几个简单文件,自行生成体会一下,具体怎么弄就看自己喜好去折腾了,在这里我建议大家还是要自行折腾一下,因为我后面讲到的一些东西需要你在命令行下使用过win_flex命令,不然在后面的过程中可能会遇到很多问题,而这个命令使用起来有一些需要注意的地方,还有词法.l文件的编写也会有一些地方得注意,否则会出差错,还有flex词法分析程序如何进行I/O操作等等,这些都是预备知识,在书《flex与bison》上都有比较详细的讲解,只不过那是linux下的操作,但是转换成windows命令行下的操作基本是一样的,是不懂的建议百度或必应。
◎注:前面三步可以自行百度或者参考此链接:
http://www.tuicool.com/articles/2aaAjy?plg_nld=1&plg_uin=1&plg_auth=1&plg_nld=1&plg_usr=1&plg_vkey=1&plg_dev=14. VisualStudio 2013上使用Flex&Bison的配置,具体教程可以查看下载后解压好的win_flex_bison-latest文件夹内custom_build_rules目录下的how_to_use.txt文件,里头记录了一个链接,是个sourceforge的链接,讲得很清楚,你的Flex&Bison也可以直接在sourceforge上下载(事实上这是官网),不过这个教程是Visual Studio 2012的配置教程,移植到Visual Studio 2013基本上大同小异。配置好后可以通过下面的例子自行感受下。
◎注:如果你的是更低版本的VS的话,比如VisualStudio 2010,可以参考下面的链接:
http://www.di-mgt.com.au/flex_and_bison_in_msvc.html
5. 这是一个例子,(前面的配置过程我想已经够烦人的了,但这仅仅只是一个开头,万事开头难,如果到这一步你还没搞定,我建议你善用百度和必应)。如果搞定了,那么接下来就让我们用一个例子来感受下Flex&Bison与Visual Studio 结合后产生的力量!这个例子也介绍了另一种配置的方法,不过两种方法我个人感觉基本上是一样的,都是自定义的生成方法(Custom Build Tool),不过我个人推荐第4步的方法。下面给出这个例子链接,我照着上面做了一遍,虽说有些代码基本看不懂,但最后能跑起来还是感觉很开心的。需要特别注意的是,一定得从头到尾仔细得看完!!!你会有很大的收获,否则的话你会被坑得很惨!切记!
例子链接为:http://www.codeproject.com/Articles/652229/VS-C-project-w-Flex-Bison
五.代码实现
前面我已经介绍过一些基本知识和环境的配置,相信如果做完前面的那个例子,我们会对Flex&Bison这个强有力的工具更加熟悉,这也是我们的一个基础。那么现在开始我们终于要进入正题了!TINY语言的简单编译器构造:
现在开始介绍怎么利用《编译原理及实践》附录B里头的源码来构建这个简单的TINY语言编译器。在书里的各个章节里头,对这些源码会有一些很好的介绍,前面我已经说过了,而且代码本身会有很好地注释,命名规范也很容易看懂,这里就不着重介绍了,我只介绍我的改动部分以及一些可能的错误分析。
在此之前我再次重申下:
本文所采取的实现策略是:用Flex生成词法分析代码,然后用递归下降方法来手工构建语法分析器。并不是用Flex与Bison结合起来实现词法语法分析!
下面介绍具体实现步骤(低版本的VS同样适用,只要能配置好):
1. 在Visual Studio 2013上建立一个空的C++项目,在此我命名为Flex_test_TINY,然后添加以下文件(默认我们手上有第四部分所介绍的那些源代码)添加完成后如下图所示,你可以手动添加现有文件,也可以添加新建项,然后把代码直接copy过来
◎注意,其中“tiny.h”和“tiny.c”这两个文件在书的附录源码中是没有的,所以我们现在只是给其添加了两个空的文件,这两个文件里头任何东西都不用写,因为稍后我们将会用利用Flex和词法定义文件”TINY.L”来生成这两个文件的内容。 不过前提是,你得将这个项目按照我之前介绍的方法配置好,或者你可以不配置,只要你不嫌麻烦,每一次修改”TINY.L“的内容都得重新生成一次,然后把生成的的文件复制进来。 |
细心的人可能已经发现了,如果你不仅把我上面所说的那个例子做了,还另外折腾过Flex的使用的话,我会发现win_flex --wincompat 命令默认只能生成一个叫做”lex.yy.c”的文件文件,而此处我说建立两个文件,“tiny.h”和“tiny.c”,一个是词法分析器yylex()的实现的.c文件,另一个是.c文件中所包含的一些定义的.h头文件,这个头文件的作用是什么?是如何来的?还有如何改生成的文件名?请自行思考。
2. 好了,其实只要你按我前面说过的,自行折腾过Windows下Flex的使用,以及把那个例子认认真真地做了一遍并且成功运行了,那么这些问题你一定是知道的。
现在公布原因:如下图所示
这是文件”TINY.L”的属性页,这里我们设置好了两个输出的文件:一个词法分析主程序”Filename.c”文件和包含了相关定义的”Filename.h”头文件,至于这个头文件有什么用,其实在这个工程中并没有什么用,甚至你在配置的时候可以只让Flexs输出一个.c文件,但既然如此,那么我为什么还要提这一茬呢?原因我将会在后面讲到。
3. 如果你成功完成了上面的步骤,那么恭喜你,现在你一定以为大功告成可以运行了,当然我也是这么想的,于是我猜你应该是迫不及待地地按下了F5,然后天真地想象着它编译成功的样子(不要问我是怎么知道的,因为起初我也是这么做的)。但你要真这么做那就太天真了,我也就说说而已,可别真信了。
现在这样子是绝对不能运行的,原因其实很简单:没有main()函数!如果你直接编译,那么肯定会报一大堆错误,不信可以试试看。所以,接下来我们就添加main()函数使它能够运行。那么现在问题又来了,如何添加main函数来使它运行?你可以稍微思考下,然后自己动手写一下,试一试,而不是直接看我的方法,因为我也是初学者,这也是我自己的经验,经验仅仅只是证明我这种方法是可行的,并不能说明这种方法是最优的,所以获得最优解的唯一途径,往往就是无数人的无数次尝试,这些尝试所积累出来的经验,就是目前的最优解!
4. 按理说本文写到这里就应该打住了,因为前面的东西都算作是个客观框架,到现在,后面的东西才是我自己的改动,因此这不能算是教程,只能算作方法之一,而且还可能是很烂的方法,下面我要介绍的东西会有很多bug(事实上很多内部原理我也不求甚解,只知道个大概),所以特此声明:仅供参考!
5. 讲了那么多废话,现在请把目光回到本文开头部分,看看我一开始是如何定义此次程序设计的功能目标的:
输入:由TINY语言编写的程序
输出:输入TINY代码的线性语法树
那我就按照我设定的目标来改,现在的问题是,如何添加main()函数使其能够运行,还有就是,如何输出一个抽象语法树,其实仔细思考的话,这两个问题其实就是一个问题,因为在代码中,我们伟大的作者Kenneth C. Louden 已经帮我们把第二个问题解决了,在” util.c”中,已经写好了一个函数叫做printTree,所以,我们只需要在语法分析完成后调用这个函数,便可把抽象语法树打印出来。但问题又来了,到底怎么打印,输出到哪里,或者更基础的,如何读取数据流?想回答这些问题,又得看看代码了,在文件 ”global.h”中,作者已经声明好了一切我们需要的东西:
extern FILE* source; /* source code text file */
extern FILE* listing; /* listing output text file */
extern FILE* code; /* code text file for TM simulator */
extern int lineno; /* source line number for listing */
在这里我们只需要关注两个文件指针 source和listing,这两个就是我们的输入和输出,通过文件指针source来读取TINY语言所编写的程序,通过listing来输出错误信息(如果编译出错的话,代码里头有基本的错误分析)和语法树(如果编译成功的话)。所以现在,我们可以在”parse.c”这个文件中添加一个main函数,并且添加如下几行代码:
在这里我们只需要关注两个文件指针 source和listing,这两个就是我们的输入和输出,通过文件指针source来读取TINY语言所编写的程序,通过listing来输出错误信息(如果编译出错的话,代码里头有基本的错误分析)和语法树(如果编译成功的话)。所以现在,我们可以在”parse.c”这个文件中添加一个main函数,并且添加如下几行代码:
int main()
{
char* inputname = "test.txt";
char* outputname = "output.txt";
source = fopen(inputname, "r");
listing = fopen(outputname, "w");
if (!source){
perror(source);
return 1;
}
if (!listing)
{
perror(listing);
return 2;
}
parse();
getchar(); //输出停顿用
return 0;
}
并且为了打印语法树,我们应该把parse函数稍微改一下,在分析完成将要退出之前加上printTree,这样就可以打印出完整的抽象语法树了。
6. 好了现在mian函数也有了,这下应该可以运行了吧?如果你认认真真按照我的步骤来,走到这一步,按下F5,你依旧发现不能运行,那么别灰心,因为原则性的大错误已经没有了,虽然错误很多,但接下来只是来处理小错误了:
在众多的错误输出中应该会有这么一条:
IntelliSense: PCH 警告: 标头停止点不能位于宏或#if 块中。未生成 Intellisen
处理这个错误的方法就是在”global.h”的开头加上一句:#pragmaonce
此外还会有:
等等一系列令人眼花缭乱的错误,一看这些错误就很烦人,因为不是编译错误,而是链接错误,编译错误还好处理,但链接错误我们连定位都不能定。所以这个时候又得发挥百度和必应的强大作用了,通过一番努力后,我终于找出了这些错误的原因:这些出错的变量(lineno,listing,source等),并没有被真正定义就被其他函数使用了,extern仅仅只是声明了有这些变量,并没有定义,更别说初始化了,所以我们还得再加几行全局变量的定义语句,在mian函数的外部,并把这些变量能初始化的就初始化(这些错误归根到底就是我当初C语言没学好)。
7. 做完这些后,再按下F5,这时我才能真正恭喜你运行成功了(当然你得在根目录下建两个文件,一个作为TINY语言输入,一个作为输出)到此为止,整个过程基本上结束了。如果你还没运行成功的话,请继续努力调试,直到成功为止。
8. 貌似我还有一个坑没填,就是我之前说的那个”Filename.h”有什么用,这个坑我也不打算填了,其实整个过程折腾下来你就差不多能知道,提示一下,可以看看这个奇葩的头文件”Filename.h”里头包含了些什么东西,查一查那些东西是干嘛的,可以参照书本《flex与bison》第二章P29-34,以及第五章Flex参考规范。
六.总结
其实上述过程,就算按我的步骤一步步来,你还是有可能运行不成功,除了各种不可控因素外,原因我也说过了:我们都是初学者,哪那么容易就搞定呢?如果这些问题都解决了,我们早就进阶成大神了,所以我才在第四部分反复建议去亲自多折腾,这样才能更好地解决你遇到的问题。这种折腾出来的思考问题,解决问题的能力,也是我们逆向工程的基础之一。
自此为止,逆向工程的第一步就算完成了,这个东西说实话非常简单,稍微有点编程经验的人一下就搞定了,但对于我们这种及其缺乏实践经验的人来说,确实是一件不那么容易的事情,所以,后续我将会更加深入地学习探索。
七.参考资料
第三部分-背景知识介绍部分引用自:
http://www.cnblogs.com/Ninputer/archive/2011/06/07/2074632.html
两本书:《编译原理及实践》--KennethC. Louden,《flex与bison》--John Levine
第四部分的引用已经在前面标出。
本书代码全权CopyRight @KennethC. Louden 本人作为教育用途只做部分改动。
这是本人第一次发表博客,目前作为学生,才疏学浅,见识浅薄,有诸多幼稚错误的观点望请大神指正,吾将更加深入学习。