JAVA

关于编译器自举问题的个人见解

C编译器可以编译C语言,即,将任意的C代码转换为机器语言(或汇编,看语境)。那么C编译器本身作为一个可执行程序,它必然也是用某种语言编写并编译为机器语言的,是用什么语言编写的呢,是机器语言吗?是汇编吗?据说都不是,因为用机器语言或者汇编语言实在是太难太复杂了!答案是用C语言编写的。初次遇到这个问题一定很奇怪,用C语言这样的语言编写了一段可以将自己翻译为全是0或1构成的机器语言的C语言程序?不是陷入了先有鸡还是先有蛋的怪圈了吗?

然而实际上这个和鸡蛋悖论还是有所区别的,计算机领域内这个问题叫做编译器的自举问题。

我们不妨从编译器的大概原理来考虑这个问题:

编译器编译某种语言是从通过对语言的词法分析,句法结构入手的,可以理解为如何处理特定的代码文本。我们假定用某种语言–X语言去写Y语言的编译器,当它编译某段Y语言代码时,比如看到“int c=100””import lib “什么之类文本片段,就把它转为同样意义的X语言写的代码。通过比较充足的测试用例后,这段X代码功能就完善好了,它已经可以将任意的Y代码转为X代码。那么这段特定的X代码经过编译(这是指X到机器语言的编译)后成为可执行文件,比如命名为y2m.exe,就可以当编译器用了。以后任何Y代码都可以通过该编译器y2m.exe来编译。

转到原问题,X、Y都成了C语言,用自己分析自己,可行吗?似乎有那么一点可能性,为什么?我们不妨先来举个例子,当我们小学生时只会几千个汉字,为什么通过查字典方式就可以认识理论上所有汉字了呢?因为有那一部分已知汉字作为基础,就可以理解在字典上用简单汉字解释复杂冷僻汉字的条目。作为类比,用精简后的C语言(一般称为C的一个子集),也就是保留比较核心的功能,比如保留重要的关键字什么的,的简化版的C语言,随便给个名字比如叫Cn语言,来分析所有C语句。再假定我们的Cn语言到机器语言的编译器我们已经实现了(如何实现的暂时不管它,也许是别人给你的,也许是手工写汇编或者机器语言来的,反正总比写C的全集工作量要小)。再回想下汇编和机器语言的关系,不也和这个有点相像吗?

汇编和机器语言的关系:早期计算机通过识别纸带上的有没有孔的方式(也就是代表数字0或1)来读取各种指令,后来也许可以直接输入机器码了,但仍然比较麻烦。有人觉得那些指令,比如相加操作,假设对应的机器码为10010001,不好记,那么我不如先记成ADD这样的意义明确的帮助记忆的符号(后来称之为助记符),到时候实际输入计算机的时候用某种办法(硬件实现或者软件实现都可以)让它被翻译为10010001再输入计算机就行,这样程序员省事了,计算机也还能像以前一样继续读取机器码。这样,把基本的操作都用助记符来表示,就产生了汇编语言。只要用机器代码写一段机器代码,可以将助记符转为机器代码(即编译),就可以行得通,因为汇编的助记符种类确实不多。那么,刚才这段代码就是所谓的汇编语言编译器(就是汇编器)了。

好了,再来回头想这个问题,从C到Cn好似隧道的一头,汇编到机器语言是隧道另一头,中间过程再打通,就全线连接,完成C的编译器了。现在我们必须考虑刚才我们说的那个问题,从Cn到机器语言的编译器怎么写?那我们不如继续精简,继续分层,用Cn-1语言来写一个能够编译Cn的编译器,前提是假定我们Cn-1到机器语言的编译器已经实现。然后再继续精简,分层,用Cn-2语言来写一个能够编译Cn-1的编译器… …

然后你会发现这成了一个类似递归的问题,最后我们需要一种能将C0语言编译的、用汇编写成的编译器,至此全部环节打通。

好了,我们终于用一种几乎等同于C语言的C子集–Cn语言写好了C的编译器,而当你看Cn语言你根本就觉得它就是C语言,因为它只是用了C语言的部分内容,所以称之为用C语言写的C编译器也不为过吧。

至此,我们似乎可以创造自己的语言W了,只要用C语言写一段可以分析W代码的C代码,再用C编译器编译,就得到了W语言的编译器,比如取名叫W2M.exe。

那么,新的问题是,你能用W写一个W的编译器吗?是不是又像刚才那样分那么多层?难道不那么分层就做不了吗?

可以只分W-C-Machine Language吗?试试吧。假设我们已经拥有了用C编译W的编译器,取名compiler0。

先用W给自己写一段简单点的分析代码W,取名code1,也许可能会有一些问题,没关系,只要能把大部分功能完成即可。好了,现在我们用compiler0编译code1得到compiler1,compiler1是W编写的可能不那么完善的W解释器。为了让其完善,得想办法。我们试试用compiler1再去编译code1,得到compiler2,如果顺利走到这一步,说明至少compiler1可以编译自己,那么,我们有compiler1和compiler2了,哪个更好呢,我也不太清楚,也许是后者吧。如果以后相对code1进行改进,那么我们用compiler1,2,…n.继续编译就好了,因为这些compiler都是从编译W代码而来,编译过程没bug(有bug就不能生成exe),至少可以保证编译过程没错(W源文件代码是否写对不能保证)。

在以上的这个过程中,对code1的多次修改以达到预期效果,是和前文里的多次分层的效果是差不多的,殊路同归。


既然编译器本身是一段代码,那么如果想编译一个编译器,就需要更早的编译器来进行编译操作。而编译这个更早的编译器还需要更更早的编译器——长此以往,第一个编译器是怎么产生的呢?难道是直接用机器语言书写的?如果能这么想,那么你就猜对了。第一个编译器一定是用机器语言写出的。实际上,很久以前的程序员还在纸带上打孔编程呢,他们也照样乐在其中。机器语言只是很难书写,并不是不能书写。但是,毕竟用这么难书写的语言写一个C++编译器,谁都不会愿意去干。所以,第一个编译器的功能一定是很简单的。而人们会用这很简单的编译器的语言,去写一个稍微复杂一些的编译器;然后再用这个新的编译器,去写一个更复杂的编译器;最终得到一个很复杂的编译器,比如C++编译器——好了,如果能够理解这个过程,那么你实际上几乎就理解了自举。

所谓自展,实际上就是用一个功能不太完善的编译器支持的语言写代码,然后放到这个编译器上去编译,产生一个比自己更完善的编译器的过程。用一个不太恰当的例子来描述,就是我们已经有了一个C语言的编译器,然后我们用C语言写一个C++的编译器代码,并用C语言编译器编译这个代码生成可以运行的C++编译器(之所以不太恰当,是因为C语言不是C++语言的严格子集)。也就是说,我们有一个编译器A(C –> A),现在写一个编译器C(C++ –> A),将后者放入前者中进行编译,即A(C(C++ –> A) –> A),得到一个可以执行的编译器A(C++ –> A)。