自制编程语言,六个令你迷惑的问题

自制编程语言和虚拟机,这是一个看似很深奥的课题,也涉及当今互联网流行的主题,许多技术人员对其心驰神往,但要领悟其精髓寸步难行。php

《自制编程语言》按部就班、由浅到深地讲解了丰富的基础知识,覆盖了常见的编译原理入门知识,更难能难得的是,做者讲解的知识具备其独特的理解和视角,相信本书能让读者可以受益不浅。前端

                                                    

本文涉及一些编译原理基础,我担忧没学过编译原理的读者会以为吃力,所以顺带介绍了编译原理的基础知识。固然,不会编译原理也没法阻止你成功写出一门脚本语言。
python

由于原理太抽象了,并且为了严谨,理论老是把简单的描述成复杂的。在实践中你会发现,编译器的实现比理解编译器原理容易,你会发现——原来晦涩难懂的概念其实就是这么简单,以致于你是经过实践才懂得了编译原理。毕竟纸上得来终觉浅,绝知此事要躬行。今天咱们来介绍一些自制编程语言可能使人迷惑的问题。web

编译型程序和脚本程序的异同
shell

二者最明显的区别就是看它们各是谁的“菜”。二者的共性是最终生成的指令都包含操做码和操做数两部分。编程

编译型程序所生成的指令是二进制形式的机器码和操做数,即二进制流。一样是数据,和文本文件相比,这里的数据是二进制形式,并非文本字符串(如ASCII码或unicode等)形式。后端

若是二进制流按照有无格式来划分,无格式的即是纯粹的二进制流,程序的入口即是文件的开始。另一种是按照某种协议(即格式)组织的二进制流,好比Lnux下elf格式的可执行文件。它是硬件CPU的直接输入,所以硬件CPU是“看获得”编译型程序所对应的指令的,CPU亲自执行它,即机器码是CPU的菜。数组

编译型语言编译出来的程序,运行时自己就是一个进程,它是由操做系统直接调用的,也就是由操做系统加载到内存后,操做系统将CS:IP寄存器(IA32体系架构的CPU)指向这个程序的入口,使它直接上CPU运行,这就是所说的CPU“看获得”它。总之调度器在就绪队列中能看到此进程。浏览器

脚本语言,也称为解释型语言,如JavaScript、Python、Perl、Php、Shell脚本等。它们自己是文本文件,是做为某个应用程序的输入,这个应用程序是脚本解释器。因为只是文本,这些脚本中的代码在脚本解释器看来和字符串无异。缓存

也就是说,脚本中的代码历来没真正上过CPU去执行,CPU的CS:IP寄存器历来没指向过它们,在CPU眼里只看获得脚本解释器,而这些脚本中的代码,CPU历来就不知道有它们的存在,脚本程序却因硬件CPU而间接“运行”着。

这就像家长给孩子生活费,孩子用生活费养了只狗狗,家长只关心孩子的成长,从不知道狗狗的存在,但狗狗却间接地成长。这些脚本代码看似在按照开发人员的逻辑在执行,本质上是脚本解释器在时时分析这个脚本,动态根据关键字和语法来作出相应的行为。

解释器有两大类,一类是边解释边执行,另外一类是分析完整个文件后再执行。若是是第一类,那么脚本中如有语法错误,先前正确的部分也会被正常执行,直到遇到错误才退出;若是是第二类,分析整个文件后才执行的目的是为了建立抽象语法树或者是用与之等价的遍历去生成指令,有了指令以后再运行这些指令以表示程序的执行,这一点和编译型程序是一致的。

脚本程序所生成的指令是文本形式的操做码和操做数,即数据以文本字符串的形式存在。其中的操做码称为opcode,一般opcode是自定义的,因此相应的操做数也要符合opcode的规则。为了提升效率,一个opcode的功能每每至关于几百上千条机器指令的组合。

若是虚拟机不是为了效率,多半是用于跨平台模拟程序运行。这种虚拟机所处理的opcode就是另外一体系架构的机器码,好比在x86上模拟执行MIPS上的程序,运行在x86上的虚拟机所接收的opcode就是MIPS的机器码。

