编译原理 :计算机专业课程

更新时间:2024-09-20 22:31

编译原理是计算机专业的一门重要专业课,旨在介绍编译程序构造的一般原理和基本方法。内容包括语言和文法、词法分析、语法分析、语法制导翻译、中间代码生成、存储管理、代码优化和目标代码生成。

编译原理是计算机专业设置的一门重要的专业课程。虽然只有少数人从事编译方面的工作,但是这门课在理论、技术、方法上都对学生提供了系统而有效的训练,有利于提高软件人员的素质和能力。目前各个大学使用的教材机械工业出版社国防工业出版社出版的《编译原理》。

基本概念

编译器是将汇编或高级计算机语言翻译为二进制机器语言代码的计算机程序。编译器将源程序(source language)编写的程序作为输入,翻译产生目标语言(target language)机器代码的等价程序。通常地,源程序为高级语言(high-level language),像C或C++、汉语语言程序等,而目标则是机器语言的目标代码(object code,有时也称作机器代码(machine code)),也就是可以在计算机硬件中运行的机器代码软件。这一过程可以表示为:

源程序→编译器→目标机器代码程序

发展历程

在20世纪40年代,由于约翰·冯·诺依曼在存储-程序计算机方面的先锋作用,编写一串代码或程序已成必要,这样计算机就可以执行所需的计算。开始时,这些程序都是用机器语言(machine language )编写的。机器语言就是表示机器实际操作的数字代码,例如:

C7 06 0000 0002 表示在IBM PC 上使用的英特尔 8x86处理器将数字2移至地址0 0 0 0 (16进制)的指令。

但编写这样的代码是十分费时和乏味的,这种代码形式很快就被汇编语言(assembly language )代替了。在汇编语言中,都是以符号形式给出指令和存储地址的。例如,汇编语言指令 MOV X,2 就与前面的机器指令等价(假设符号存储地址X是0 0 0 0 )。汇编程序(assembler )将汇编语言的符号代码和存储地址翻译成与机器语言相对应的数字代码。

汇编语言大大提高了编程的速度和准确度,人们至今仍在使用着它,在编码需要极快的速度和极高的简洁程度时尤为如此。但是,汇编语言也有许多缺点:编写起来也不容易,阅读和理解很难;而且汇编语言的编写严格依赖于特定的机器,所以为一台计算机编写的代码在应用于另一台计算机时必须完全重写。

发展编程技术的下一个重要步骤就是以一个更类似于数学定义或自然语言的简洁形式来编写程序的操作,它应与任何机器都无关,而且也可由一个程序翻译为可执行的代码。例如,前面的汇编语言代码可以写成一个简洁的与机器无关的形式 x = 2。

在1954年至1957年期间,IBM的John Backus带领的一个研究小组对Fortran及其编译器的开发,使得上面的担忧不必要了。但是,由于当时处理中所涉及到的大多数程序设计语言的翻译并不为人所掌握,所以这个项目的成功也伴随着巨大的辛劳。几乎与此同时,人们也在开发着第一个编译器, Noam Chomsky开始了他的自然语言结构的研究。他的发现最终使得编译器结构异常简单,甚至还带有了一些自动化。Chomsky的研究导致了根据语言文法(grammar ,指定其结构的规则)的难易程度以及识别它们所需的算法来为语言分类。正如现在所称的-与诺姆·乔姆斯基分类结构(Chomsky hierarchy )一样-包括了文法的4个层次:0型、1型、2型和3型文法,且其中的每一个都是其前者的专门化。2型(或上下文无关文法(context-free grammar ))被证明是程序设计语言中最有用的,而且今天它已代表着程序设计语言结构的标准方式。

分析问题( parsing problem ,用于限定上下文无关语言的识别的有效算法)的研究是在20世纪60年代和70年代,它相当完善地解决了这一问题,现在它已是编译理论的一个标准部分。它们与诺姆·乔姆斯基的3型文法相对应。对它们的研究与乔姆斯基的研究几乎同时开始,并且引出了表示程序设计语言的单词(或称为记号)的符号方式。

人们接着又深化了生成有效的目标代码的方法,这就是最初的编译器,它们被一直使用至今。人们通常将其误称为优化技术(optimization technique ),但因其从未真正地得到过被优化了的目标代码而仅仅改进了它的有效性,因此实际上应称作代码改进技术(code improvement technique )。

这些程序最初被称为编译计算机程序编译器,但更确切地应称为分析程序生成器(parser generator ),这是因为它们仅仅能够自动处理编译的一部分。这些程序中最著名的是 Yacc (yet another 编译器- compiler),它是由Steve Johnson在1975年为unix系统编写的。

类似地,有穷自动机的研究也发展了另一种称为扫描程序生成器(扫描仪 generator )的工具,Lex (与Yacc同时,由Mike Lesk为Unix系统开发的)是这其中的佼佼者。在20世纪70年代后期和80年代早期,大量的项目都关注于编译器其他部分的生成自动化,这其中就包括代码生成。这些尝试并未取得多少成功,这大概是因为操作太复杂而人们又对其不甚了解。

