概述
写本篇的时候想起我刚面实习的时候,面试官问编译原理的知识,我当时只隐约记得常量折叠,所谓常量折叠简单的说就是将一些在编译期能确定的计算过程放在编译期就计算完成,而不用将计算过程都放在运行期。老实说,我是一个好奇心比较强的人,有些问题出现在眼前,可能当前没有时间去得到答案的话,这个问题会一直放在我的脑海,直到有时间给出答案为止。我也想起刚毕业的时候加入的一些QQ群,我会饶有兴致的跟别人去争论,但是大多数情况下得到的并不是有益的讨论,大多数人都无意跟你争论,得到的基本是嘲讽。当时一个群里的人嘲讽我,是大厂的人嘛?这些就大厂能用到。现在想想这位兄弟某种程度上说的也是,所以这篇文章算是纯粹的满足我自己的好奇心,基本上面试也问不到。我也想起我大学时思考的两个问题:
- 什么是机器语言?
- 程序是如何被执行的?
我时常带着第一个问题去咨询我的大学老师,但老师的回答却常常不让我满意,有一个老师说机器语言就是打孔卡上写好送入机器执行,像下面这样:
这两张截图来自B站视频: https://www.bilibili.com/video/BV1i64y1M7Sc/ 有兴致的可以看下。当时的这个回答,我脑补的场景大致就是上面两张图的模糊版本。但我感觉我的问题仍然被完全回答,我后面又想那打孔纸带上的就是机器语言吗?既然是一种语言有什么样的语法、单词呢?现在我们构建软件基本上会选择高级语言:C#、C++、C、Python、Java等。按照一般的理解计算机是无法直接识别我们编写的文本的,所以还是需要有编译器来将我编写的文本编译成计算机能够识别的代码。会有人说计算机只认识1和0。计算机也有好几个部分,那这个1和0究竟是谁来识别的呢?我想应该是CPU, 我们知道冯.诺依曼下的现代计算机由存储器、运算器、控制器、输入设备、输出设备五部分组成,其中运算器和控制器合称为中央处理器(Central Processing Processor,简称CPU), 粗略的说我们可以认为CPU在不断执行计算任务,运算器是计算机中负责计算(包括算数计算和逻辑计算等)的部件。运算器包括算术和逻辑运算部件(Arithmetic Logic Units,简称ALU)、移位部件、浮点运算部件(Floating Point Units,简称FPU)、向量运算部件、寄存器等。CPU通过运算器来完成计算功能,现代的CPU是数字集成电路的结晶,这里不做赘述,我们只需要知道借助于电路信号可以完成计算任务即可。有兴致的话,可以参看下面这个视频:
写到这里想起海明威的一句话: 冰山在海上之所以显得庄严宏伟,是因为它只有八分之一露出水面。那机器语言是电路信号?当然不是,机器语言在电路信号之上,抽象出了计算(累加),加载数据等指令, 然后由对应的指令译码器将机器语言翻译成电路信号。总结一下机器语言也就是机器指令的集合,机器指令展开来讲就是一台机器可以正确执行的命令。电子计算机的机器指令是一列二进制数字。计算机将之转变为一系列高低电平,以使计算机的电子器件收到驱动,进行运算。那机器指令集应当去哪里查呢?应当去找CPU的设计商,比如我想了解Intel家族CPU的指令集,那我就会上bing去搜索关键词:X86 intel关键字,为什么不去百度,原因在于百度的第一页都没有intel的官网:
下面是bing的搜索:
我们点进去看一下:
注意上面的大字: Intel® 64 and IA-32 Architectures Software Developer Manuals,也就是Intel 64 和IA-32位架软件开发人员手册。看到这里有同学可能就会问了,你搜到明明是X86 Intel, 搜到了Intel的IA-32 和 IA-64,这不是搜的不精准嘛,其实IA-32、IA-64、X86,这三个词是同义词,我们可以翻一下维基百科对于IA-32的论述来印证我们的论断:
IA-32 (short for "Intel Architecture, 32-bit", commonly called i386) is the 32-bit version of the x86 instruction set architecture, designed by Intel and first implemented in the 80386 microprocessor in 1985.
IA-32是Intel Architecture, 32-bit的简称,是32位版本的X86指令集架构,由intel于1985年的80386处理器上首次设计和实现。
IA-32 is the first incarnation of x86 that supports 32-bit computing; as a result, the "IA-32" term may be used as a metonym to refer to all x86 versions that support 32-bit computing.
IA-32是X86的第一个支持32位计算的化身,因此,"IA-32" 一词也是所有支持32位计算的X86的代名词。
Within various programming language directives, IA-32 is still sometimes referred to as the "i386" architecture. In some other contexts, certain iterations of the IA-32 ISA are sometimes labelled i486, i586 and i686, referring to the instruction supersets offered by the 80486, the P5 and the P6 microarchitectures respectively. These updates offered numerous additions alongside the base IA-32 set including floating-point capabilities and the MMX extensions.
在一些编程语言指令中,IA-32有时也被称为"i386"架构,在其他的一些上下文中IA-32 ISA也有被称为i486、i586、i686。分别指80486、P5和P6微架构提供的超集。这些更新基本在基本的IA-32指令集的基础上提供了许多功能,比如浮点功能和MMX扩展。
那看来bing搜索还是准的,那我们能不能得到,IA-64是X86指令集架构下的64位计算实现呢,并不能,原因在于IA-64和指令集完全不兼容,在已有大量运行在X86-32位架构之上的软件情况下,不兼容,对于用户体验来说可相当不友好,比如我想下载一个JDK,不小心下载到了一个32位的,然后你告诉我运行不了。
这无疑会让人难以接受,i586就是32位版本的,你下载下来可以运行的原因在于AMD推出了兼容32位的X86-64位架构,所以x86-64的最大优势是能在64位环境里直接运行32位程序,也就是兼容性好。我们接着翻intel的文档,碰见不认识的单词,不要紧,翻翻词典就行了:
这里是文档概述,我们摘录一下读读看:
This document contains the following: 该文档包含以下内容
Volume 1: Describes the architecture and programming environment of processors supporting IA-32 and Intel® 64 architectures.
卷一: 描述了支持IA-32和intel 64位处理器的架构和编程环境。
Volume 2: Includes the full instruction set reference, A-Z. Describes the format of the instruction and provides reference pages for instructions.
卷二: 包括全部的指令集参考,A-Z描述了指令的格式,并提供了指令参考页。
Volume 3: Includes the full system programming guide, parts 1, 2, 3, and 4. Describes the operating-system support environment of Intel® 64 and IA-32 architectures, including: memory management, protection, task management, interrupt and exception handling, multi-processor support, thermal and power management features, debugging, performance monitoring, system management mode, virtual machine extensions (VMX) instructions, Intel® Virtualization Technology (Intel® VT), and Intel® Software Guard Extensions (Intel® SGX).NOTE: Performance monitoring events can be found here: https://perfmon-events.intel.com/
包括完整的系统编程指导,第1、2、3和4部分描述了Intel 64和IA-32架构的操作系统支持的环境,包括: 内存管理、保护、任务管理、中断和异常处理、多处理器支持,散热和电源管理 、调试、性能监控、系统管理模式、虚拟机扩展指令、intel虚拟化技术,intel软件防护扩展。注意性能监控事件可以在这个链接里找到。
Volume 4: Describes the model-specific registers of processors supporting IA-32 and Intel® 64 architectures.
卷4: 描述了支持IA-32和Intel® 64架构的处理器的特定型号寄存器。
那机器指令的格式应该就存在卷二里面,我们可以将这个PDF下载下来,细细观赏,里面就有机器指令的格式。看来这就是答案。x64 体系结构是 x86 向后兼容的扩展。它提供新的 64 位模式和旧版 32 位模式,这与 x86 相同。术语“x64”包括 AMD 64 和 Intel64。指令集几乎相同。所以我们看intel公布的文档就行了。那到这里问题就结束了?算是回答了当初大一自己思考的问题?应该不算是,现在想来自己当初应该思考的应该包括以下几个问题:
- 高级语言如何变成机器语言?
- 机器语言如何被CPU执行?
这两个问题事实上可以被合并成一个问题,程序是如何被执行的?本篇只做概述,先绘其骨骼,再填充其血肉,避免陷入到细节里,这里的目的是建立一个高屋建瓴般的认识。那程序究竟是如何被执行的呢? 让我们从计算机体系架构讲起。
计算机体系架构讲起
我们先来理解结构这个词,再讨论计算机体系结构。事实上在计算机领域,结构和架构出现的频率是蛮高的,比如数据结构,计算机体系结构,软件架构,架构师等等。前面的文章里面我们已经讨论过数据结构,软件架构:
- 且谈软件架构 https://juejin.cn/post/6844904089692700680
- 数据结构与算法分析学习笔记(一) 数据结构简论 https://juejin.cn/post/6980929046733553695
在《且谈软件架构》中我们谈到架构对应的英语单词是Architecture,而结构对应的单词是structure。在这篇文章中我们提到:
Architecture: The architecture of something is its structure
即某物的结构就是架构。参考的是柯林斯词典[1], 但今天重翻这篇文章的时候,又翻了几个词典,在《牛津高阶英汉双解 第七版》中没有这样的语义,在这个词典中关于Architecture这个单词的语义有以下几个:
- the art and study of designing buildings 建筑学
例句是: to study architecture。 - the design or style of a building or buildings 建筑设计,建筑风格。
- the design and structure of a compute system 计算机行业的名词,体系结构;(总体、层次)结构
但是从字面上意思来看应当是计算机系统的结构与设计更好,层次结构,总体结构也还说的过去 , 体系结构就有些怪怪的,因为体系在汉语词典里的意思是:"整体结构", 所以构词的时候,就是结构的结构。我的考据癖发作,我想这个词一定是外来词,从其他语言引入的,所以我一眼看上去才会感觉这个词有点体会不到意思,于是我咨询了下chatGPT(强大的AI问答产生器), 这确实是一个引进中文的词,原词为system architecture,到后面不知道怎么变成了体系结构。我们理解成系统结构的话,就好理解的多, 那什么是系统呢?我认为这是一个组织的整体,由多个相互作用的部分组成,以达到特定的目标或功能。
那结构呢,我们看下structure这个词在牛津词典中的意思:
the way in which the parts of sth are connected together, arranged or organized; a particular arrangement of parts
某物各部分连接或排列、组织的方式; 各部分特定的排列方式
那现在我们就可以来审视计算机系统(体系)结构这个词了,当我们说起计算机系统结构的时候,我们说的是什么? 我想应当是各个层次的结构, 如下图所示:
从编译器到JIT与AOT
我们结合这幅图来介绍程序是如何被执行的,一般来说,现代软件都会选择用高级语言构建,一般的高级语言都会带有标准库,这些标准库里面提供了网络、文件、集合、线程等能力,我们借助于这些高级语言附带的标准库来构建软件,一开始我们使用的是高级语言编写的文本文件,这些文件的后缀或是.java,或是.cpp,或是.c ,目前躺在这些文本文件里面的文本还只被人类所识别,无法被计算机直接执行,所以我们需要一个翻译者,来将这些文本翻译给计算机听,其实也是给CPU听,遗憾的是这世上所有的CPU的机器语言并不一致,但是对于程序的编写者来说,我们只想编写一次,但是希望能够跨平台,所以翻译者,也就是编译器不能选择将程序员们编写的文本文件翻译成所在平台的机器指令,而是翻译成了IR(Intermediate Language) , 这一方面的杰出代表是Java的字节码、C#的IL(Intermediate Language)。以Java为例,最终交付的文件现在一般是一个jar, 启动的时候,一个JVM实例会加载这个jar,那我们知道最终交付给CPU的应该是机器语言,那jar里面的各个class文件是如何变成机器语言呢? 默认配置下,一开始所有Java方法都由解释器执行,这里面出现了一个名词解释器,那什么是解释器? 什么是编译器?
简单地说, 一个编译器就是一个程序,它可以阅读以某一种语言(源语言)编写的程序,并把该程序翻译成为一个等价的、用另一种语言(目标语言)编写的程序。编译器的一个重要任务之一是报告它在翻译过程中发现的源程序中的错误。如果目标程序是一个可执行的机器语言程序,那么它就可以被用户调用,处理输入并产生输出。
解释器是另一种常见的语言处理器。它并不通过翻译的方式生成目标程序,从用户的角度看,解释器直接利用用户提供的输入执行源程序中的指定操作。
我们可以将其粗浅的理解为解释器也将IR翻译成所在平台的机器指令,然后执行。我们接着回到上面的讨论,也就是一开始所有Java方法都由解释器执行,解释器记录着每个方法的调用次数和循环次数,并以这两个数值为指标去判断一个方法的"热度"。等到一个方法足够"热"的时候,HotSpot VM就会启动该方法的编译。这种在所有执行过的代码里只寻找一部分来编译的做法,就叫做自适应编译(adaptive compilation)。在Java里面承担这个自适应工作的叫JIT(Just In Time)编译器,随着时间推移,即时编译逐渐发挥作用,把越来越多的代码编译优化成本地代码,来获取更高的执行效率。解释器这时可以作为编译运行的降级手段,在一些不可靠的编译优化出现问题时,再切换会解释执行,保证程序可以正常执行。 这种程序执行方式,一定是有开销的,能不能提前将源代码翻译好呢,当然可以了,这也就是AOT,Ahead Of Time,提前编译解决了在执行时解释的开销,理论上来说性能会很好,这看起来很美好,不是嘛? 但是编译面临的一个普遍问题是:
具有挑战性的是,从数学上讲,为给定源程序生成一个最优的目标程序是不可判定问题,在代码生成中碰到的很多子问题(比如寄存器分配)都具有难以处理的计算复杂性。在实践中,我们必须使用那些能够产生良好但不一定最优的代码的启发性技术。幸运的是,启发性技术已经非常成熟,一个静心设计的代码生成器所产生的代码要比那些由简单的生成器生成的代码块好几倍。 《编译原理》 第二版
写到这里想起了Java的GraalVM项目,目前一个比较热的点也是AOT编译,降低内存占用。目前来看,AOT和JIT还是各有优劣势的。截止我们目前的讨论还没有到操作系统,操作系统管理着计算机的资源,为了安全,程序不能直接接触到CPU,只能请求操作系统执行机器指令。那在Windows平台上我们打交道最多的还是exe文件,那exe文件里面的内容是机器语言吗?
从JIT与AOT到可执行文件
答案是不是,最多只能说exe的一部分是机器语言,正规Windows平台上的一个32位*.exe可执行的文件格式,大名叫做Portable Executable(PE), 即可移植可执行。Mac上也有可执行的文件格式。这里我们不做讨论。我们目前只将目光放在Windows平台上。 PE的文件格式如下所示:
以PE格式为例,这里只有两块区域是机器语言代码:
第一块区域是 Image DOS Stub 。这是 16 位机器语言代码,用于让 DOS 操作系统在实模式(英特尔 8086 模式)下输出这句话:This program cannot be run in DOS mode.
第二块区域是
.text
节。这是是英特尔 32 位(英特尔80386模式)的机器语言代码,用于运行常规的 Windows 程序。
我们可以认为AOT最后的产物是所在操作系统的可执行文件。采取解释 + JIT方式执行的程序,则是要将翻译的机器代码写入指定区域,并请求操作系统执行的方式来执行,这句话是我的主观判断,之所以是主观判断的原因是因为,我本来问了chatGpt在Windows上哪两个接口做写入机器代码,请求操作系统执行。chatGpt也回答了,我翻了下Open JDK的源码:
说句题外话,我截图的时候,发现JDK的虚拟机文件夹在六个小时内有提交。打开这个文件夹就会看到hotspot的目录结构:
CPU下面有JDK适配的指令集架构:
貌似熟悉的也只有x86、arm、riscv、aarch64。os里面有适配的操作系统:
里面的代码,大多都是C/C++, 看的有点头皮发麻,翻了几个文件之后,遂放弃。看来还是需要复习一下C/C++和操作系统的知识才能再看看。
程序是如何执行的
到现在为止,那个少年在大一提出的问题,已经得到了解决,程序究竟是怎么执行的呢,对于高级语言来说,计算机或者CPU本身并不认识,所以我们需要将编写的文本翻译成对应的机器语言,那么在什么时候翻译呢,就有两种翻译方式,一种是运行时,一种是在运行时之前。在运行时之前翻译,我们称之为AOT,但这个最终要形成被操作系统认可的格式,不同的操作系统有不同的可执行文件格式。在运行时执行机器语言的,则要调用对应操作系统的接口。不同文件格式的介绍, 可以参看下面这个知乎问答:
具体的格式细则,我只翻了windows的介绍,看了之后,感觉就是知道了一些,但有好像什么都不知道。
总结一下
如果有时光机,将这篇文章送给大一的我,不知道大一的我能不能看的懂。仿佛像是大一的我,在纸上写了这个问题,将纸叠成纸飞机扔向了未来的自己。现在的我在纸飞机上写上答案扔给过去的自己。整体上来看,这个答案并不那么完全易懂,后面会对这篇文章进行重构,我的愿景是尽可能让大一的我读的懂,我想大一的我收到这个答案。估计也会有些头痛吧,也许会问出新的问题。这篇文章照最初的设想是三到四条线。第一条线是程序是怎样执行的,也就是由高级语言到机器指令,其中穿插各个操作系统的介绍,但是这条线引用的知识太多了,比如计算机体系架构什么的,后面也都砍掉了。第二条线是从希尔伯特的第十问题到图灵机,再到冯诺依曼,再到指令集体系架构。我试图寻找一条线将这些问题连接起来,但是最终这条线也没有编织起来。第三条线则是从计算机体系架构入手,也就是上面的层次结构图里面,从上往下,介绍联系。但是依然没有找到一条线将其贯穿起来。索性都砍掉了。其中本来也打算穿插介绍一下编译器相关的理论,但是关于编译器相关的理论,坦率的说,我现在是一个门外汉,我不满足只介绍干巴巴的理论,我想一边介绍理论,一边也有实践,也就是做一个编译器。后面发现这四条线要都融入进去是无法做到恰到好处的,后面的打算是将这几条线都分拆到不同的文章里面。
参考资料
[1] 【纸带计算机】Elliott903现场演示 https://www.bilibili.com/video/BV1i64y1M7Sc/
[2] Architecture https://www.collinsdictionary.com/zh/dictionary/english/architecture
[4] difference-compiler-vs-interpreter https://www.guru99.com/difference-compiler-vs-interpreter.html
[5] 计算机语言是什么? http://programarcadegames.com/index.php?chapter=what_is_a_computer_language&lang=cn
[6] Instruction Set Architecture https://www.arm.com/glossary/isa
[7] What is the difference between Instruction Set and Instruction Set Architecture (ISA)? https://stackoverflow.com/questions/43293943/what-is-the-difference-between-instruction-set-and-instruction-set-architecture
[8] 神奇的电路,CPU就是靠它计算的!加法器的工作原理 https://www.youtube.com/watch?v=EQYk2681sSA
[9] 汇编语言 (第三版) 王爽著 清华大学出版社
[10] 为什么IA-64指令集架构失败了?https://www.zhihu.com/question/27654453
[11] x64 体系结构 https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debugger/x64-architecture
[12] A Deeper Inspection Into Compilation And Interpretation https://medium.com/basecs/a-deeper-inspection-into-compilation-and-interpretation-d98952ebc842
[13] 即时编译器与解释器的区别?https://www.zhihu.com/question/36746487/answer/75003562
[14] 为什么 JVM 不用 JIT 全程编译?https://www.zhihu.com/question/37389356/answer/73820511
[15] 编译器的工作过程 https://www.ruanyifeng.com/blog/2014/11/compiler.html
[16] 编译原理(龙书) d第二版
[17] What does a just-in-time (JIT) compiler do? https://stackoverflow.com/questions/95635/what-does-a-just-in-time-jit-compiler-do
[18] 如何通俗易懂地介绍「即时编译」(JIT),它的优点和缺点是什么?https://www.zhihu.com/question/21093419
[19] 基本功 | Java即时编译器原理解析及实践 https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html
[20] exe文件是机器语言,为什么mac不能运行Windows的exe文件?https://www.zhihu.com/question/374125621/answer/2678745161