除跨平台模拟外,一般虚拟机的用途是提升执行效率,所以opcode不多按照实际机器码来定义,不然还不如直接生成机器指令交给硬件CPU执行更快呢。故此种自定义的指令是虚拟机的输入,即所谓虚拟机的菜。

虚拟机分为两大类,一类是模拟CPU,也就是用软件来模拟硬件CPU的行为,这种每每是给语言解释器用的,好比Python虚拟机。另外一类是要虚拟一套完整的计算机硬件,好比用数组虚拟寄存器,用文件虚拟硬盘等,这种虚拟机每每是用来运行操做系统的,好比VMware,由于只有操做系统才会操做硬件。

脚本程序是文本字符流(即字符串),其以文本文件的形式存储在磁盘上。具体的文本格式由文本编译器决定,执行时由解释器将其读到内存后,逐行语句地分析并执行。

执行过程多是先生成操做码,而后交给虚拟机逐句执行,此时虚拟机起到的就是CPU的做用,操做码即是虚拟机器的输入。

固然也能够不经过虚拟机而直接解析,由于解析源码的顺序就是按照程序的逻辑执行的顺序,也就是生成语法树的顺序,所以在解析过程当中就能够同时执行了,好比解析到 2+3 时就能够直接输出 5 了。

但方即是有限的,实现复杂的功能就不容易了,由于计算过程当中须要额外的数据结构,比较对于函数调用来讲总该有个运行时栈来存储参数和局部变量以及函数运行过程当中对栈的需求开销。所以对于复杂功能,多数状况下仍是专门写个虚拟机来完成。

顺便猜测一下解释型语言是如何执行的。咱们在执行一个PHP脚本时,其实就是启动一个C语言编写出来的解释器而已。这个解释器就是一个进程,和通常的进程是没有区别的,只是这个进程的输入则是这个PHP脚本。在PHP解释器中,这个脚本就是个长一些的字符串,根本不是什么指令代码之类。

只是这种解释器了解这种语法,按照语法规则来输出罢了。举个例子,假设下面是文件名为a.php的PHP代码。

php解释器分析文本文件a.php时,发现里面的echo关键字,将其后面的参数获取后就调用C语言中提供的输出函数,好比printf((echo的参数))。PHP解释器对于PHP脚本,就至关于浏览器对于JavaScript同样。

不过这个彻底是我猜想的,我不知道PHP解释器里面的具体工做,以上只是为了说清楚个人想法,请你们辩证地看。

说到最后,也许你有疑问,若是CPU的操做数是字符串的话,那CPU就能直接执行脚本语言了,为何CPU不直接支持字符串做为指令呢?后面会有分享。

脚本语言的分类

脚本语言大体可分为如下4类。

(1)基于命令的语言系统

在这种语言系统中,每一行的代码实际上就是命令和相应的参数,早期的汇编语言就是这种形式。此类语言系统编写的程序就是解决某一问题的一系列步骤,程序的执行过程就是解决问题的过程,就像作菜同样,步骤是提早写好在脑子里(或菜谱中)的。如如下炒菜脚本。

 

以上步骤中第1列都是命令,后面是命令的参数。其中把菜放进锅后不断地搅拌(示意而已,不用太严谨),因为命令式语言系统中没有循环语句,须要连续填入多个stir以实现连续多个相同的操做。会有一个解释器逐行分析此文件,执行相应命令的处理函数。如下是一个解释器示例。

   

(2)基于规则的语言系统

此类语言的执行是基于条件规则,当知足规则时便触发相应的动做。其语言结构是谓词逻辑→动做,如图1-1所示。

                                             

                                                                  图1-1

所以此类语言常称为逻辑语言,经常使用于天然语言处理及人工智能方面,典型的表明有Prolog。

(3)面向过程的语言系统

面向过程的语言系统咱们都比较熟悉,批处理脚本和shell脚本,perl、lua等属于此类,和基于命令的语言系统相比,它能够把一系列命令封装成一个代码块供反复调用。此代码块即是借用了数学中函数的概念,一个x对应一个y,即给一个输入便有一个输出,因而这个代码块便称为函数。

(4)面向对象的语言系统