编译器设计最近的发展包括:首先,编译器包括了更为复杂的算法的应用程序,它用于推断或简化程序中的信息;这又与更为复杂的程序设计语言(可允许此类分析)的发展结合在一起。其中典型的有用于函数语言编译的Hindle y - Milner类型检查的统一算法。

其次,编译器已越来越成为基于窗口的交互集成开发环境(interactive development environment,IDE )的一部 分,它包括了编辑器、链接程序、调试程序以及项目管理程序。这样的集成开发环境的标准并没有多少,但是已沿着这一方向对标准的窗口环境进行开发了。

编译器

c语言编译器是一种现代化的设备,其需要借助计算机编译程序,C语言编译器的设计是一项专业性比较强的工作,设计人员需要考虑计算机程序繁琐的设计流程,还要考虑计算机用户的需求。计算机的种类在不断增加,所以,在对C语言编译器进行设计时,一定要增加其适用性。C语言具有较强的处理能力,其属于结构化语言,而且在计算机系统维护中应用比较多,C语言具有高效率的优点,在其不同类型的计算机中应用比较多。

C语言编译器前端设计

编译过程一般是在计算机系统中实现的,是将源代码转化为计算机通用语言的过程。编译器中包含入口点的地址、名称以及机器代码。编译器是计算机程序中应用比较多的工具,在对编译器进行前端设计时,一定要充分考虑影响因素,还要对词法、语法、语义进行分析。

1.词法分析

词法分析是编译器前端设计的基础阶段,在这一阶段,编译器会根据设定的语法规则,对源程序进行标记,在标记的过程中,每一处记号都代表着一类单词,在做记号的过程中,主要有标识符、关键字、特殊符号等类型,编译器中包含词法分析器、输入源程序、输出识别记号符,利用这些功能可以将字号转化为熟悉的单词。

2.语法分析

语法分析是指利用设定的语法规则,对记号中的结构进行标识,这包括句子、短语等方式,在标识的过程中,可以形成特殊的结构语法树。语法分析对编译器功能的发挥有着重要影响,在设计的过程中,一定要保证标识的准确性。

3.语义分析

语义分析也需要借助语法规则,在对语法单元的静态语义进行检查时,要保证语法规则设定的准确性。在对词法或者语法进行转化时,一定要保证语法结构设置的合法性。在对语法、词法进行检查时,语法结构设定不合理,则会出现编译错误的问题。前端设计对精确性要求比较好,设计人员能够要做好校对工作,这会影响到编译的准确性,如果前端设计存在失误,则会影响c语言编译的效果。

C语言编译器总体设计

C语言编译器是一种先进的设备,其可以将繁琐复杂的程序转换为机器语言,具有化繁为简的功能,在对C语言编译器进行划分的过程中,需要了解语法构成原理,设计人员需要灵活掌握语言语法知识,还要应用C语言代码转化方式,在对C语言编译器进行总体设计时,需要从以下几个方面入手。

1.词法分析

C语言程序具有一定的复杂性,语法分析是编译的初级阶段,这一过程主要是对词法进行扫描,所以词法分析阶段,编译器也被用作是扫描器,其主要的任务是将源程序中的字符进行连接,并且对其中的词语进行识别,并且对词汇进行转化,将其转换为内部编码,并且对其语法进行分析与标记。单词符号在编译的过程中,一般会被转化为二元式,单词类别主要有二进制、分隔符、单词等,计算机在正常工作时,会通过扫描器自动完成词法扫描工作,这一过程也会自动将注释进行删除,会将源程序中的单词进行自动识别,还会将其转换为内部编码。从工作方式角度来分析,编译流程与语法属于两种接口方式,若是从功能上讲,编译器词法分分析的过程主要就是将相应的字符源程序进行转换处理,从而变成单词串的形式。

2.语义分析

将编译程序转换为一种内部表现形式后,我们将该种内部表现形式称之为中间语言或者是中间代码,它含义明确、结构简单,属于一种记号系统。比如一些编译程序,基本上没有中间代码,但是为了在实际应用中,确保机器的独立运行,易于实现目标代码的优化,在许多的编译程序中均设置了中间语言。这种中间语言介于机器语言和源程序语言中,程序相对复杂,而c语言编译器却在很大程度上改变以上现状,其语义分析和语法分析相对成熟,理论和算法比较完善,可仍旧存在的问题是没有公认的形式系统,中间代码仍旧接近于形式化的方法。

3.语法分析

语法分析主要是以单词串形式的源程序作为分析与输入对象,其最为根本的目标和任务就是为了以设计语言的语法规则为标准,对源程序的语法结构进行具体的分析,根据设计语言的语法规则,对组成这些源程序的语法成分进行分析,如函数、下标变量、各种程序语句、各种表达式等等,并且要通过正确性的语法检查,将中间代码进行阶段处理。但是要注意的一点是根据需要进行了归约处理,必然存在着相应语法错误,那么可以将其中全部输入的符号删除,改变上述格局,进行移进和归约分析,并且在此基础上,不断地寻找一个相应的策略,从而形成有效的语法分析方法。

编译原理课程

