前言
今天,我把我学到的关于编译原理的一些知识整理了一下,于是写了这篇文章来进行简单的记录,废话就不多说了,开始介绍。
编译和解释
再介绍这两个的区别和联系前,先来介绍这两个是什么。
什么是编译
从百度百科引一段
编译是指利用编译程序从源语言编写的源程序产生目标程序的过程
而什么是编译程序呢
将某一种程序设计语言写的程序翻译成等价的另一种语言的程序的程序, 称之为编译程序(compiler)
可以这样通俗的理解,只要是从一种源代码翻译成另外一种源代码的过程就是编译。
什么是解释
它将源语言直接作为源程序输入,解释执行解释一句后就提交计算机执行一句,并不形成目标程序
这一段也是从百科上面摘的,简单的说,就会如果使用解释执行的话,那么就不会产生目标程序,而是直接通过解释器执行命令。
区别和联系
一开始,我对这2个概念,也不是很清楚,再加上解释型语言和编译型语言的区分和运行时概念的引入,我就更加的混乱了。后来,我也是在接触了更多的技术和在看了R大的文章(他的文章我会在文末的附录中列出),才有了比较清楚的认识。
我混乱的原因是因为因为一开始就引入了过多的概念,其实编译,解释和编程语言之间并没有太大的联系 。如果想要的话,不同的编程语言都可以实现编译和解析执行。还有就是运行时,运行时和编程语言是有关系的,但是和编译和运行,也没有实质性的联系,这个也在文章后面在分析。
在把编程语言和运行时的概念暂时抛开后,简单的来说,编译和解释执行的最大区别就是有没有生成目标语言的代码片段,解释执行是解释程序在解释代码后直接进行计算得到结果 ,从逻辑上来说,机器语言实际上也是解释执行的,计算机硬件就是机器语言的解释器。而编译是由编译程序把源程序的代码片段经过编译过程后产生目标语言的代码片段 。而如果要说它们之间的关联性的话,那么,就是无论是编译还是解释,它们的最终目的都是为了能够让计算机执行程序。
编译的具体过程
再聊完了编译和解释的区别之后,接下来,谈谈编译的过程,一个编程语言的编译过程分为前端编译和后端编译。
前端编译主要目的是代码的错误检查和生成AST语法树。
后端编译的主要目的是根据AST语法树生成目标代码。
具体过程是比较复杂的,所以我就简述每个过程的主要作用就行了,而且,编译也并不是需要把每一个步骤都完,很多开发中编译过程其实只涉及到前端编译或者编译中的几个步骤。

