本章描述了本书的目标和用到的方法并鸟瞰编译器和解释器的全貌。
目标和方法
本书讲授编译器和解释器的基本写法,目标是呈现给你怎样设计和开发它们:
- 用Java写的编译器,编译Pascal(一个高级的面向过程的编程语言)的一个主要子集。
(即包含主要的语言特征,但去掉一些为写编译器方便而去掉的无关大雅的特性。)
- 用Java写的解释器且包含一个交互式的符号调试器
(符号调试器即基于符号表,而不是基于机器的指令集、硬件的调试功能)
,解释同样的Pascal语言子集。 - 带图形用户界面的集成开发环境(IDE)。这个IDE是你看到的功能全面的开源的Eclipse或者Borland的JBuilder等IDE的一个简化版。不过,它也包含一个源程序编辑器和一个交互界面用来设置断点,单步调试,查看和修改变量值等等其它。
达成这些个极具野心的目标是个大挑战。好的技能将会帮你如何如把程序编译成为机器语言或解释执行程序。现代软件工程法则和优秀的面向对象设计思想将会给你呈现怎么通过代码实现一个编译器或解释器而最终所有组件能良好协作。编译器和解释器程序大且复杂。开发个小程序仅需要某种技能即可,然NB的程序如编译器或解释器还需要软件工程法则和面向对象设计。因此本书强调必备技能,软件工程法则和面向对象思想。
什么是编译器和解释器
编译器和解释器的主要目的是“翻译”由高阶(High-Level)源语言写的源程序。把源程序翻译成什么样是接下几个段落的主题。
本书中源语言为Pascal的一个大子集,换句话说,你能够编译或解释正规的Pascal程序。因为编译器和解释器是用Java写的,实现语言是Java。
Pascal编译器将Pascal源程序翻译成为低阶(Low-Level)的某具体机器的机器语言(更准确的讲是CPU的机器语言)
。通常源程序是文本格式。如果编译器工作正常,对应的机器语言和最初的Pascal源程序殊路同归(一样的行为,只不过呈现方式不一样。比如你用钥匙而偷车的直接电线打火发动汽车一样)
。机器语言是目标语言,编译器生成用机器语言组成目标代码。代码生成之后,编译器任务就算完成。目标代码一般写到文件里(一般是二进制文件)
。
一个程序可包含数个源文件,而编译器为每个文件生成一个目标文件。一个名叫“链接器”(linker)的辅助程序将这些目标文件的内容连同运行时库程序合成到一个计算机能够加载和执行的目标程序(如windows的PE程序)
中。库程序一般来自于预先编译好的目标文件。
因为机器语言不好记,编译器可生成汇编语言作为目标语言,汇编语言离机器语言只有一步之遥。通常每个汇编指令都有机器语言的指令与之对应。如果你掌握了短助记名(比如ADD和MOV等)
汇编语言好记多了。汇编器(另一个编译器)
将汇编语言翻译成为机器语言。
下图概括了将一个或多个源程序编译成为目标程序的过程。
上图展示了将一个包含三个源文件sort1.pas、sort2.pas、sort3.pas的Pascal程序翻译成为三个相应机器语言目标文件sort1.obj、sort2.obj、sort3.obj。链接器将三个目标文件(连带相关运行时库)
合成为一个可执行的目标程序sort.exe。图右边展示了编译器将Pascal源文件翻译为汇编语言目标文件sort1.asm、sort2.asm、sort3.asm,接着汇编器将其转化为机器语言目标文件。最后链接器产生目标程序sort.exe。
那么编译器和解释器到底有和不同?
解释器不生成任何目标程序,相反它读进源程序就会执行。这好比你被一个Pascal程序把住手,按照它说的某种语句读进顺序去做。你可以在一张草稿纸上记下程序的变量值直到程序结束才输出每条语句的输出结果。本质上你做的正是Pascal解释器干的事情。Pascal解释器读进程序,执行程序。没有任何目标程序需要生成和加载,相反,解释器将程序翻译成为一系列用来执行程序的动作(Action)。
比较编译器和解释器
该如何决策何时用编译器和何时用解释器?
当你把一个源程序交给解释器,解释器接管检查和执行。编译器也检查但生成目标代码。运行完编译器之后还有运行链接器产生目标程序,且还需加载目标程序到内存中去执行它。如果编译器生成汇编语言代码,你还得运行汇编器。所以很显然解释器需要更少步骤。
解释器比编译器更常见。你可用Java写个Pascal解释器运行在基于微软Windows的PC上,苹果的MAC或某个Linux主机上,解释器能够在前面提到的平台上执行Pascal程序。而编译器必须为某个具体的机器生成代码(无论直接生成或间接通过汇编器生成)
。所以即使你要把原来为PC写的Pascal编译器放到MAC上运行,它生成的代码仍旧是PC的,如果想让它为MAC生成代码,你可能得重写编译器的某些部分。
接下来讨论的编译器将问题的重心放在为Java虚拟机生成代码上,因为虚拟机能够运行在很多平台上。所以为具体机器生成代码先放一边,有兴趣可以将虚拟机替换成为真实PC机上生成x86指令看看。
如果源程序中包含逻辑错误,比如除值为0的变量,直到运行时才发现,那么会发生什么情况?
因为解释器在执行程序过程中控制一切,它能停下来告诉你出问题的行数和变量名称。它甚至能提示你在继续执行程序之前可以做哪些正确操作比如修改变量值为非零。解释器可包含一个交互式的源级(source-level)
调试器,俗称符号调试器(symbolic debugger)
。符号调试器意味着你可用程序中的符号,比如变量名。
另一方面,由编译器和链接器产生的目标程序通常自我运行(由机器执行,无需第三方)
。源程序有关行号和变量名等信息在目标程序中不可见。当运行时抛错,程序简单中断,还可能打印一条包含出问题指令地址的消息。于是找出源程序中相关语句变量除零的问题就交给你了。
所以通常就调试来说,解释器才是正道。有些编译器在目标代码中添加一些额外的信息,这样当错我发生时,目标程序能打印出相应的问题行数和变量名等。于是你改正错误,重新编译,然后重新运行。生成额外的信息会导致程序执行的比正常要慢(这也是Visual C++为什么有Run/Debug编译模式)
。这提示你在认为程序到达最终“产品”版本后,应关掉调试特征重新编译。
假设你已经成功调试好程序,那重点将是怎样使运行更快。因为机器能够以最快速度执行原生机器语言程序,编译程序能够比解释器快好几个量级。显然就速来来说编译器是胜者,当优化版编译器知道怎么生成具体场景的优化代码的情况下尤其确定。所以是否使用编译器或解释器取决于程序的开发和执行谁更重要。理想情况是一个带符号源级调试器的解释器用在开发过程中,一个生成机器代码的编译器在程序调试OK之后以求更快的执行速度。这些就是本书的目标,因为它编译器,解释器都教。
情景变得有点模糊
编译器和解释器的差异很容易说明清楚,但是随着虚拟机的快速流行,情景变得有点模糊。
虚拟机是一个用来模拟机器(计算机)
的程序。此程序能够运行在不同的真实计算机平台上。举个例子,Java 虚拟机(JVM)能够运行在基于微软Windows的PC上,苹果的MAC,Linux系统和其它很多平台上。(比如Sparc,IBM小型机等)
。
虚拟机有自己的虚拟机器语言,而虚拟语言指令被真实宿主机所解释。那么如果你写了一个翻译器将Pascal源程序翻译成为被宿主机解释的虚拟机语言,这个翻译器算编译器还是解释器?
不斤斤计较了,我们本书约定如果一个翻译器将源程序转化成为机器语言,不管是真实的机器语言还是虚拟机器语言,那么这个翻译器就是编译器。翻译器没有优先生成机器语言去执行程序的就算解释器。
声明:本文转载自博客园Bang老师,为英版原书《Writing Compilers and Interpreters 3rd Edition》译文。转载只为学习。博客地址
至今与Bang老师联系不上,译文阅读量极低,故为扩大该译文的影响力,笔者擅自转载。由于Bang老师只译到第十一章,但最后的九个章节笔者会自己翻译。