《编译原理》作为计算机专业的一门重要专业课程,是日后深入研究专业领域知识的基础。这门课作为计算机科学与技术的专业课,融合了离散数学数据结构、操作系统、计算机组成原理等多个学科的知识,属于综合性与理论性较强的一门课,由于编译原理课程内容的以上特点,目前在实验教学中仍存在一些问题。编译原理实验部分若要设计制作完整的编译器,实验周期长,涉及的模块较多,各模块间衔接较复杂,不易立即看到整体效果。

传统编译原理课程教学中存在的问题

计算机类专业本科生学习本专业的第一门语言课程是c语言。C语言由于其类型不安全性,容易出现一些难以捉摸的错误,使得学生难以定位和解决问题。如果能让学生根据编译器提供的提示信息,精确定位程序中的错误类型和位置,把编译原理中所学用于实际C语言编程需求,这既完成了课程的教学内容,也提升了学生的软件编程和系统分析的能力。

从普通高等院校的编译原理教学实际出发,其课程覆盖范围一般仅限于编译器的前端,即词法分析、语法分析和语法制导翻译等内容。这其中包括大量抽象且逻辑复杂的理论知识点,如形式语言理论、正规式、有限自动机、上下文无关文法、属性文法和语法制导翻译等。传统的教学方式强调知识点的灌输,让学生解决孤立的单一问题,缺乏各知识点之间的关联。这种“只见树木,不见森林”的教学方式会极大地削弱学生的学习积极性,导致整体效果不佳。

以实践为导向的互助式编译原理教学

(一) 理论与实践相衔接

理论知识的来源一般都有其确定的问题背景。脱离实际问题来进行理论教学,对学生实际能力的提升没有益处。编译原理课程中的大量理论知识,存在一种衔接递进的关系,每个知识点的引入和拓展,都是对于现实遇到问题的解决路径再现。因此,整个授课过程就在重现这种解决方案演变的变化历程。而实现这一目标的关键之处,是教师从之前的“站到讲台前”变到现在的“坐在学生中”。这一变化绝不仅仅是简单地将所有问题留给学生,从“讲授”变成“答疑”,而是从问题设计、思考启迪、讨论引导到过程管理等各方面都对教师提高了要求。特别是现代高级语言发展日新月异,各种新问题层出不穷,如何在面对开放性的未知问题时,从系统和整体的角度给出学生解决问题的方式方法,而不是给出具体每个问题的回答,这是对教师能力的一种新考验。

(二) 编译器设计实现方案

编译原理课程教学理想情况,学生应该能够独立自主完成小型编译系统的构造。实际教学中,学生只需吃透关键的几条原理知识,如NFA的确定化,LL (1) 文法中FIRST和FOLLOW集合的构造,LR (1) 文法中识别活前缀DFA构造等,基本上已经满足了课程考试要求。然而,仅靠理论学习对实现一个基础编译器来说是远远不足的。相比较于学生对理论知识的接受程度,学生自主动手完成编译系统的能力缺乏就更为明显。如何面对全体学生,制定出一套适用的实践方案,是课程实际效用的关键。

有关程序

解释程序

解释器(interpreter):解释程序是如同编译器的一种语言翻译程序。它与编译器的不同之处在于:它立即执行源程序而不是生成在翻译完成之后才执行的目标代码。从原理上讲,任何程序设计语言都可被解释或被编译,但是根据所使用的语言和翻译情况,很可能会选用解释程序而不用编译器。例如,我们经常解释BASIc语言而不是去编译它。类似地,诸如LISP 的函数语言也常常是被解释的。

解释程序也经常用于教育和软件的开发,此处的程序很有可能被翻译若干次。而另一方面,当执行的速度是最为重要的因素时就使用编译器,这是因为被编译的目标代码比被解释的源代码要快得多,有时要快10倍或更多。但是,解释程序具有许多与编译器共享的操作,而两者之间也有一些混合之处。

汇编程序

汇编程序(assembler):汇编程序是用于特定计算机上的汇编语言的翻译程序。正如前面所提到的,汇编语言是计算机的机器语言的符号形式,它极易翻译。有时,编译器会生成汇编语言以作为其目标语言,然后再由一个汇编程序将它翻译成目标代码。

连接程序

连接程序(linker):编译器和汇编程序都经常依赖于连接程序,它将分别在不同的目标文件中编译或汇编的代码收集到一个可直接执行的文件中。在这种情况下,目标代码,即还未被连接的机器代码,与可执行的机器代码之间就有了区别。连接程序还连接目标程序和用于标准库函数的代码,以及连接目标程序和由计算机的操作系统提供的资源(例如,存储分配程序及输入与输出设备)。连接程序现在正在完成编译器最早的一个主要活动(这也是“编译”一词的用法,即通过收集不同的来源来构造)。连接过程对操作系统和处理器有极大的依赖性。

装入程序

装入程序(loader):编译器、汇编程序或连接程序生成的代码经常还不完全适用或不能执行,但是它们的主要存储器访问却可以在存储器的任何位置中且与一个不确定的起始位置相关。这样的代码被称为是可重定位的(relocatable ),而装入程序可处理所有的与指定的基地址或起始地址有关的可重定位的地址。装入程序使得可执行代码更加灵活,但是装入处理通常是在后台(作为操作环境的一部分)或与连接相联合时才发生,装入程序极少会是实际的独立程序。

