编译器如何编译自己?
Posted
技术标签:
【中文标题】编译器如何编译自己?【英文标题】:How can a compiler compile itself? 【发布时间】:2016-10-26 14:47:33 【问题描述】:我正在http://coffeescript.org/ 网站上研究 CoffeeScript,它有文字
CoffeeScript 编译器本身是用 CoffeeScript 编写的
编译器如何编译自己,或者这个语句是什么意思?
【问题讨论】:
可以自行编译的编译器的另一个术语是self-hosting
编译器。见programmers.stackexchange.com/q/263651/6221
为什么编译器不能自己编译?
涉及的编译器至少有两个副本。一个预先存在的编译一个新的副本。新的可能与旧的相同,也可能不同。
您可能还对 Git 感兴趣:它的源代码当然是在 Git 存储库中进行跟踪的。
这就像在问“施乐打印机如何自己打印原理图?”编译器将文本编译为字节码。如果编译器可以编译成任何可用的字节码,您可以用相应的语言编写编译器代码,然后将代码传递给编译器以生成输出。
【参考方案1】:
编译器的第一版不能由特定于它的编程语言机器生成;你的困惑是可以理解的。第一个编译器可以构建具有更多语言功能的更高版本的编译器(用新语言的第一个版本重写源代码)。然后该版本可以编译下一个编译器,依此类推。这是一个例子:
-
第一个 CoffeeScript 编译器是用 Ruby 编写的,产生了 CoffeeScript 版本 1
CoffeeScript 1 重写了 CS 编译器的源代码
原始 CS 编译器将新代码(用 CS 1 编写)编译成编译器的版本 2
对编译器源代码进行了更改以添加新的语言功能
第二个 CS 编译器(第一个用 CS 编写的)将修改后的新源代码编译成版本 3 的编译器
每次迭代重复第 4 步和第 5 步
注意:我不确定 CoffeeScript 版本的编号方式,这只是一个示例。
这个过程通常被称为bootstrapping。引导编译器的另一个示例是rustc
,它是Rust language 的编译器。
【讨论】:
引导编译器的另一条途径是为您的语言的(一个子集)编写一个解释器。 作为用另一种语言编写的编译器或解释器引导的另一种选择,非常老式的方法是手动组装编译器源代码。 Chuck Moore 在 Programming a Problem-Oriented Language (web.archive.org/web/20160327044521/www.colorforth.com/POL.htm) 的第 9 章“引导程序”中介绍了如何为 Forth 解释器执行此操作,基于已完成之前用手两次。此处的代码输入是通过前面板完成的,该面板允许将值直接存储到由位拨动开关控制的内存地址。【参考方案2】:在论文Reflections on Trusting Trust 中,Unix 的创始人之一 Ken Thompson 写了一篇引人入胜(且易于阅读)的关于 C 编译器如何编译自身的概述。类似的概念可以应用于 CoffeeScript 或任何其他语言。
编译自己的代码的编译器的想法有点类似于quine:源代码在执行时会生成原始源代码作为输出。 Here is one example CoffeeScript quine。 Thompson 举了一个 C quine 的例子:
char s[] =
'\t',
'0',
'\n',
'',
';',
'\n',
'\n',
'/',
'*',
'\n',
… 213 lines omitted …
0
;
/*
* The string s is a representation of the body
* of this program from '0'
* to the end.
*/
main()
int i;
printf("char\ts[] = \n");
for(i = 0; s[i]; i++)
printf("\t%d,\n", s[i]);
printf("%s", s);
接下来,您可能想知道编译器是如何被告知像'\n'
这样的转义序列表示 ASCII 代码 10。答案是在 C 编译器的某个地方,有一个解释字符文字的例程,其中包含一些类似这样的条件识别反斜杠序列:
…
c = next();
if (c != '\\') return c; /* A normal character */
c = next();
if (c == '\\') return '\\'; /* Two backslashes in the code means one backslash */
if (c == 'r') return '\r'; /* '\r' is a carriage return */
…
所以,我们可以在上面的代码中添加一个条件……
if (c == 'n') return 10; /* '\n' is a newline */
... 生成一个知道 '\n'
代表 ASCII 10 的编译器。有趣的是,该编译器,以及由它编译的所有后续编译器,“知道”该映射,因此在下一代源代码,你可以把最后一行改成
if (c == 'n') return '\n';
……它会做正确的事! 10
来自编译器,不再需要在编译器的源代码中显式定义。1
这是在 C 代码中实现的 C 语言功能的一个示例。现在,对每一个语言特性重复这个过程,你就有了一个“自托管”编译器:一个用 C 编写的 C 编译器。
1 论文中描述的情节转折是,由于编译器可以像这样“教导”事实,它也可能被错误教导以一种难以生成的方式生成木马可执行文件检测,并且这种破坏行为可以持续存在于受污染的编译器生成的所有编译器中。
【讨论】:
虽然这是一个有趣的信息,但我认为它不能回答问题。您的示例假设您已经有一个引导编译器,或者 C 编译器是用哪种语言编写的? @ArturoTorresSánchez 不同的解释适用于不同的人。我的目的不是重申其他答案中所说的话。相反,我发现其他答案比我想的要高。我个人更喜欢具体说明如何添加单个功能,并让读者从中推断,而不是肤浅的概述。 好的,我理解你的观点。只是问题更多的是“如果编译器不存在编译器如何编译自己”,而不是“如何向自举编译器添加新功能”。 这个问题本身就是模棱两可和开放式的。似乎有些人将其解释为“CoffeeScript 编译器如何自行编译?”。评论中给出的轻率回应是“为什么它不能自己编译,就像它编译任何代码一样?”我将其解释为“如何实现自托管编译器?”,并举例说明了如何教授编译器了解其自身的语言特性之一。它以不同的方式回答了这个问题,提供了一个低层次的说明来说明它是如何实现的。 @ArturoTorresSánchez:“[I]C 编译器是用哪种语言编写的?”很久以前,我维护了旧的 K&R 附录中提到的原始 C 编译器(IBM 360 的那个)。很多人都知道首先有 BCPL,然后是 B,并且 C 是 B 的改进版本。事实上,有很多旧编译器的一部分仍然用 B 编写,并且从未重写为 C。变量的形式为单个字母/数字,指针算术不假定为自动缩放等。旧代码证明了从 B 引导到 C。第一个“C”编译器是用 B 编写的。【参考方案3】:您已经得到了很好的答案,但是我想为您提供一个不同的视角,希望对您有所启发。让我们首先确定两个我们都可以同意的事实:
-
CoffeeScript 编译器是一个可以编译用 CoffeeScript 编写的程序的程序。
CoffeeScript 编译器是用 CoffeeScript 编写的程序。
我相信您会同意 #1 和 #2 都是正确的。现在,看看这两种说法。你现在看到CoffeeScript编译器能够编译CoffeeScript编译器是完全正常的吗?
编译器并不关心它编译的什么。只要是用 CoffeeScript 编写的程序,就可以编译。而 CoffeeScript 编译器本身恰好就是这样一个程序。 CoffeeScript 编译器并不关心它正在编译的是 CoffeeScript 编译器本身。它所看到的只是一些 CoffeeScript 代码。期间。
编译器如何编译自己,或者这个语句是什么意思?
是的,这正是那句话的意思,我希望你现在能明白那句话是如何正确的。
【讨论】:
我对咖啡脚本知之甚少,但您可以通过说明它是用咖啡脚本编写的但经过编译然后是机器代码来澄清第 2 点。无论如何,请你解释一下鸡和蛋的问题。如果编译器是用尚未编写编译器的语言编写的,那么编译器如何运行或编译? 您的陈述 2 不完整/不准确且非常具有误导性。因为正如第一个答案所说,第一个不是用咖啡脚本写的。这与他的问题非常相关。至于“编译器如何编译自己,或者这句话是什么意思?”你说“是”我想是的(虽然我的头脑有点小),我看到它被用来编译它自己的早期版本,而不是它自己。但它是否也用于编译自身?我想这毫无意义。 @barlop:将语句 2 更改为“Today,CoffeeScript 编译器是用 CoffeeScript 编写的程序。”这是否有助于您更好地理解它?编译器“只是”将输入(代码)转换为输出(程序)的程序。因此,如果你有一个 Foo 语言的编译器,然后用 Foo 语言本身编写一个 Foo 编译器的源代码,并将该源代码提供给你的第一个 Foo 编译器,你会得到第二个 Foo 编译器作为输出。这由很多语言完成(例如,我所知道的所有 C 编译器都是用……C 编写的)。 编译器无法自行编译。输出文件与生成输出文件的编译器不是同一个实例。我希望你现在能看到那句话是多么的错误。 @pabrams 你为什么这么认为?输出很可能与用于生成它的编译器相同。例如,如果我用 GCC 6.1 编译 GCC 6.1,我会得到一个用 GCC 6.1 编译的 GCC 6.1 版本。然后如果我用它来编译 GCC 6.1,我还会得到一个用 GCC 6.1 编译的 GCC 6.1 版本,它应该是相同的(忽略时间戳之类的东西)。【参考方案4】:编译器如何编译自己,或者这个语句是什么意思?
就是这个意思。首先,有几点需要考虑。我们需要查看四个对象:
任意 CoffeScript 程序的源代码 任意 CoffeScript 程序的(生成的)程序集 CoffeScript 编译器的源代码 CoffeScript 编译器的(生成的)程序集现在,很明显,您可以使用 CoffeScript 编译器生成的程序集(可执行文件)来编译任意 CoffeScript 程序,并为该程序生成程序集。
现在,CoffeScript 编译器本身只是一个任意的 CoffeScript 程序,因此,它可以被 CoffeScript 编译器编译。
您的困惑似乎源于这样一个事实:当您创建自己的新语言时,您没有编译器,但您可以使用它来编译您的编译器。这肯定看起来像一个鸡蛋问题,对吧?
介绍名为bootstrapping的进程。
-
您使用已经存在的语言编写编译器(如果是 CoffeScript,原始编译器是用 Ruby 编写的),可以编译新语言的子集
您编写的编译器可以用新语言本身编译新语言的子集。您只能使用上述步骤中的编译器可以编译的语言功能。
您使用第 1 步中的编译器来编译第 2 步中的编译器。这会留下一个最初用新语言的子集编写的程序集,并且能够编译新语言的子集。
现在您需要添加新功能。假设你只实现了while
-loops,还想要for
-loops。这不是问题,因为您可以重写任何for
-loop,使其成为while
-loop。这意味着您只能在编译器的源代码中使用while
-loops,因为您手头的程序集只能编译它们。但是你可以在你的编译器中创建函数,用它来编译for
-loops。然后使用已有的程序集,编译新的编译器版本。现在你有了一个编译器的程序集,它也可以解析和编译for
-loops!您现在可以返回编译器的源文件,并将您不想要的任何 while
-loops 重写为 for
-loops。
冲洗并重复,直到可以使用编译器编译所需的所有语言功能。
while
和 for
显然只是示例,但这适用于您想要的任何新语言功能。然后你就处于 CoffeScript 现在的情况:编译器自己编译。
那里有很多文学作品。 Reflections on Trusting Trust 是一本经典,每个对该主题感兴趣的人都应该至少阅读一次。
【讨论】:
(“CoffeeScript 编译器本身是用 CoffeeScript 编写的”这句话是对的,但“A compiler can compile self”是错的。) 不,完全正确。编译器可以自行编译。这没有任何意义。假设您拥有可以编译该语言版本 X 的可执行文件。您编写一个可以编译版本 X+1 的编译器,并使用您拥有的编译器(即版本 X)对其进行编译。您最终会得到一个可以编译该语言版本 X+1 的可执行文件。现在您可以使用新的可执行文件重新编译编译器。但是为了什么目的?您已经拥有可执行您想做的事情。编译器可以编译任何有效的程序,所以它完全可以自己编译! 确实编译不少次也不是没听说过,iirc现代freepascal编译器一共编译了5次。 @pabrams 写“请勿触摸”和“热物体。请勿触摸”对短语的预期信息没有影响。只要消息的目标受众(程序员)理解短语的预期消息(编译器的构建可以编译其源代码),无论它是如何编写的,这个讨论都是没有意义的。就目前而言,你的论点是无效的。除非您能够证明该消息的目标受众是非程序员,否则,只有这样,您才是正确的。 @pabrams 'Good English' 是一种能以作者或演讲者的意图清晰地向目标受众传达想法的英语。如果目标受众是程序员,并且程序员理解它,那么它的英语很好。说“光既作为粒子又作为波存在”从根本上等同于“光既作为光子又作为电磁波存在”。对于物理学家来说,它们的意思实际上是一样的。这是否意味着我们应该总是使用更长更清晰的句子?不!因为当目标读者已经清楚意思时,它会使阅读变得复杂。【参考方案5】:一个小而重要的说明
这里的术语编译器掩盖了涉及两个文件的事实。一个是可执行文件,它将用 CoffeScript 编写的输入文件作为输入文件,并生成另一个可执行文件、可链接目标文件或共享库作为其输出文件。另一个是 CoffeeScript 源文件,它恰好描述了编译 CoffeeScript 的过程。
您将第一个文件应用于第二个文件,生成第三个文件,该文件能够执行与第一个文件相同的编译操作(如果第二个文件定义了第一个文件未实现的功能,则可能更多),因此可以替换如果您愿意,请先。
【讨论】:
【参考方案6】:-
CoffeeScript 编译器最初是用 Ruby 编写的。
CoffeeScript 编译器随后用 CoffeeScript 重新编写。
由于 CoffeeScript 编译器的 Ruby 版本已经存在,它被用来创建 CoffeeScript 编译器的 CoffeeScript 版本。
这称为self-hosting compiler。
这非常普遍,通常是由于作者希望使用自己的语言来维持该语言的发展。
【讨论】:
【参考方案7】:这里不是编译器的问题,而是语言表达能力的问题,因为编译器只是用某种语言编写的程序。
当我们说“一种语言被编写/实现”时,我们实际上是指该语言的编译器或解释器已经实现。有一些编程语言,您可以在其中编写实现该语言的程序(相同语言的编译器/解释器)。这些语言称为universal languages。
为了能够理解这一点,想想金属车床。它是用来塑造金属的工具。仅使用该工具,就可以通过创建其零件来创建另一个相同的工具。因此,该工具是通用机器。当然,第一个是使用其他方式(其他工具)创建的,并且可能质量较低。但是第一个是用来构建精度更高的新的。
3D 打印机几乎是一台万能机器。您可以使用 3D 打印机打印整个 3D 打印机(您无法构建熔化塑料的尖端)。
【讨论】:
我喜欢车床的类比。然而,与车床类比不同的是,第一次编译器迭代中的缺陷会传递给所有后续编译器。例如,上面的答案提到添加一个 for 循环功能,其中原始编译器仅使用 while 循环。输出理解 for 循环,但实现是使用 while 循环。如果最初的 while 循环实现有缺陷或效率低下,那么它总是会这样! @Physics-Compute 这完全是错误的。在没有恶意缺陷的情况下,编译编译器时通常不会传播。 程序集翻译肯定会从迭代传递到迭代,直到程序集翻译被修复。以旧功能为基础的新功能不会改变底层实现。想一想。 @plugwash 参见 Ken Thompson 的“对信任信任的反思”-ece.cmu.edu/~ganger/712.fall02/papers/p761-thompson.pdf【参考方案8】:归纳证明
归纳步骤
编译器的第 n+1 个版本是用 X 编写的。
因此它可以被第n个版本的编译器编译(也是用X写的)。
基本情况
但是用X写的第一个版本的编译器必须要编译 由使用非 X 语言编写的 X 编译器执行。此步骤称为引导编译器。
【讨论】:
第一个用于语言 X 的编译器可以很容易地用 X 编写。这第一个编译器可以被解释。 (由使用非 X 语言编写的 X 解释器)。【参考方案9】:编译器采用高级规范并将其转换为低级实现,例如可以在硬件上执行。因此,除了目标语言的语义之外,规范的格式与实际执行之间没有任何关系。
交叉编译器从一个系统转移到另一个系统,跨语言编译器将一种语言规范编译成另一种语言规范。
编译基本上是一个公正的翻译,级别通常是高级语言到低级语言,但有很多变体。
Bootstrapping 编译器当然是最令人困惑的,因为它们会编译它们所使用的语言。不要忘记引导的初始步骤,它至少需要一个最小的现有可执行版本。许多自举编译器首先处理编程语言的最小特性,然后添加额外的复杂语言特性,只要新特性可以使用以前的特性来表达。如果不是这种情况,则需要事先用另一种语言开发“编译器”的那一部分。
【讨论】:
【参考方案10】:虽然其他答案涵盖了所有要点,但我认为不包括可能是已知的最令人印象深刻的编译器示例,该编译器是从自己的源代码引导的。
几十年前,一个名叫 Doug McIlroy 的人想为一种名为 TMG 的新语言构建一个编译器。他用纸和笔写出了一个简单的 TMG 编译器的源代码……用 TMG 语言本身。
现在,只要他有一个 TMG 解释器,他就可以使用它在自己的源代码上运行他的 TMG 编译器,然后他就会有一个可运行的机器语言版本。但是……他确实已经有TMG interpreter了!这是一个缓慢的,但由于输入很小,它会足够快。
Doug 在他眼窝后面的 TMG 解释器上运行了该论文上的源代码,并为其提供了与其输入文件完全相同的源代码。随着编译器的工作,他可以看到从输入文件中读取的标记,调用堆栈在进入和退出子过程时增长和缩小,符号表在增长......以及当编译器开始向其“输出”发出汇编语言语句时文件”,Doug 拿起笔,在另一张纸上写了下来。
编译器完成执行并成功退出后,Doug 将生成的手写汇编列表带到计算机终端,键入它们,然后他的汇编器将它们转换为可工作的编译器二进制文件。
所以这是“使用编译器自行编译”的另一种实用 (???) 方法:在硬件中实现工作语言,即使“硬件”是湿软的并且由花生酱三明治驱动!
【讨论】:
以上是关于编译器如何编译自己?的主要内容,如果未能解决你的问题,请参考以下文章
如何在自己的 Yocto 包中访问 protoc 编译器并引用 gRPC 库