现代脚本语言基本上都是面向对象,大伙儿用的都挺多的,好比python。不少读者误觉得只要语言中含有关键字class,那么该语言就是面向对象的语言,这就不严谨了。由于在perl语言中也能够经过关键字class定义一个类,但其内部实现上并非彻底面向对象,其本质是面向过程的语言。世界上第一款血统纯正的面向对象语言是smalltalk,它在实现上就是一切皆对象,具备彻底面向对象的基因。

为何CPU要用数字做为指令

在以前小节“编译型程序和脚本程序的异同”的结束处咱们讨论过,为何CPU不直接支持字符串做为指令。我估计有的读者会误觉得CPU将直接执行汇编代码,这是不对的,由于汇编代码是机器码的符号化表示,几乎是与机器码一一对应,但汇编代码绝对不是机器语言。

你想,若是汇编代码是机器指令的话,那么CPU看到的输入即是字符串,好比如下汇编代码用于计算1+10-2。

汇编语言实际上是汇编器的输入,对于汇编器来讲,汇编代码文件也是文本,所以其中mov指令也是字符串。若是让CPU直接读取汇编文件逐行分析各类字符串以判断指令,这效率必然很是低下。

毕竟要比较的字符数太多,比较的次数多了效率固然就低了,所以把指令编号为数字,这样比较数字多省事。并且最主要的是,CPU更擅长处理数字,它自己的基因就是数字电路,数字计算是创建在数值处理的基础上,这就是本质上二进制数据比文本ASCII码更快更紧凑的缘由。

为何脚本语言比编译型语言慢

而脚本语言的编译有两类,一类是边解释边执行,不产生指令,这个解释过程最占时间的部分就是字符串的比较过程,字符串比较的时间复杂度是O(n),也就是在比较n次以后解释器才肯定了操做码是什么,而后再去获取操做码的操做数,你看能不慢吗?而编译型语言编译后是机器码,是二进制数字,所以可直接上CPU运行,而CPU擅长处理数字,比较一次数字即可肯定操做码。

另外一类脚本语言是先编译,再生成操做码,最后交给虚拟机执行,这样多了一个生成操做码的过程,彷佛“显得”更慢了。其实这都不是主要的。

你看,程序“执行”速度的快慢是比较出来的,编译型语言在执行时已是二进制语言了,而大多数脚本语言在执行时仍是文本,必然要先有个编译过程。

这里面全是字符串处理,整个脚本的源码对于编译器来讲就是一个长长的字符串,都要完整地进行各类比较,所以多了一个冗长的步骤,必然要慢。有些脚本系统为减小编译的过程,第一次编译后将编译结果缓存为文件,如Python会将.py文件编译后存储为.pyc文件,下次无须编译直接运行即可。

可是,这样无须二次编译的脚本语言就能和编译型程序媲美吗?不见得磁盘IO是整个系统最慢的部分,解释器读取缓存文件难道不须要时间吗?等等,有读者说了,编译型的程序被操做系统加载时也要从磁盘上读取啊,这不同吗?

固然不同,别忘了,脚本程序在执行时先要加载解释器,解释器也是位于硬盘上的文件,只是二进制可执行文件而已,依然须要读取硬盘,而后解释器再去从硬盘上读取脚本语言文件并编译脚本文件。

你看,编译型程序在执行时只有1个IO,而脚本程序在执行时有两个,比前者多了1个低速的IO操做,所以,脚本语言更慢一些是注定的。

既然脚本语言比较慢,为何你们还要用

这里的语言是指语言的编译器或解释器,如下简称为语言。

语言慢并不影响整个系统,影响整个系统速度的短板并非语言自己,目前来讲系统的瓶颈广泛是在IO部分。语言再慢也比IO快一个数量级,并非语言执行速度快10倍后整个系统就快10倍,语言慢了,整个系统依然不受影响,这要看瓶颈是哪块儿。

这就像动物园运送动物的船超载了,人们不会埋怨某些人太胖了,而是清楚地知道占份量的主要是船上的大象,人的体重和大象根本就不是一个量级。

再说,即便是语言提速后,因为IO这块跟不上,依然会被阻塞(因为是脚本语言,这里阻塞的是脚本解释器),并且因为语言太慢而显得阻塞时间更漫长。