预处理器

预处理器(preprocessor ):预处理器是在真正的翻译开始之前由编译器调用的独立程序。预处理器可以删除注释、包含其他文件以及执行宏(宏macro是一段重复文字的简短描写)替代。预处理器可由语言(如 C )要求或以后作为提供额外功能(诸如为Fortran提供Ratfor预处理器)的附加软件。

调试程序

调试程序(debugger ):调试程序是可在被编译了的程序中判定执行错误的程序,它也经常与编译器一起放在IDE 中。运行一个带有调试程序的程序与直接执行不同,这是因为调试程序保存着所有的或大多数源代码信息(诸如行数、变量名和过程)。它还可以在预先指定的位置(称为断点(breakpoint ))暂停执行,并提供有关已调用的函数以及变量的当前值的信息。为了执行这些函数,编译器必须为调试程序提供恰当的符号信息,而这有时却相当困难,尤其是在一个要优化目标代码的编译器中。因此,调试又变成了一个编译问题。

描述器

描述器(profiler):描述器是在执行中搜集目标程序行为统计的程序。程序员特别感兴趣的统计是每一个过程的调用次数和每一个过程执行时间所占的百分比。这样的统计对于帮助程序员提高程序的执行速度极为有用。有时编译器也甚至无需程序员的干涉就可利用描述器的输出来自动改进目标代码。

项目管理程序

项目管理程序(project manager):软件项目通常大到需要由一组程序员来完成,这时对那些由不同人员操作的文件进行整理就非常重要了,而这正是项目管理程序的任务。例如,项目管理程序应将由不同的程序员制作的文件的各个独立版本整理在一起,它还应保存一组文件的更改历史,这样就能维持一个正在开发的程序的连贯版本了(这对那些由单个程序员管理的项目也很有用)。项目管理程序的编写可与语言无关,但当其与编译器捆绑在一起时,它就可以保持有关特定的编译器和建立一个完整的可执行程序的链接程序操作的信息。在Unix系统中有两个流行的项目管理程序:sccs (source code ctrl system )和rcs (revision control system )。

步骤

编译器内部包括了许多步骤或称为阶段源代码(phase),它们执行不同的逻辑操作。将这些阶段设想为编译器中一个个单独的片断是很有用的,尽管在应用中它们是经常组合在一起的,但它们扫描程序确实是作为单独的代码操作来编写的。

扫描程序

扫描程序(扫描仪):在这个阶段编译器实际阅读源程序(通常以分析程序字符流的形式表示)。扫描程序执行词法分析注释树符号表(Lexical analysis ):它将字符序列收集到称作记号错误处(token )的有意义单元中,记号同自然语言,如英源代码理器语中的字词相似。因此可以认为扫描程序执行与优化程序拼写相似的任务。中间代码例如在下面的代码行(它可以是C程序的一部分)中:代码生成器 a [index] = 4 + 2 这个代码包括了1 2个非空字符,但只有 8个目标代码记号:a 标识符目标代码优化程序 [ 左括号 i n d e x 标识符 ] 右括号 = 赋值目标代码 4 数字编译器的阶段 + 加号 2 数字 每一个记号均由一个或多个字符组成,在进一步处理之前它已被收集在一个单元中。扫描程序还可完成与识别记号一起执行的其他操作。例如,它可将标识符输入到符号表中,将文字(litral)输入到文字表中(文字包括诸如3 . 1415926535的数字常量,以及诸如“Hello,world ! ”的引用字符串)。

语法分析

语法分析(parser ):语法分析程序从扫描程序中获取记号形式的源代码,并完成定义程序结构的语法分析(syntax analysis ),这与自然语言中句子的语法分析类似。语法分析定义了程序的结构元素及其关系。通常将语法分析的结果表示为分析树(parse tree)或语法树(syntax tree)。例如,还是那行C代码,它表示一个称为表达式的结构元素,该表达式是一个由左边为下标表达式、右边为整型表达式的赋值表达式组成。这个结构可按下面的形式表示为一个分析树:请注意,分析树的内部节点均由其表示的结构名标示出,而分析树的叶子则表示输入中的记号序列(结构名以不同字体表示以示与记号的区别)。分析树对于显示程序的语法或程序元素很有帮助,但是对于表示该结构却显得力不从心了。分析程序更趋向于生成语法树,语法树是分析树中所含信息的浓缩(有时因为语法树表示从分析树中的进一步抽取,所以也被称为抽象的语法树(abstract syntax tree ))。下面是一个C赋值语句的抽象语法树的例子:请注意,在语法树中,许多节点(包括记号节点在内)已经消失。例如,如果知道表达式是一个下标运算,则不再需要用括号“[”和“]”来表示该操作是在原始输入中。

语义分析

