回头再来理解编译和解释

目录
[隐藏]

感觉很久没有接触过编译了,突然想到一些相关的,发现在脑袋里找不到记忆了,那就再好好回顾一下吧

1、编译和解释

应该从刚接触编程开始,就知道了计算机是只能直接运行二进制代码的(即机器语言)。但从来没有直接写过二进制代码,最接近的一次应该是学汇编语言时。

对于我们使用的高级语言写的代码,都需要经过某种工具“翻译”成机器码才能在计算机上执行。这个"翻译”的过程又按是否产生中间代码分为了两大类:编译解释。相应的,编程语言也就分为了两大阵营:编译型语言 和 解释型语言。

编译型语言:程序需要经过专门的编译过程,然后生成相应的机器码文件(可执行文件),运行时不需要再次编译,直接可以运行之前编译产生的可执行文件。

解释型语言:程序不需要经过编译,在运行时才会被翻译成机器码。每次执行都需要进行翻译,所以效率没有编译型语言那么高。           

对于现代编译器来说,虽然我们只需要点一下编译按钮就可以将源文件编译生成可执行文件。但实际上“编译”是包含了好几个步骤的,这点在C语言、操作系统中都有提到过。

通常的步骤是这样的:源程序(source code)——>预处理(processor)——>编译(compiler)——>汇编(assembler)——>目标代码(object code)——>链接(Linker)——>可执行文件(executable)

具体这些步骤都做了什么就不再赘述了,可以参考下列文章:  

编译:一个 C 程序的艺术之旅

编译器的工作过程——阮一峰

偶尔可以看到说现在 编译 和 解释 的界限越来越不明显了,但个人觉得一直都还是比较清晰的,只要抓住“是否产生独立的中间代码”这点就很好区分了。

编译型语言编译产生的中间代码(二进制代码、可执行文件),是可以直接在相应的机器上运行的,只依赖于硬件,除此之外不依赖任何其它东西了。

而现在的有些解释型语言,虽然也能产生中间代码,甚至能缓存中间代码,后续直接运行缓存的中间代码。但还得看到,这些中间代码并不是二进制代码,执行这些中间代码往往是需要依赖其它程序的!也就是说这些中间代码并不是独立的,所以这和编译型语言还是有着本质区别的。以 PHP 为例,一个 PHP 源程序,经过 Zend 引擎的词法分析、语法分析就被翻译成一种中间代码 opcode,但这个 opcode 还是必须依赖 Zend 引擎才能运行,没办法直接运行在机器上,而缓存中间代码 opcode 通常也只是为了提高解释型语言的效率而实施的一种方法。

再看一个非常通俗的比喻:编译相当于做好一桌子菜再开吃,而解释则是吃火锅(边做边吃)