前端编译
词法解析
语法解析
语义解析
生成AST树
词法分析
词法分析的主要任务是读入源程序的输入字符,然后生成并输出一个词法单元序列。词法解析主要使用了正则表达式进行字符串匹配
语法解析
语法解析使用词法分析产生的词法单元进行分析,进而构造出语法树。
语义分析
在语法分析构造出的语法树的基础上,再根据语法树和符号表中的信息来检查源程序是否和语言定义的语义一致,如果不符合,就会进行代码的报错。语义分析完毕后,就会生成AST语法树。
后端编译
- 生成中间代码
- 代码优化
- 生成目标代码
生成中间代码
生成中间代码的过程,是在AST语法树生成之后,对AST语法树进行扫描,然后产生的一种介于目标代码和输入代码的中间代码,例如Java中的字节码
代码合并,优化
对中间代码进行优化,以便生成更好的目标代码。
生成目标代码
将优化后的中间代码映射为目标语言的代码。
编译原理和编程语言
再对编译原理和作用和流程有了个大体了解后,就可以来聊下编译原理和编程语言之间的关系了。我一开始的话,会把一个编程语言看做是编译语言和解释型语言,例如把C/C++ 看成是编译型的语言,把Java看成是解释型的语言,因为Java是先编译出字节码,再由VM对字节码进行解释执行,把js也看成是解释性语言。而且,我那时觉得解释型语言和编译型语言还有一个大的区别,就是进程的不同,当然这准确来讲是运行时的区别,下文再说。
直到后来,我接触了JIT 和AOT ,才知道,原理没有那么简单,JIT就是即时编译了,以Java为例,JVM在执行字节码的时候,会将部分字节码直接编程成平台相关的代码(因为不同的cpu架构能够识别的机器码是不同的),例如在x86平台上,就会编译出x86相关的机器码,然后再将机器码进行运行,这是个时候,因为JVM 是C/C++ 写的,所以利用指针的特性,是可以在程序运行的时候,将机器码加载到程序中运行起来的。当然JVM除了JIT之外,还保留了解释执行的能力,JVM是基于栈式的,安卓里面的DVM则是基于寄存器的,这里可以打个比方,那就是使用解释执行的字节码对于JVM或者DVM来说,就如同机器码对于CPU来说是一样的,都是直接解释执行了 ,只不过解释逻辑CPU是基于硬件实现的,JVM是C++实现的(这里以HotSpot为例)。
再来聊下JIT在JS中的实现,其实我一开始觉得JS是解释型语言其实也没错,因为JS引入JIT技术也是到了V8引擎才开始的,V8一开始是直接JIT的,就是在生成AST树之后,连字节码都没有产生,就直接JIT产生native代码了,所以可以看成是编译型语言了,后来才加入了字节码和解释执行,现在的模型基本上和JVM是一样的,只不过Java编译成字节码这个过程是分开的,而js编译成字节码这个过程是接下来的步骤是连在一起的。
说完了JIT,再来讲下AOT,AOT是完全编译的意思,最常见的AOT的编程语言就是C/C++了,都是先编译成成汇编,再编译成机器码。但其实像JAVA这种高级的语言,也是可以实现AOT的,例如在安卓的ART中,应用在安装的过程,就会将.dex(就是字节码文件)进行二进制的编译,编译成arm平台的代码,当然,因为运行时的原因,JAVA还是没办法像c那样直接二进制跑在操作系统上,需要ART的支持。
所以,总的来说,每一种编程语言想要被计算机执行,都是可以采取编译型或者解析执行的,当然最后,肯定是由CPU进行执行,正因为如此,我在开头说了,编译,解释和编程语言之间关系不大,编译和解释执行对于编程语言来说,都是一种具体实现的方案而已。
编程语言和运行时
编程语言和运行时这个方面是很大的,而且和编译原理关系也不是很大,但是编程语言和运行时是关系很大的,这也决定了不同的编程语言的主要运用场景不一样。所以,因为这篇文章的主题是漫谈,所以,也来聊聊吧。
C语言的运行时可以是操作系统,也可以直接就是硬件,当C语言跑在内核中的时候,就需要直接和硬件打交道了,利用内联汇编和指针去操控硬件。C++ 的运行时是操作系统,它利用C库函数去调用一些多线程,多进程,io操作,网络编程的功能,同时,利用自身的指针,去进行程序的内存管理。Java的运行时是jre,利用jvm实现内存管理,多线程和io的功能也需要由运行时提供,或者使用jni调用动态链接库。Java的运行时一般由c++/c 这类能直接跑在操作系统上的编程语言编写。所以,即使Java代码使用aot编译,但是因为Java代码本身并没有提供内存管理的能力,所以仍然需要虚拟机对其内存进行管理,而io,多线程这类需要和操作系统交互的功能也得依赖虚拟机进行调用。Js其实和Java差不多,但它比java更弱,语言本身连多线程,io都没有提供相应的语法,所以,它的io操作必须要依赖于宿主,可以是node.js,也可以是浏览器,由宿主提供和操作系统沟通的功能,gc则由js引擎进行管理。而在node.js中 宿主环境提供原生功能的操作其实就类似于jni,不过,因为js本身没有io和多线程的语言模块,所以这些功能c/c++实现封装的程度要比Java高很多。Java还可能只在涉及系统调用时使用c库,所以Java还可以有自己的网络库,例如netty,但js是没办法的。
再回到前面提到的进程问题,其实js的线程就是v8的线程,从操作系统的角度看,它们是同一个的,java 的 进程就是JVM 所在的进程,当然Java 的线程是操作系统级别的, 所以我那时觉得编译型语言和解释型语言(就是需要带个虚拟机或者引擎)的最大区别,就是解释性语言执行的时候并没有创建新的进程,还是使用它们的虚拟机的进程,而像C/C++ 启动.exe文件的时候就创建了一个新的进程,现在我觉得我这种观点是不对的,在学了fork和exec之后,进程的创建是父进程通过fork产生的,然后将可执行文件(二进制文件)exec 到进程中,然后换核不换壳,这种操作其实和加载class差不多,如果非要说区别,那也只能说Java,js这类语言和C/c++ 相比 多了一层虚拟机。
编译原理的应用
其实编译原理涉及的范围很广,它本质上是一种基于字符串的算法。编程语言方面现在还出现了LLVM这种可以将多种编程语言进行编译的编译器,通过它能将多种编程语言编译成字节码,还有就是像ktolin这种可以编译成多种编程语言的语言,学个ktolin,Java,js,python,c都有了,哈哈。
除了编程语言需要使用到编译原理,在开发中也都有涉及,这里就说下前端的了,例如Vue中template模板编译成render函数,es6通过bable编译成es3,less编译成css ,再例如uni-app和 mpvue之类的框架,也是使用了编译原理。所以呀,要造轮子,编译原理是肯定要学的。
最后
推荐几本编译原理的书和大佬的博客
龙书 <<编译原理>>
虎书 <<现代编译原理:C语言描述>>
鲸书 <<高级编译器设计与实现>>
送上大佬的知乎首页