语义分析(semantic analyzer ):程序的语义就是它的“意思”,它与语法或结构不同。程序的语义确定程序的运行,但是大多数的程序设计语言都具有在执行之前被确定而不易由语法表示和由分析程序分析的特征。这些特征被称作静态语义(static semantic),而语义分析程序的任务就是分析这样的语义(程序的“动态”语义具有只有在程序执行时才能确定的特性,由于编译器不能执行程序,所以它不能由编译器来确定)。一般的程序设计语言的典型静态语义包括声明和类型检查。由语义分析程序计算的额外信息(诸如数据类型)被称为属性(attribute),它们通常是作为注释或“装 饰”增加到树中(还可将属性添加到符号表中)。在正运行的C表达式 a [index] = 4 + 2 中,该行分析之前收集的典型类型信息可能是:a是一个整型值的数组,它带有来自整型子范围的下标;index则是一个整型变量。接着,语义分析程序将用所有的子表达式类型来标注语法树,并检查赋值是否使这些类型有意义了,如若没有,则声明一个类型匹配错误。在上例中,所有的类型均有意义,有关语法树的语义分析结果可用以下注释了的树来表示。

优化程序

优化程序(source code optimizer):编译器通常包括许多代码改进或优化步骤。绝大多数最早的优化步骤是在语义分析之后完 成的,而此时代码改进可能只依赖于源代码。这种可能性是通过将这一操作提供为编译过程中的单独阶段指出的。每个编译器不论在已完成的优化种类方面还是在优化阶段的定位中都有很大的差异。在上例中,我们包括了一个源代码层次的优化机会,也就是:表达式4 + 2可由编译器计算先得到结果6 (这种优化称为常量合并(constant folding ))。当然,还会有更复杂的情况。还是在上例中,通过将根节点右面的子树合并为它的常量值,这个优化就可以直接在(注释)语法树上完成:尽管许多优化可以直接在树上完成,但是在很多情况下,优化接近于汇编代码线性化形式的树更为简便。这样节点的变形有许多,但是三元式代码(three-address code )(之所以这样称呼是因为它在存储器中包含了3个(或3个以上)位置的地址)却是标准选择。另一个常见的选 择是P -代码(P - code ),它常用于Pascal编译器中。在前面的例子中,原先的C表达式的三元式代码应是:t = 4 + 2 a [ index] = t (请注意,这里利用了一个额外的临时变量t 存放加法的中间值)。这样,优化程序就将这个代码改进为两步。首先计算加法的结果:t = 6 a [index] = t 接着,将t替换为该值以得到三元语句 a [index] = 6 ,指出源代码优化程序可能通过将其输出称为中间代码(intermediate code )来使用三元式代码。中间代码一直是指一种位于源代码和目标代码(例如三元式代码或类似的线性表示)之间的代码表示形式。但是,我们可以更概括地认为它是编译器使用的源代码的任何一个内部表示。此时,也可将语法树称作中间代码,源代码优化程序则确实能继续在其输出中使用这个表示。有时,这个中间代码也称作中间表示(intermediate 表征,IR)。

代码生成

代码生成(code generator):代码生成器得到中间代码(IR),并生成目标机器的代码。正是在编译的这个阶段中,目标机器的特性成为了主要因素。当它存在于目标机器时,使用指令不仅是必须的而且数据的形式表示也起着重要的作用。例如,整型数据类型的变量和浮点数据类型的变量在存储器中所占的字节数或字数也很重要。在上面的示例中,现在必须决定怎样存储整型数来为数组索引生成代码。例如,下面是所给表达式的一个可能的样本代码序列(在假设的汇编语言中):

M O V R0,index ;;

value of index -\u003e R0 M U L R0,2 ;;

double value in R0 M O V R1,\u0026a ;;

address of a -\u003e R1 A D D R1,R0 ;;

add R0 to R1 M O V *R1,6 ;;

constant 6 -\u003e address in R1

在以上代码中,为编址模式使用了一个类似C的协定,因此\u0026 a是a的地址(也就是数组的基地址),* R1则意味着间接寄存器地址(因此最后一条指令将值6存放在R1包含的地址中)。这个代码还假设机器执行字节编址,并且整型数占据存储器的两个字节(所以在第2条指令中用2作为乘数)。

目标代码

目标代码(target code optimizer ):在这个阶段中,编译器尝试着改进由代码生成器生成的目标代码。这种改进包括选择编址模式以提高性能、将速度慢的指令更换成速度快的,以及删除多余的操作。在上面给出的样本目标代码中,还可以做许多更改:在第2条指令中,利用移位指令替代乘法(通常地,乘法很费时间),还可以使用更有效的编址模式(例如用索引地址来执行数组 存储)。使用了这两种优化后,目标代码就变成:

MOV R0,index ;;

value of index -\u003e R0 SHL R0 ;;

double value in R0 MOV \u0026a[R0],6 ;;

constant 6 -\u003e address a + R0

到这里就是编译原理的简要描述,但还应特别强调编译器在其结构细节上差别很大。

数据结构

编译原理一直是计算机学习的必修课.

当然,由编译器的阶段使用的算法与支持这些阶段的数据结构之间的交互是非常强大的。编译器的编写者尽可能有效实施这些方法且不引起复杂性。理想的情况是:与程序大小成线性比例的时间内编译器,换言之就是,在0 ( n )时间内,n是程序大小的度量(通常是字符数)。本节将讲述一些主要的数据结构,它们是其操作部分阶段所需要的,并用来在阶段中交流信息。

记号

