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

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

  自制编程语言和虚拟机,这个看似深奥的课题,也涉及到当今互联网的热门主题。很多技术人员对它着迷,却很难理解它的本质。

  055-79000循序渐进讲解丰富的基础知识,由浅入深,涵盖编译原理常用入门知识。更何况作者讲解的知识有其独特的理解和视角。相信这本书能让读者受益匪浅。

  本文涉及到一些编译的基本原理,担心没学过编译原理的读者会觉得吃力,所以顺便介绍一下编译原理的基础知识。当然,不会编译原理并不能阻止你成功地写出一门脚本语言。

  因为原理太抽象,而且为了严谨,理论总是把简单说成复杂。在实践中,你会发现编译器的实现比理解编译器原理更容易,你会发现——这个原本晦涩难懂的概念其实很简单,你只有通过实践才知道编译器原理。毕竟纸上谈兵很容易学,永远不知道怎么做。今天就来介绍一些自制编程语言容易混淆的问题。

  编译器和脚本的异同

  两者最明显的区别就是看是谁的“菜”。两者的共同点是最终生成的指令包含两部分:操作码和操作数。

  编译器生成的指令是二进制形式的机器码和操作数,即二进制流。是同样的数据。与文本文件相比,这里的数据是二进制形式,而不是文本字符串形式(如ASCII码或unicode等。).

  如果按照有无格式来划分二进制流,无格式流就是纯二进制流,程序的入口就是文件的开头。另一种是按照一定协议(即格式)组织的二进制流,比如Lnux下elf格式的可执行文件。是硬件CPU的直接输入,所以硬件CPU可以“看到”编译程序对应的指令,CPU亲自执行,也就是机器码是CPU的菜。

  C语言编译的程序本身就是一个运行时的进程,由操作系统直接调用,即操作系统将其加载到内存后,操作系统将CS:IP寄存器(IA32架构的CPU)指向这个程序的入口,使其直接运行在CPU上,也就是说CPU可以“看到”它。简而言之,调度程序可以在就绪队列中看到这个进程。

  脚本,也称为解释性语言,如JavaScript、Python、Perl、Php、Shell脚本等。它们本身是文本文件,它们是应用程序的输入,应用程序是脚本解释器。因为它只是文本,所以对于脚本解释器来说,这些脚本中的代码看起来像字符串。

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

  就像父母给孩子生活费一样。孩子养一只狗有生活费。父母只关心孩子的成长。他们从来不知道狗的存在,但狗是间接成长的。这些脚本代码看似是按照开发者的逻辑在执行,但本质上是脚本解释器时不时对这个脚本进行分析,根据关键字和语法动态做出相应的行为。

  解释器有两种,一种是解释执行,一种是执行前分析整个文件。如果是第一类,如果脚本有语法错误,会正常执行前面正确的部分,直到遇到错误才会退出脚本;如果是第二种,在分析整个文件后执行的目的是创建一棵抽象语法树,或者通过等价遍历生成指令。有了指令之后,运行这些指令来表示程序的执行,这与编译后的程序一致。

  脚本生成的指令是文本形式的操作码和操作数,即数据以文本字符串的形式存在。的操作码称为opcode,通常opcode是用户自定义的,所以对应的操作数也要符合opcode的规则。为了提高效率,一个操作码的功能往往相当于成百上千条机器指令的组合。

  虚拟机如果不是为了效率,大多是用来运行跨平台的模拟程序。这个虚拟机处理的操作码是另一个架构的机器码。比如MIPS上的程序是在x86上模拟的,运行在x86上的虚拟机收到的操作码就是MIPS的机器码。

  除了跨平台模拟,虚拟机的目的通常是提高执行效率,所以很少根据实际的机器码来定义opcode,否则还不如直接生成机器指令交给硬件CPU执行。所以这个自定义指令就是虚拟机的输入,被称为虚拟机的菜。

  虚拟机分为两类,一类是模拟CPU,即用软件模拟硬件CPU的行为,语言解释器经常用到,比如Python虚拟机。另一种是虚拟化一套完整的计算机硬件,比如带数组的虚拟寄存器,带文件的虚拟硬盘。这种虚拟机通常用于运行操作系统,如VMware,因为只有操作系统才能操作硬件。

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

  执行过程可能是老师写好操作码,然后交给虚拟机逐句执行。这时候虚拟机就扮演了CPU的角色,操作码就是虚拟机的输入。

  当然也可以不用虚拟机直接解析,因为解析源代码的顺序就是程序逻辑执行的顺序,也就是生成语法树的顺序,所以在解析过程中可以同时执行。比如解析到2/3的时候,可以直接输出5。

  但便利性有限,不容易实现复杂的功能,因为计算过程中需要额外的数据结构。对于函数调用,总要有一个运行时栈来存储参数和局部变量,以及函数运行过程中栈的需求开销。所以对于复杂的函数,大多数情况下,还是写个虚拟机比较好。

  顺便猜猜解释性语言是怎么执行的。当我们执行一个PHP脚本时,我们只是启动一个用C语言编写的解释器。这个解释器是一个进程,和普通进程没什么区别,只不过这个进程的输入是这个PHP脚本。在PHP解释器中,这个脚本只是一个更长的字符串,根本不是指令代码。

  只是这个解释器理解这个语法,按照语法规则输出。例如,假设下面是名为PHP的PHP代码。

  php解释器在分析文本文件的php时,在其中找到echo关键字,得到下面的参数,然后调用C语言提供的输出函数,比如printf((echo的参数))。PHP解释器对于PHP脚本就像浏览器对于JavaScript一样。

  不过,这是我的猜测。PHP解释器里的具体工作我不清楚。以上只是想把我的思路说清楚。请辩证阅读。

  最后,也许你有一个问题。如果CPU的操作数是字符串,那么CPU可以直接执行脚本语言。为什么CPU不直接支持字符串作为指令?后面会有分享。

  文字语言的分类

  脚本语言大致可以分为以下四类。

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

  在这种语言体系中,每一行代码实际上都是一个命令和相应的参数,这是早期汇编语言的形式。这种语言系统编写的程序是解决某个问题的一系列步骤,程序的执行过程就是解决问题的过程。就像做菜一样,步骤是事先写在脑子里的(或者写在菜谱里)。比如下面这个烹饪剧本。

  上述步骤中的第一列都是命令,后面是命令的参数。其中菜放入锅内不断搅拌(只是为了说明,不要太严谨)。因为祈使句系统中没有循环句,所以需要连续填入多个stir才能连续实现多个相同的操作。会有一个解释器来逐行分析这个文件,执行相应命令的处理功能。下面是一个解释器的例子。

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

  这类语言的执行是基于条件规则的,当满足规则时,就会触发相应的动作。它的语言结构是谓词逻辑动作,如图1-1所示。

  图1-1

  因此,这类语言常被称为逻辑语言,常用于自然语言处理和人工智能中。普洛斯就是典型代表。

  (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上。比较一次数字,就可以确定操作码。

  先编译另一种脚本语言,然后生成操作码,最后交给虚拟机执行。这又增加了一个生成操作码的过程,看起来“显得”比较慢。其实这不是主要的。

  你看,程序“执行”的速度是比较出来的。编译语言执行的时候已经是二进制语言,而大多数脚本语言执行的时候还是文本,所以首先要有一个编译过程。

  它充满了字符串处理。整个脚本的源代码对于编译器来说就是一个很长的字符串,各种比较都要完整的做。所以,多了一个冗长的步骤,必然缓慢。为了减少编译过程,一些脚本系统在第一次编译后将编译结果缓存为文件。例如,Python将编译。py文件并将它们存储为。pyc文件,所以下次不用编译就可以直接运行。

  但是,这种不经过二次编译的脚本语言能和编译过的程序媲美吗?磁盘IO不一定是整个系统最慢的部分。解释器读取缓存文件不需要时间吗?等等,有读者说编译好的程序在操作系统加载的时候要从磁盘读取。那不是不一样吗?

  当然不一样。别忘了,脚本程序执行的时候必须先加载解释器。解释器也是一个位于硬盘上的文件,只是一个二进制的可执行文件。它还是需要读取硬盘,然后解释器从硬盘读取脚本语言文件,编译脚本文件。

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

  既然脚本语言很慢,为什么还有人用?

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

  慢语不影响整个系统。影响整个系统速度的短板不是语言本身。目前系统的瓶颈一般在IO部分。语言再慢,也比IO快一个数量级。并不是说语言执行快10倍后整个系统就快10倍。如果语言很慢,整个系统将保持不受影响,这取决于瓶颈是哪个块。

  就像动物园里载动物的船超载一样。人们不会抱怨有些人太胖,但他们清楚地知道,体重主要是船上的大象。人的体重和大象根本不是一个量级。

  况且,即使语言加快了,还是会因为IO跟不上而被阻塞(因为是脚本语言,这里阻塞的是脚本解释器),而且因为语言太慢,阻塞的时间会更长。

  为什么被屏蔽了?这种阻塞往往是因为程序的后续指令需要从IO设备中读取的数据,也就是说程序的后续步骤依赖于这些数据,没有这些数据程序的运行是没有意义的。例如,Web服务器必须先读取硬盘上的数据,然后通过网卡发送给用户。获得硬盘数据后,就可以在CPU上执行web服务器进程中操作网卡发送数据的指令。

  因为语言的解释器是CPU处理的,CPU速度肯定比IO设备快很多,所以在等待IO设备响应的时候什么都不能做。为了充分利用宝贵的CPU资源,操作系统一定会把进程(二进制可执行程序或脚本语言的解释器)加入阻塞队列,让其他可以直接运行的不需要阻塞的进程使用CPU(阻塞是指进程不会在CPU上运行,也就是从操作系统调度程序的就绪队列中删除)。

  但是language(脚本语言解释器)比IO设备更慢更快,所以还是会因为IO更慢而被阻塞。也就是说,让整个系统变慢的一定是系统最慢的部分,而无论脚本语言有多慢,IO设备永远比语言慢,所以“影响系统性能”是黑锅,脚本语言是背不了的。

  另一方面,人们喜欢使用脚本语言是因为它的开发效率高,这也是脚本语言发明的初衷。很多用C实现需要多个步骤的功能,用脚本语言一句话就能搞定,当然更受开发者欢迎。

  什么是中间代码?

  很多编译器都是先把源语言编译成中间代码,再编译成目标代码,但中间语言不是必须的。中间代码缩写为IR,是介于源程序和机器语言之间的一种语言。有N进制(如三进制、四进制)、反波兰式、树形等形式。

  目标代码指的是cod

  (1)可以跨平台

  因为中间代码不是目标代码,所以可以作为所有平台的通用语言,通过中间代码实现前端分离。比如在多平台多语言环境下开发,可以提高开发效率。只要在某个平台上编译中间代码,剩下的从中间代码到目标代码的工作就可以由目标平台的编译器来完成。

  (2)易于优化。

  中间代码更接近源代码,对优化更直接有效。而且中间代码可以在一个平台上优化,然后送到其他平台编译成目标机,提高了优化效率。

  编译器的前端和后端是什么?

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

  图1-2

  前端主要负责读取源代码,对源代码进行预处理,通过词法分析将单词转换成令牌流,然后进行语法分析和语义分析,将源代码转换成中间代码。

  后端负责优化中间代码,转换成目标代码。

  词法分析、语法分析、语义分析和代码生成不是串行执行的。

  许多教科书将编译阶段分成几个独立的部分:

  (1)词法分析;

  (2)语法分析;

  (3)语义分析;

  (4)生成中间代码;

  (5)优化中间代码;

  (6)生成目标代码。

  很容易给人一种“这些步骤是按顺序执行的”的错觉,即“这六个步骤必须从源代码到目标代码依次执行”,但事实并非如此。至少一个高效的编译器永远不会这样。

  这只是函数逻辑的一个步骤。以前四步为例。它们以平行穿插的方式一起执行,以语法分析为主线,即这四个步骤与语法分析同时开始和结束。

  每一步的功能实现都是由其实际的模块来完成的。负责词法分析的模块叫词法分析器,负责代码生成的模块叫代码生成器,负责语法分析的模块叫解析器。

  编译器由词法分析器、解析器和代码生成器组成(如果有目标代码优化,还包括优化模块)。

  编译的入口是解析,所以编译从调用解析器开始,解析器调用词法分析器和代码生成器两个子例程。换句话说,词法分析器和代码生成器只会被解析器调用。没有解析器,他们将没有“露脸”的机会。

  所以编译是基于解析器的,解析器并行调用词法分析器和代码生成器。

  虽然语法分析和语义分析是两个功能,但其实可以合二为一。因为经过语法分析,我们知道了它的语义。这个很好理解。毕竟语法是语义的规则,规则是编译器(设计者)制定的。那么编译器(设计者)在分析了自己制定的规则后当然会理解语义(不可能不理解自己制定的规则的含义)。

  比如阅读英语句子,尤其是复杂长句时,先找到句子的谓语动词,以谓语动词为分界线把句子分成两部分,前一部分找主语,后一部分找宾语。分析完语法,句子的意思就清楚了。

  也就是说,语法分析和语义分析同时是两件事,所以合并在一起也就不足为奇了。你看,语法分析和语义分析确实是并行的。

  为了解析的效率,词法分析器往往被解析器作为一个子程序调用,也就是每次解析器需要一个单词的令牌时就调用词法分析器。你看,语法分析和词法分析确实是并行的。

  最后说生成代码。目前,生成代码的方法称为语法指导。什么是语法指导?就是在分析语法的时候“同时”生成目标代码或者中间代码。其实是以语法分析为导向的。解析器在知道源代码的语义后,立即调用代码生成器生成目标代码或中间代码,所以这是与解析器并行的。

  提醒一下,与其在解析器解析完整个源代码后一次生成整个源代码对应的目标代码或中间代码,不如解析一部分源代码,立即生成该部分源代码对应的目标代码或中间代码,这样更高效,也更容易实现。

  举个例子,如果源代码文件中有10行代码,解析器不断调用词法分析器,每次得到一个单词的token,在读取完所有前三行源代码后,确定源代码的语义,立即生成与这三行源代码含义相同的目标代码或中间代码。

  然后解析器继续调用词法分析器读取第4行后的源代码,重复解析语法和生成代码的过程。总之语法分析是主线。语法分析根据语法把源代码拆分成几个小部分,每次生成这个小部分的目标代码或者中间代码。

  综上所述,为了使编译更加高效,词法分析、语法分析、语义分析和代码生成是以语法分析为中心并行执行的。词法分析和代码生成是由解析器调用的子例程。

  什么是符号表?

  之所以列出符号表,是因为这个词听起来“吓人”。因为看不见摸不着,所以很多新手都觉得是很神秘的东西。实际上,符号表是存储符号的表,就这么简单。

  你想,源代码里的符号应该总存放在某个地方,这样引用的时候就能找到。因此,符号表的目的是记录文件中的符号。包括符号串、方法名、变量名、变量值等。之所以把符号放在表中,还有一个重要原因是方便生成指令,指令的格式是统一的。

  编译器将使用符号表中符号的索引作为指令的操作数。如果不使用索引,指令会很乱。例如,如果函数名或字符串直接用作操作数,则指令会很长。“桌子”并不是特指计算机中的“桌子”。“表”是一个通用的概念,用来表示所有可以添加、删除、修改和查找的数据结构。因此,符号表可以用任何结构来实现,如链表、哈希表、数组等。

   《自制编程语言》

  郑

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

   《自制编程语言》

  郑

  而且研究生有操作系统课程。这些人学术能力很高,但书太抽象晦涩,很多学生害怕这门课,不敢提问。只有会提问的人才能。运营理论书籍无法让读者理解什么是运营系统。学习操作系统不能靠想象。他们需要看到一些具体的东西。

  大多数技术人员都对操作系统感到好奇。他们渴望一本讲述操作系统是什么的书。没有太多无关行政的东西在里面。代码很小,是现代操作系统的原型。他们渴望不用花费大量的时间和成本就能很快看到本质。

  今天的互动

  你想为自己的成长选择哪本书?为什么?截至9月5日17: 00,消息转发至朋友圈,边肖将抽取两位读者赠送纸质书。(参加活动直接到微信自制编程语言,六个让你困惑的问题)

  转载请联系作者取得转载授权,否则将追究法律责任。

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

相关文章阅读

  • office2010激活密钥大全 怎么永久激活office2010
  • project2010产品密钥免费_project2010激活密钥永久激活码
  • c语言调用退出函数 c语言退出整个程序怎么写
  • c语言中怎么给函数初始化 c语言的初始化语句
  • c语言编写函数计算平均值 c语言求平均函数
  • chatgpt是什么?为什么这么火?
  • ChatGPT为什么注册不了?OpenAI ChatGPT的账号哪里可以注册?
  • OpenAI ChatGPT怎么注册账号?ChatGPT账号注册教程
  • chatgpt什么意思,什么是ChatGPT ?
  • CAD中怎么复制图形标注尺寸不变,CAD中怎么复制图形线性不变
  • cad中怎么创建并使用脚本文件,cad怎么运行脚本
  • cad中快速计算器的功能,cad怎么快速计算
  • cad中快速修改单位的方法有哪些,cad中快速修改单位的方法是
  • cad中心点画椭圆怎么做,cad轴测图怎么画椭圆
  • CAD中常用的快捷键,cad各种快捷键的用法
  • 留言与评论(共有 条评论)
       
    验证码: