写在前面
最近看到了很早之前自己写的一些关于了解编译原理
的笔记,突然来了兴致,于是乎想根据当前的了解,写一篇自己对代码编译
的了解及猜想。
本文并不会完全按照<<编译原理>>
或特定语言进行说明,更多是自己的理解,由于技术水平和理解能力受限,本文当中一定存在理解错误,尽管如此,对自我追求技术而言,本文依然有存在的必要性。
一、编译的目的
编译就是将高级语言转换为低级语言的过程。
- 高级语言的特征
- 不能被计算机直接识别。例如
c、java、python、nodejs、javaScript、ruby...
等等,编程开发常用的语言。 - 容易被人类更好的理解和掌握
- 本身与硬件特性不是强相关
- 不能被计算机直接识别。例如
- 低级语言的特征
- 能直接被计算机识别。例如
汇编
。日常开发几乎不会使用到。 - 不容易被人类理解,学习曲线和掌握难度大。
- 本身根据硬件做强相关编译,常见的比如
cpu指令集、内存指令
等
- 能直接被计算机识别。例如
人类想要提高生产力或者创造力,必须从低级语言的束缚中摆脱出来。而低级语言之所以如此,是为了考虑计算机系统的理解、执行等方向,设计出发点不是完全趋于编程;而高级语言的诞生就是趋于易编程设计,从近几年流行起来的高级语言看,高级语言的发展方向只会越来越接近人类语言。
所以,编译是至关重要的一步。
二、编译的差异
如果说编译是1到100
的过程,而并不是所有语言都是从1走到100
的,某一些语言的设计对于编译而言有微小的区别,比如c、java、python、javaScript、ruby
等语言会形成一些步骤差异,但总体上大致是相同的。在编程中最直观的感受就是不同的语言依赖的函数库
不一样,外部依赖/引用也不一样。
所以编译的目的有一部分就是要将这些差异面向计算机进行解决。注意,请不要混淆编译概念,这里的编译指的是单个高级语言对计算机而言。而不是所有语言,因为高级语言的差异跨度过大是无法通过编译解决的。但是高级语言在设计时可以尽量减少这种差异,比如kotlin
。
所有的高级语言有两个显著的差异:
- 编译器:
- 将高级语言代码根据
静态计算
得到一个可执行的程序,但并不实际执行这个生成的可执行程序。 - 通常经过
编译器
产生的可执行文件可能是能直接运行的,也有可能是需要借助解释器
才能运行的。 编译器
输出的生成物不一定是实际的文件,也可以直接在内存中进行解释执行,这取决于编译器
和解释器
的特性。
- 将高级语言代码根据
- 解释器:
- 将高级语言代码根据当前
硬件
的指令集、函数库
不同,对其进行解释,使计算机能够理解识别。 解释器
就是实际执行代码的过程
- 将高级语言代码根据当前
所以,想要运行一段高级语言,通常最长的路径顺序是源代码 > 编译器 > 解释器
。
为什么有些语言是由解释器
进行解释执行
的?
正如先前所说,有些高级语言有步骤的差异,如ruby
不需要编译器
,直接就是源代码 > 解释器
。
原因在于这一类代码的开发团队提供了非常强大的静态计算
能力,同时在解释器
中解决了以下基本问题:
- 非常精简的语法
- 高效的语义、语法、上下文解析算法
- 高效的运行机制,运行时分配策略
- 不同的
函数库
- 外部依赖/引用
- 不同硬件下的
指令兼容
- (不同语言还包含了不同的方面)
基于以上的方向支撑,使得程序可以逐行解释执行。而不需要过多的干预。这实际上是一种语言设计的优雅体现。
为什么有些语言是需要编译器
编译后再由解释器
执行的的?
我们已经知道高级语言实际上是可以开发一个强大的解释器
来甩掉编译器
这一大步的,为什么很多高级语言还需要由简入繁,带上一个编译器
呢?
原因在于编译
可比我们理解的复杂多了。
三、编译器
的主要工作流程:
- 分析高级语言代码的词法,进行标记,将代码拆分成最小粒度
- 基于标记结果进行
词法分析
,输出记号流
。这里通常都是很多算法来完成,包括词法分析自动生成
(这个过程非常繁复)。 - 根据
记号流
判断词法是否正确,如果正确,会根据记号流
构建抽象语法树
。这个步骤称为语法分析
(这个过程更加繁复,有更多的算法参与)。 - 当
抽象语法树
完成后,会进行语义分析
,其实就是类型检查、上下文检查
等。大白话就是根据语言定义语法规范查看上下文检查有没有不符合当前高级语言定义的内容
。通常高级语言的版本不兼容会在此报错。 - 关于
变量
,这里有个非常核心的东西叫符号表
,记录了变量
的数据类型、作用域、访问权限
等。其中作用域
是一个类似于可变化的栈。需要时插入,退出时移出(我没有搞清楚这个符号表
具体是什么时候创建的,很可能在分构建抽象语法树
之前就存在了)。 语义分析
完成后,会进行翻译语法
,使其格式化(编译器中的标准化,也叫去差异化),通常不同的语言版本不兼容,大部分会在这个地方报版本不兼容的错误。- 根据
编译器
当前的硬件环境和软件环境选择指令集,为后面的代码生成做准备。 - 根据
物理机
或者虚拟机
生成代码的技术主要有2
种,Stack 栈计算机代码生成、REG 寄存器计算机的代码生成
(这两种我并未理解到位)。总体上都是通过字节码计算单元指令集
或RISC 精简指令集
将符号表
与翻译过的语法树
进行加减乘除运算
变成一个个栈帧,压入栈中。 - 这个过程实际上还有代码优化的过程,也叫
指令重排
,这里非常复杂,简单来说就是,会根据语义分析
后的结果使用算法进行代码优化,优化的目标是减少栈帧、简化逻辑
,优化原则是不能影响到程序在单线程下的准确性
(这个过程是一堆非常复杂的算法)
jvm
字节码就是Stack 栈计算机代码生成
这种方式。
现在可以详细的观察一些主流的高级语言哪些是解释执行
,哪些是先编译再解释执行
。最具争议的python、javaScript
,其实跟java
是非常相似的,都是先编译再解释执行
,如果要杠请了解下.pyc、v8
。
四、使用编译器的好处
使用编译器
可以理解为非实时运行,可以有更多的空间和时间更大程度的检查、优化
代码,能够很从容的根据软硬件的差异做出兼容。因此对于跨平台运行来说更加稳定。
并不是说不用编译器
的高级语言兼容性、跨平台能力就差,这是相对而言的。比如ruby
与python
就存在很大差异,本身承载的量不同,兼容难度就会发生变化。
五、关于代码自动生成
这本是与本文无关的方向,但是篇幅过短,在这简要记录。
实现一个代码生成器的基本思路:
- 检查代码引用并标记,比如包引用,方法引用
- 检查代码生成标记并匹配代码生成模板
- 将标记的引用代码进行全局公共缓存(物理磁盘或内存)
- 将标记的代码生成进行局部缓存(物理磁盘或内存)
- 从代码引用标记处重置引用地址(物理路径到引用路径)或插入代码段
- 检查原始代码的引用和代码生成标记是否达到预期
- 开始代码编译
评论区