记号(token):当扫描程序将字符收集到一个记号中时,它通常是以符号表示这个记号;这也就是说,作为一个枚举数据类型的值来表示源程序的记号集。有时还必须保留字符串本身或由此派生出的其他信息(例如:与标识符记号相关的名字或数字记号值)。在大多数语言中,扫描程序一次只需要生成一个记号(这称为单符号先行(single symbol lookahead))。在这种情况下,可以用全程变量放置记号信息;而在别的情况(最为明显的是Fortran)下,则可能会需要一个记号数组。

语法树

语法树(syntax tree):如果分析程序确实生成了语法树,它的构造通常为基于指针的标准结构,在进行分析时动态分配该结构,则整棵树可作为一个指向根节点的单个变量保存。结构中的每一个节点都是一个记录,它的域表示由分析程序和之后的语义分析程序收集的信息。例如,一个表达式的数据类型可作为表达式的语法树节点中的域。有时为了节省空间,这些域也是动态分配或存放在诸如符号表的其他数据结构中,这样就可以有选择地进行分配和释放。实际上,根据它所表示的语言结构的类型(例如:表达式节点对于语句节点或声明节点都有不同的要求),每一个语法树节点本身都可能要求存储不同的属性。在这种情况下,可由不同的记录表示语法树中的每个节点,每个节点类型只包含与本身相关的信息。

符号表

符号表(symbol table):这个数据结构中的信息与标识符有关:函数、变量、常量以及数据类型。符号表几乎与编译器的所有阶段交互:扫描程序、分析程序或将标识符输入到表格中的语义分析程序;语义分析程序将增加数据类型和其他信息;优化阶段和代码生成阶段也将利用由符号表提供的信息选 出恰当的代码。因为对符号表的访问如此频繁,所以插入、删除和访问操作都必须比常规操作更有效。尽管可以使用各种树的结构,但杂凑表却是达到这一要求的标准数据结构。有时在一个列表或栈中可使用若干个表格。

常数表

常数表(literal table):常数表的功能是存放在程序中用到的常量和字符串,因此快速插入和查找在常数表中也十分重要。但是,在其中却无需删除,这是因为它的数据全程应用于程序而且常量或字符串在该表中只出现一次。通过允许重复使用常量和字符串,常数表对于缩小程序在存储器中的大小显得非常重要。在代码生成器中也需要常数表来构造用于常数和在目标代码文件中输入数据定义的符号地址。

中间代码

中间代码(intermediate code):根据中间代码的类型(例如三元式代码和P -代码)和优化的类型,该代码可以是文本串的数组、临时文本文件或是结构的连接列表。对于进行复杂优化的编译器,应特别注意选择允许简单重组的表示。

临时文件

临时文件(temporary file):计算机过去一直未能在编译器时将整个程序保留在存储器中。这一问题已经通过使用临时文件来保存翻译时中间步骤的结果或通过“匆忙地”编译(也就是只保留源程序早期部分的足够信息用以处理翻译)解决了。存储器的限制现在也只是一个小问题了,现在可以将整个编译单元放在存储器之中,特别是在可以分别编译的语言中时。但是偶尔还是会发现需要在某些运行步骤中生成中间文件。其中典型的是代码生成时需要反填(backpatch)地址。例如,当翻译如下的条件语句时 if x = 0 then ... else ... 在知道else部分代码的位置之前必须由文本跳到else部分:

CMP X,0 JNE NEXT ;;

location of NEXT not yet known \u003c code for then-part \u003e NEXT : \u003c code for else-part \u003e

通常,必须为NEXT的值留出一个空格,一旦知道该值后就会将该空格填上,利用临时文件可以很容易地做到这一点。

如果想利用上面的编译原理开发一套属于自己的编程语言,或者想在一个产品中嵌入编程语言,可以参考zengl开源网开发的zengl编程语言,该编程语言为国人使用c语言开发,里面包含两个部分,一个是编译器,一个是解释执行中间代码的虚拟机编译器包含了词法扫描,语法分析,中间代码输出等,虚拟机则类似JAVA一样解释执行中间代码。作者将所有的版本都公布出来,好让读者可以由浅入深的做研究,并且为了证明该编程语言的实用性,还结合SDL游戏开发库开发了一款图形界面和命令行界面的21点扑克小游戏。

zengl编程语言目前适用平台为windows和Linux (最开始在Linux下使用gcc开发,后来移植到windows平台)

其他问题

可从许多不同的角度来观察编译器的结构,还有其他一些可能的观点:编译器的物理结构、操作的顺序等等。由于编译器的结构对其可靠性、有效性、可用性以及可维护性都有很大的影响,所以编译器的编写者应熟悉尽可能多的有关编译器结构的观点。

分析和综合

在这个观点中,已将分析源程序以计算其特性的编译器操作归为编译器的分析(analysis)部分,而将生成翻译代码时所涉及到的操作称作编译器的综合(synthesis )部分。当然,词法分析、语法分析和语义分析均属于分析部分,而代码生成却是综合部分。在优化步骤中,分析和综合都有。分析正趋向于易懂和更具有数学性,而综合则要求更深的专业技术。因此,将分析步骤和综合步骤两者区分开来以便发生变化时互不影响是很有用的。

前端和后端

