侧边栏壁纸
博主头像
术业有道之编程博主等级

亦是三月纷飞雨,亦是人间惊鸿客。亦是秋霜去叶多,亦是风华正当时。

  • 累计撰写 99 篇文章
  • 累计创建 50 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

编译原理初探

Administrator
2023-09-14 / 0 评论 / 0 点赞 / 294 阅读 / 5206 字

写在前面

最近看到了很早之前自己写的一些关于了解编译原理的笔记,突然来了兴致,于是乎想根据当前的了解,写一篇自己对代码编译的了解及猜想。

本文并不会完全按照<<编译原理>>或特定语言进行说明,更多是自己的理解,由于技术水平和理解能力受限,本文当中一定存在理解错误,尽管如此,对自我追求技术而言,本文依然有存在的必要性。

一、编译的目的

编译就是将高级语言转换为低级语言的过程。

  • 高级语言的特征
    • 不能被计算机直接识别。例如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

四、使用编译器的好处

使用编译器可以理解为非实时运行,可以有更多的空间和时间更大程度的检查、优化代码,能够很从容的根据软硬件的差异做出兼容。因此对于跨平台运行来说更加稳定。

并不是说不用编译器的高级语言兼容性、跨平台能力就差,这是相对而言的。比如rubypython就存在很大差异,本身承载的量不同,兼容难度就会发生变化。

五、关于代码自动生成

这本是与本文无关的方向,但是篇幅过短,在这简要记录。
实现一个代码生成器的基本思路:

  • 检查代码引用并标记,比如包引用,方法引用
  • 检查代码生成标记并匹配代码生成模板
  • 将标记的引用代码进行全局公共缓存(物理磁盘或内存)
  • 将标记的代码生成进行局部缓存(物理磁盘或内存)
  • 从代码引用标记处重置引用地址(物理路径到引用路径)或插入代码段
  • 检查原始代码的引用和代码生成标记是否达到预期
  • 开始代码编译

个人公众号

0

评论区