为何会阻塞呢?这种阻塞每每是因为程序后续的指令须要从IO设备读取到的数据,也就是说程序后面的步骤依赖这些数据,没这些数据程序运行没意义。好比说Web服务器先要读取硬盘上的数据而后经过网卡发送给用户,必须得到硬盘数据后,web服务器进程中那部分操做网卡发送数据的指令才能上CPU上执行。

因为语言的解释器是由CPU处理的,CPU速率确定比IO设备快太多,所以在等待IO设备响应的过程当中啥也干不了。操做系统为了让宝贵的CPU资源获得最大的利用,确定会把进程(二进制可执行程序或脚本语言的解释器)加入阻塞队列,让其余可直接运行的、不须要阻塞的进程使用CPU(阻塞指的是并不会上CPU运行,也就是将该进程从操做系统调度器的就绪队列中去掉)。

而语言(脚本语言解释器)再慢也比IO设备快,所以依然会由于更慢的IO而难逃阻塞的命运。也就是说,拖慢整个系统后腿的必定是系统中最慢的部分,而不管脚本语言多慢,IO设备老是会比语言更慢,所以“影响系统性能”这个黑锅,脚本语言不能背。

另外一方面大伙儿喜欢用脚本语言的缘由是开发效率高,这也是脚本语言被发明的初衷,不少在C中须要多个步骤才能实现的功能在脚本语言中一句话就搞定,固然更受开发人员欢迎了。

什么是中间代码

不少编译器会将源语言先编译为中间代码,最后再编译为目标代码,但中间语言并非必需的。中间代码简称IR,是介于源程序和机器语言之间的语言,有N元式(如三元式、四元式)、逆波兰、树等形式。

目标代码是指运行在目标机器上的代码,与目标机器的体系架构直接相关,编译器干嘛不直接生成目标代码,多这一道程序有什么好处呢?

(1)能够跨平台

因为中间代码并非目标代码,所以能够做为全部平台的公共语言,从而可经过中间代码实现先后端分离。好比在多平台、多语言的环境下开发可提升开发效率,只要在某一平台上编译出中间代码后,中间代码到目标代码的剩余工做能够由目标平台的编译器继续完成。

(2)便于优化

中间代码更接近于源代码,对于优化来讲更直接有效。并且能够在一种平台上优化好中间代码,再发送到其余平台编译为目标机器,提升优化效率。

什么是编译器的前端、后端

编译器的先后端是由中间代码来划分的,如图1-2所示。

                       

                                                                     图1-2

前端主要负责读取源码,对源码进行预处理,经过词法分析把单词变成Token流,而后进行语法分析,语义分析,将源码转换为中间代码。

后端负责把中间代码优化后转换为目标代码。

词法分析、语法分析、语义分析和生成代码并非串行执行

不少教材上会把编译阶段分为几个独立的部分:

(1)词法分析;

(2)语法分析;

(3)语义分析;

(4)生成中间代码;

(5)优化中间代码;

(6)生成目标代码。

这容易给人形成“这几个步骤是串行执行”的错觉,即“从源码到目标代码必需要顺序地执行这6个步骤”,其实不是这样子的,至少一个高效的编译器毫不会这样作。

这只是在功能逻辑上的步骤,就拿前4步来讲,它们是以语法分析为主线,以并行的、穿插的方式在一块儿执行的,即这4个步骤是随语法分析同时开始,同时结束。

每一个步骤的功能实现由其实际的模块完成,负责词法分析的模块称为词法分析器,负责生成代码的模块称为代码生成器,负责语法分析的模块称为语法分析器。

咱们所说的编译器就是由词法分析器、语法分析器和代码生成器组成的(若是有目标代码优化的话还包括优化模块)。

编译工做的入口是语法分析,所以编译是以调用语法分析器为开始的,语法分析器会把词法分析器和代码生成器视为两个子例程去调用。换句话说,词法分析器和代码生成器只会被语法分析器调用,若是没有语法分析器,它们就没有“露脸儿”的机会。

所以说编译是以语法分析器为主线,由语法分析器穿插调用词法分析器和代码生成器并行完成的。

语法分析和语义分析尽管是两个功能,但这其实能够合并为一个。由于在语法分析事后便知道了其语义。这个很好理解,毕竟语法就是语义的规则,规则是由编译器(的设计者)制定的,那么编译器(的设计者)分析了本身设定的规则后固然就明白了语义(不可能不明白本身所制定规则的意义)。