本观点认为,将编译器分成了只依赖于源语言(前端(front end ))的操作和只依赖于目标语言(后端(back end ))的操作两部分。这与将其分成分析和综合两部分是类似的:扫描程序、分析程序和语义分析程序是前端,代码生成器是后端。但是一些优化分析可以依赖于目标语言,这样就是属于后端了,然而中间代码的综合却经常与目标语言无关,因此也就属于前端了。在理想情况下,编译器被严格地分成这两部分,而中间表示则作为其间的交流媒介。这一结构对于编译器的可移植性(portability)十分重要,此时设计的编译器既能改变源代码(它涉及到重写前端),又能改变目标代码(它还涉及到重写后端)。在实际中,这是很难 做到的,而且称作可移植的编译器仍旧依赖于源语言和目标语言。其部分原因是程序设计语言和机器构造的快速发展以及根本性的变化,但是有效地保持移植一个新的目标语言所需的信息 或使数据结构普遍地适合改变为一个新的源语言所需的信息却十分困难。然而人们不断分离前端和后端的努力会带来更方便的可移植性。

编译器发现,在生成代码之前多次处理整个源程序很方便。这些重复就是遍( pass)。首遍是从源中构造一个语法树或中间代码,在它之后的遍是由处理中间表示、向它增加信息、更换结构或生成不同的表示组成。遍可以和阶段相应,也可无关-遍中通常含有若干个阶段。实际上,根据语言的不同,编译器可以是一遍(one pass )-所有的阶段由一遍完成,其结果是编译得很好,但(通常)代码却不太有效。Pascal语言和C 语言均允许单遍编译。(Modula - 2语言的结构则要求编译器至少有两遍)。大多数带有优化的编译器都需要超过一遍:典型的安排是将一遍用于扫描和分析,将另一遍用于语义分析和源代码层优化,第3遍用于代 码生成和目标层的优化。更深层的优化则可能需要更多的遍:5遍、6遍、甚至8遍都是可能的。

语言定义和编译器

程序设计语言的词法和语法结构通常用形式的术语指定,并使用正则表达式上下文无关文法。但是,程序设计语言的语义通常仍然是由英语(或其他的自然语言)描述的。这些描述(与形式的词法及语法结构一起)一般是集中在一个语言参考手册(language reference manual )或语言定义(language definition)之中。因为编译器的编写者掌握的技术对于语言的定义有很大的影响,所以在使用了一种新的语言之后,语言的定义和编译器同时也能够得到开发。类似地,一种语言的定义对于构造编译器所需的技术也有很 大的关系。编译器的编写者更经常遇到的情况是:正在实现的语言是众所周知的并已有了语言定义。有时这个语言定义已达到了某个语言标准(language standard )的层次,语言标准是指得到诸如美国国家标准协会(American National Standards Institute ,ANSI )或国际标准化组织.int Organization for Standardization,ISO )的官方标准组织批准的标准。Fortran、 Pascal和c语言就具有ANSI标准,Ada有一个通过了美国政府批准的标准。在这种情况下,编译器的编写者必须解释语言的定义并执行符合语言定义的编译器。通常做到这一点并不容易,但是有时由于有了标准测试程序集(测试组(test suite )),就能够测试编译器(Ada有这样一个测试组),这又变得简单起来了。有时候,一种语言可从数学术语的形式定义(formal definition )中得到它的语义。现在人们已经使用了许多方法,尽管一个称作表示语义(denotational 语义学 )的方法已经成为较为常用的方法,在函数编程共同体中尤为如此,但现在仍然没有一种可成为标准的方法。当语言有一个形式定义时,那么在理论上就有可能给出编译器与该定义一致的数学证明,但是由于这太难了,而几乎从未有人做过。无论怎样,运行时环境的结构和行为是尤其受到语言定义影响的编译器构造的一个方面。

编辑器

编辑器(editor):编译器通常接受由任何生成标准文件(例如ASCII文件)的编辑器编写的源程序。现在,编译器已与另一个编辑器和其他程序捆绑进一个交互的开发环境-IDE中。此时,尽管编辑器仍然生成标准文件,但会转向正被讨论的程序设计语言的格式或结构。这样的编辑器称为基于结构的(structure based ),且它早已包括了编译器的某些操作;因此,程序员就会在程序的编写时而不是在编译时就得知错误了。从编辑器中也可调用编译器以及与它共用的程序,这样程序员无需离开编辑器就可执行程序。

课程

这门课程关注的是编译器方面的产生原理和技术问题,似乎和计算机的基础领域不沾边,可是编译原理却一直作为大学本科的 必修课程,同时也成为了研究生入学考试的必考内容。编译原理及技术从本质上来讲就是一个算法问题而已,当然由于这个问题十分复杂,其解决算法也相对复杂。我们学的数据结构与算法分析也是讲算法的,不过讲的基础算法,换句话说讲的是算法导论,而编译原理这门课程讲的就是比较专注解决一种的算法了。在20世纪 50年代,编译器的编写一直被认为是十分困难的事情,第一Fortran编译器据说花了18年的时间才完成。在人们尝试编写编译器的同时,诞生了许多跟 编译相关的理论和技术,而这些理论和技术比一个实际的编译器本身价值更大。就犹如数学家们在解决著名的哥德巴赫猜想一样,虽然没有最终解决问题,但是其间 诞生不少名著的相关数论