2、编译语言的自举(bootstrap

编译语言的自举(bootstrap) 这个概念之前没有接触过,现在想来主要还是自己没去思考一些东西,所以就不会去接触到了。考虑一个问题:

C 语言程序 需要 编译器 去进行编译生成 可执行文件,那 C 语言的编译器 是什么语言写的?如果也是 C 语言写的,那这个可执行的 C语言编译器程序 又是谁编译的呢?

为了回答这个问题,我们先回去机器语言时代。

早期时候,人们用机器语言写程序,先根据 CPU 设计好一系列表达不同操作的二进制代码(这些就是机器语言了)。然后人们想用计算机做什么事时,就去查表找到对用的二进制代码,然后输入计算机,让计算机执行。慢慢人们发现,查表这件事用计算机来做才更正确啊,人没必要记住那么多二进制数。于是就设计出了“助记符”(即汇编语言),这些“助记符”的指令和二进制代码是一一对应的,人只要记那些有意义的“助记符”即可。但问题就来啦,我是可以用“助记符”开始写程序了,但计算机怎么知道我这个程序中某个“助记符”是什么意思啊,对应哪条二进制代码啊。为了解决这个问题,人们就果断用 机器语言 写一个可以将“助记符”翻译成 二进制代码 的“编译器”(最早的编译器应该毫无疑问是二进制代码写的吧,这里编译器之所以打上引号,主要是这个东西应该叫汇编程序,但为了避免和汇编语言写的程序搞混就借用“编译器”一名)。这样后面就可以用这个 机器语言 写的“编译器”去翻译用 汇编语言 写的程序了,这就简化了编程难度,人们就可以用 汇编语言 写出更复杂的程序了,然后就有人用 汇编语言 写了一个 汇编语言的“编译器”的源程序,然后这个源程序经过之前用 机器语言 编写出来的“编译器”的编译,就得到一个可以直接运行在机器上的用 汇编语言 写的用来编译 汇编语言 的“编译器”。这个过程就实现了编译语言的自举!

既然汇编语言是这样,那后面慢慢出现的 C 语言自然也是同理。这也就回答了上面的问题,最早的 C 语言编译器程序 是使用更早期的语言编写的(比如汇编语言),然后用汇编语言的“编译器”编译的。不过现在用的 C 语言编译器 大都应该是用 C/C++ 编写的了(因为 C 也像 汇编语言一样实现了自举)。

从这大段文字可以知道:所谓“编译语言的自举”,就是用编译语言本身去实现一个编译本语言的编译器,并再更早期语言去编译这个编译器,从而达到可以使用本语言编写的编译器来编译本语言写的程序。

然后就不停地更高级的语言来写编译器,编译器就在这种自举之中慢慢迭代发展,有这样一句话:“机器生汇编,汇编生B,B生C,C生万物”很好的概括了迭代自举的过程。

3、交叉编译(Cross Compile

聊过了编译语言的自举,再来看另一个问题:我们知道,编译语言编译得到可执行文件是和 CPU 相关的,可执行文件要能在这个 CPU 上运行,就必须使用这个 CPU 的指令集。那如果使用不同的 CPU ,是否又要从 机器语言 开始呢?答案当然是否定的, 因为在已有 CPU 上已经可以编写和编译很复杂的程序了(假设为 C 语言),那就直接在已有 CPU 上利用 C 语言和编译器,开发一个可以将 C 语言编译为可以在 新CPU 上运行的可执行文件的 编译器(假设为 F,但 F 和 之前的 C编译器并不相同,虽然都是编译 C语言,但编译后得到的二进制代码是和 CPU 相关的),这样就能直接在 新CPU 上使用 C语言和编译器 F 开发 C 语言程序了,而不用从机器语言重新开始。那在已有 CPU 上编译出可运行在 新CPU 上的可执行文件就是交叉编译了。

目前看来,GNU 开源的 GCC 编译器,已经基本支持所有 CPU 了。而 交叉编译 主要用在为嵌入式系统开发应用程序的方面,因为嵌入式系统无论是存储空间还是CPU性能,都不足以支持编译器所需要的条件,因此只能先在别的主机上编译出可在该嵌入式系统中运行的程序,然后植入运行即可。

4、一些问题

问: .exe 是 Windows 上编译链接后产生的 二进制代码 可执行文件,为什么不能跨平台执行呢?不是说计算机都能执行二进制代码么?

答:就现代计算机的层次来说,最接近硬件的就是操作系统了,操作系统之上的应用程序,都或多或少会依赖操作系统。Windows 上编译生成的可执行文件正好是这样的一个应用程序,所以是依赖操作系统的,应用程序要操作硬件,都要通过操作系统提供的 API(动态链接库)。而不同操作系统提供的 API 是不尽相同的,所以如果 二进制代码 中调用了 OS 的 API,则无法跨平台了。另一点,不同平台的二进制代码可执行文件并不是仅仅只存放程序代码,还需要保存一系列信息(程序大小、地址等),所以可执行文件的文件头格式是不完全相同的,Linux 里大部分是 ELF 格式,而 Windows 下大部分是 PE 格式。一个可执行文件,没办法具有两种文件头,也就无法跨平台执行了。总的来说,只要清楚 应用程序 是位于 OS 之上的,所以要受到 OS 的限制,这也就是为什么我们说是 不能跨平台 而不是说 不能跨计算机 执行啦。也给一篇参考文章:《C 语言为什么不能跨平台》

        具体 ELF 和 PE 格式这里就先打住(有兴趣有时间再回过头来学习吧)

问:编译器和操作系统哪个更加底层,哪个更早出现?

答:之所以会问这个问题,主要是现阶段,我们基本都是先接触操作系统开始的,然后知道接触到的编译器都是运行在操作系统之上的,那更早些时候是什么样的呢?

      个人感觉这个问题,在于如何看编译器的定义(如果你认为一定要是将 高级语言 翻译为 机器语言 的才算是编译器,那最早的高级语言编应该是 1957年的 FORTRAN 了,那编译器就晚于操作系统出现。如果你认为只要是将 非机器语言 翻译为 机器语言 的就算是编译器了,那最早的编译器 A-0 将英文、法文翻译成机器码诞生在 1952 年,这样编译器就早于操作系统出现)

            从历史角度,世界上第一台电子计算机 ENIAC 在 1946 年诞生;1952 年 Grace Murry Hopper 女士发明了世界上第一个高级语言编译器 A-0,可以在Speny机器上将英文、法文翻译成机器码!Bug 这个词也是她发明的;1956 年 IBM 的704 大型机才正式配备了世界上最早的 操作系统。所以说 编译器 比 操作系统 更早出现。

           再从一个发展角度来看:操作系统的出现了是为了更好的让人去使用计算机硬件,那之所以要借助一个提前写好的操作系统,肯定是因为硬件太多复杂,人手动扳开关太过麻烦。如果操作系统出现在编译器之前,那岂不是人们在越来越复杂的硬件上一直用着 机器语言 写代码,然后突然就跳跃性的想到要用 机器语言 写一个复杂的操作系统软件去管理控制硬件?这很不合逻辑,太过跳跃了。个人觉得,从在简单机器上使用 机器码 开始(此时人要靠查表才能知道某个操作对应的设计出来的二进制代码),到稍微复杂一点的硬件人们就想到每次查表输入太慢,于是就想到设计一个符号语言(即汇编语言),然后人只要记有意义的符号,再提前用 机器语言 写好查表程序(汇编编译器),这样以后就可以开始用 汇编语言 写程序去输入了(查表交给机器,当然此时还是手动扳开关运行机器,因为还比较简单)。然后再随着硬件、功能的发展,更高级的语言和编译器就出现了,开发出来的软件也就越来越复杂,然后就才能在此基础上开发一个比较复杂的操作系统软件。查阅资料后,第一个操作系统是用汇编写出来的。

        至于谁更底层,,,对现代计算机而言,基本都是先有操作系统,然后再有基于操作系统的编译器,毕竟设计出操作系统就是为了更好的管理和控制硬件、使用计算机,这样看编译器是一个应用程序,那肯定没有操作系统那么底层。但编译器当然可以不依赖操作系统,直接运行在硬件上,就想一下最早的编译器是二进制代码写的,,够不够底层,但没有太多功能啊。《C 如何编译出一个不需要操作系统的可执行文件》

        现代编译器和操作系统关系肯定是可以有的:

            现代编译器大都是应用程序,而应用程序是在操作系统之上的,运行在操作系统上肯定有关系。

            但编译器编译的过程是不关操作系统什么事,还是想想最早的二进制代码写的编译器,没有操作系统也能编译出二进制代码。一旦进行了链接这个过程,那就和操作系统有很大关系了,因为 .o 目标文件链接了操作系统提供的库(API)并且按不同操作系统的可执行文件格式生成了可执行文件。

问:操作系统是某种计算机的可执行文件,那操作系统在哪里被编译的?

答: 这个应该就是前面提到的 编译器自举 和 交叉编译 的功劳了吧。最早的操作系统用汇编写的,而对于一个汇编写的程序已经存在对应的编译器,可以将其编译为某种可执行文件。然后将这个操作系统的可执行文件放到另一台没有装系统的计算机中,然后 BIOS 就会引导安装了,那对于更C 语言这样的高级语言写的操作系统也类似,都会先在另一台计算机上编译好。(不理解建议再回过头好好看看 编译语言自举 和 交叉编译的内容)

5、举例 C、Java、PHP 的编译和解释过程

其实理解好所学过的 C、Java、PHP 的编译和解释过程,那自然也就对 编译 和 解释 会有一定的程度的认识了。因为这方面网络上也有很多文章写了,我也觉得自己写不出啥别的东西,就不重复造轮子了,推荐一篇:《PHP、Java、C语言的编译执行过程》

6、总结

虽然文章中的问题在有了基础的人眼中是非常低水平的,但谁不是从没基础的小白过来的呢?学习路上当然要多给自己问问题,更重要的是要自己去解决自己遇到的问题,然后还要从这个过程中去总结才能转化为自己的东西。

本来是准备好好搞懂下 Linux 下的源码编译安装,但发现需要这些基础才能更好去理解,就花了些时间记录了,有了基础后面再找时间写 源码编译安装 相关内容吧。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

To