好比读英文句子,尤为是复杂的长句,先找到句子谓语动词,以谓语动词为分界线把句子拆分主谓两大部分,在前一部分中找主语,后一部分中找宾语等,在分析完语法后句子的意思就搞清楚了。

也就是说,语法分析和语义分析是同时,又是先后脚的事儿,所以合并到一块儿并不奇怪。你看,语法分析和语义分析确实是并行。

为了语法分析的效率,词法分析器每每是做为一个子例程被语法分析器调用,即每次语法分析器须要一个单词的token时就调用词法分析器。你看,语法分析和词法分析确实也是并行。

最后说生成代码。目前生成代码的方式叫语法制导,什么是语法制导呢?就是在分析语法的“同时”生成目标代码或中间代码,实际上就是以语法分析为导向,语法分析器在了解源码语义后当即调用代码生成器生成目标代码或中间代码,所以这也是和语法分析器并行。

提醒一下,并非在语法分析器分析完整个源码后,再一次性地生成整个源码对应的目标代码或中间代码,而是分析一部分源码后就当即生成该部分源码对应的目标代码或中间代码,这样作比较高效且更容易实现。

举个例子,好比源码文件中有10行代码,语法分析器不断调用词法分析器,每次得到一个单词的token,把前3行源码都读完后肯定了源码的语义,当即生成与这3行源码同等意义的目标代码或中间代码。

而后语法分析器继续调用词法分析器读取第4行以后的源码,重复分析语法、生成代码的过程。总之是以语法分析为主线,语法分析把源码按照语法来拆分红多个小部分,每次生成这一小部分的目标代码或中间代码。

总结,为了使编译更加高效,词法分析、语法分析、语义分析和生成代码是以语法分析为中心并行执行的,词法分析和生成代码都是被语法分析器调用的子例程。

什么是符号表

把符号表列出来是由于这个词听上去“挺唬”人的,因为看不见摸不着,不少初学者都觉得它是个很是神秘的东西。其实符号表就是存储符号的表,就是这么简单。

你想,源码中的那些符号总该存储在某个地方,这样在引用的时候才能找获得,所以符号表的用途就是记录文件中的符号。符号包括字符串、方法名、变量名、变量值等。符号放在表中的另外一个重要缘由是便于生成指令,使指令格式统一。

编译器会把符号在符号表中的索引做为指令的操做数,若是不用索引的话,指令就会很乱,好比若直接用函数名或字符串做为操做数,指令就冗长了。“表”在计算机中并不专指“表格”,“表”是个笼统的概念,用以表示一切可供增、删、改、查的数据结构,所以符号表能够用任何结构来实现,好比链表、散列表、数组等。

                                                      

                                                              《自制编程语言》

                                                                    郑钢 著

本书全面从脚本语言和虚拟机介绍开始,讲解了词法分析的实现、一些底层数据结构的实现、符号表及类的结构符号表,常量存储,局部变量,模块变量,方法存储、虚拟机原理、运行时栈实现、编译的实现、语法分析和语法制导自顶向下算符优先构造规则、调试、查看指令流、查看运行时栈、给类添加更多的方法、垃圾回收实现、添加命令行支持命令行接口。

                                                     

                                                         《操做系统真象还原》

                                                                   郑钢 著

大学及研究生都有操做系统课程,这类人群具备很高的学术能力,但书中讲的过于抽象与晦涩,以致于不少学生对于此门课程恐惧到都提不出问题,只有会的人才能提出问题。操做系统理论书是没法让读者理解什么是操做系统的,学操做系统不能靠想像,他们须要看到具体的东西。

绝大多数技术人都对操做系统怀着好奇的心,他们渴望一本告诉操做系统究竟是什么的书,里面不要掺杂太多无关的管理性的东西,代码量不大且是现代操做系统雏形,他们渴望很快看到本质而不花费大量的时间成本。

今日互动

你想为本身的成长挑选哪本书?为何?截止9月5日17时,留言+转本活动到朋友圈,小编将抽奖选出2名读者赠送纸书1本。(参与活动直达微信端自制编程语言,六个令你迷惑的问题

点击阅读原文,直接购买《自制编程语言》

阅读原文

相关文章
相关标签/搜索