编译过程概述

c语言的源程序和对应的可执行程序执行时在内存中的运行结构,实现这一转换的最主要的过程就是编译。

源程序是给人看的,本质上就是文本文件,可以用Linux中的vi或Windows中的记事本之类的文本编辑程序打开、编写,但计算机无法直接执行源程序,需要通过一个专门的程序将源程序编译为计算机可执行程序,这个专门的程序就是编译器。编译过程主要分为词法分析、语法分析、中间代码生成、目标代码生成(忽略预处理、语义分析、优化等)。下面我们依次简要讲解编译的主要过程。

词法分析

人眼中看到的源代码是这样的:

这里看不出关键字、运算符、标识符,甚至看不出哪几个字符属于同一个符号。编译的第一阶段是词法分析,目的就是在连续的字符中识别出一个一个的符号,并尽可能地识别出符号的属性。

人们在看c语言源程序时,借助空格、括号等一眼就可以看出标识符、关键字。与人相比,现在计算机的智能还是相当低的,无法像人那样同时看多个字符,只能依据一个非常机械的“电子版”词法,一个字符一个字符地识别。“电子版”词法是将状态转换图的思路融汇在flex词法分析器的代码中得以体现的。词法分析器从源程序的字符串中识别出一个个符号(token),并按序保存。

在词法分析阶段能够识别出一些符号的含义,它们包括关键字、数字、字符串、分隔符,这些符号的含义不需要其他符号的辅助就能确定本身的含义。比如,“int”一定代表整数类型,它不可能是一个变量名称,也不可能是一个运算符。

而另外一些符号需要通过前后的其他符号才能确定出准确含义。比如“m”,在词法阶段仅能判断这是一个标识符,但是如果不对所在的句做分析,就无法得知这个标识符代表一个变量还是一个函数。更多详细的信息需要对符号所在的句型做分析才能获得。这部分工作由语法分析来完成。

语法分析

如果说词法分析的作用是从连续的字符中识别出标识符、关键字、数字、运算符并存储为符号(token)流,语法分析的作用就是从词法分析识别出的符号流中识别出符合c语言语法的语句。

因为计算机无法像人那样同时看多个标识符、关键字、数字、运算符,无法像人那样一眼看出这是一个函数声明,那是一个if语句,只能非常笨拙地一个符号一个符号去识别。与词法分析器有些类似,语法分析器也是依据用计算机表示的语法,一个符号一个符号地识别出符合C语言语法的语句。语法的计算机表示就是产生式。在语法分析器中把通过产生式产生的C语言语法映射成一套模板,并把这套模板融汇在语法分析器的程序中。语法分析器的作用就是将词法分析器识别出的符号(token)一个一个地与这套模板进行匹配,匹配上这套模板中的某个语法,就可以识别出一句完整的语句,并确定这条语句的语法。

我们以案例中“int fun(int a,int b);”这条函数声明语句为例描述这个过程,它与语句模板的匹配情景如图1-38所示。

这段token流最终与函数声明模板相匹配,在匹配成功后,计算机就认为此语句为函数声明语句,并以语法树的形式在内存中记录下来。

以树型结构来记录分析出的语句是一个非常巧妙、深谋远虑、通盘考虑的选择。一方面,树型结构能够“记住”源程序全部的“意思”,另一方面,树型结构更容易对应到运行时结构。

从语法树到中间代码再到目标代码

至此,语法树已经承载了源程序的全部信息,后续的转换工作就和源程序没关系了。

如果希望一步到位,从语法树转换为目标代码,理论上和实际上都是可行的。但计算机存在多种CPU硬件平台,考虑到程序在不同CPU之间的可移植性,先转换成一个通用的、抽象的“CPU指令”,这就是中间代码最初的设计思想。然后根据具体选定的CPU,将中间代码落实到具体CPU的目标代码。

语法树是个二维结构,中间代码是准一维结构,语法树到中间代码的转换过程,本质上是将二维结构转换为准一维结构的过程。中间代码特别是RTL已经可以和运行时结构一一对应了。

运行时结构也是一维的,图1-40左侧部分的转换结果将更贴近运行时结构。

选定具体的CPU、操作系统后,中间代码就可以转换为目标代码——汇编代码(我们配置的是AT\u0026T汇编),这时操作系统的影响还比较小。然后由汇编器依照选定操作系统的目标文件格式,将.s文件转换为具体的目标文件,对于Linux而言是.o文件,对于Windows而言是.obj文件。目标文件中已经是选定的CPU的机器指令了。

最后链接器把一个或多个目标文件(库文件本质上也是目标文件)链接成符合选定操作系统指定格式的可执行文件。

通过操作系统,可执行程序就可以被载入内存执行,形成前两节我们所看到的运行时结构。

本书后续内容将详细讲解编译的主要过程:词法分析、语法分析、中间代码到目标代码,然后是汇编与链接,最后讲解预处理。

参考资料

免责声明
隐私政策
用户协议
目录 22
0{{catalogNumber[index]}}. {{item.title}}
{{item.title}}
友情链接: