直观地说,似乎语言Foo
的编译器本身不能用Foo编写。更具体地说,语言Foo
的第一个编译器不能用Foo编写,但可以为Foo
编写任何后续编译器。
但这是真的吗?我对一种语言的阅读非常模糊,这种语言的第一个编译器是用“本身”编写的。这是可能的,如果是这样的话?
答案 0 :(得分:208)
这称为“自举”。您必须首先使用其他语言(通常是Java或C)为您的语言构建编译器(或解释器)。完成后,您可以用Foo语言编写新版本的编译器。您使用第一个bootstrap编译器来编译编译器,然后使用此编译的编译器来编译其他所有内容(包括其自身的未来版本)。
大多数语言确实是以这种方式创建的,部分原因是语言设计者喜欢使用他们正在创建的语言,而且因为非平凡的编译器通常可以作为语言“完整”的有用基准。 / p>
这方面的一个例子是Scala。它的第一个编译器是由Martin Odersky的实验性语言Pizza创建的。从2.0版开始,编译器完全在Scala中重写。从那时起,旧的Pizza编译器可以被完全丢弃,因为新的Scala编译器可以用于编译自己以用于将来的迭代。
答案 1 :(得分:68)
我记得听了一个Software Engineering Radio podcast,其中Dick Gabriel谈到了通过在LISP 在纸上编写一个简单版本并将其组装成机器代码来引导原始LISP解释器。从那时起,其余的LISP功能都被LISP编写并解释。
答案 2 :(得分:42)
为以前的答案添加好奇心。
以下是Linux From Scratch手册中的引用,其中一个开始从源代码开始构建GCC编译器。 (Linux From Scratch是一种安装Linux的方法,与安装发行版完全不同,因为你必须编译目标系统的每个单个二进制文件。)
make bootstrap
'bootstrap'目标不只是编译GCC,而是编译几次。它使用第一个编译的程序 第二次编译自己,然后第三次编译。然后比较这些第二和第三 编译以确保它可以完美地再现自己。这也意味着它编译正确。
使用'bootstrap'目标的动机是因为编译器用于构建目标系统的工具链可能没有目标编译器的相同版本。以这种方式进行,确保在目标系统中获得可以自行编译的编译器。
答案 3 :(得分:39)
当你为C编写第一个编译器时,用其他语言编写它。现在,你有一个C编译器,比如汇编器。最终,您将到达必须解析字符串的位置,特别是转义序列。您将编写代码以将\n
转换为十进制代码10(以及\r
到13等)的字符。
在编译器准备好之后,您将开始在C中重新实现它。此过程称为“bootstrapping”。
字符串解析代码将变为:
...
if (c == 92) { // backslash
c = getc();
if (c == 110) { // n
return 10;
} else if (c == 92) { // another backslash
return 92;
} else {
...
}
}
...
当这个编译时,你有一个理解'\ n'的二进制文件。这意味着您可以更改源代码:
...
if (c == '\\') {
c = getc();
if (c == 'n') {
return '\n';
} else if (c == '\\') {
return '\\';
} else {
...
}
}
...
那么'\ n'是13的代码的信息在哪里?它在二进制文件中!它就像DNA:用这个二进制文件编译C源代码将继承这些信息。如果编译器自己编译,它会将这些知识传递给它的后代。从现在开始,没有办法单独从源代码中看到编译器会做什么。
如果你想在某些程序的源代码中隐藏病毒,你可以这样做:获取编译器的源代码,找到编译函数的函数并将其替换为:
void compileFunction(char * name, char * filename, char * code) {
if (strcmp("compileFunction", name) == 0 && strcmp("compile.c", filename) == 0) {
code = A;
} else if (strcmp("xxx", name) == 0 && strcmp("yyy.c", filename) == 0) {
code = B;
}
... code to compile the function body from the string in "code" ...
}
有趣的部分是A和B.A是包含病毒的compileFunction
的源代码,可能以某种方式加密,因此搜索结果二进制文件并不明显。这确保了编译器本身的编译将保留病毒注入代码。
B对于我们想要用我们的病毒取代的功能是相同的。例如,它可能是源文件“login.c”中的函数“login”,它可能来自Linux内核。除了普通密码之外,我们可以用一个接受root帐户密码“joshua”的版本替换它。
如果你编译它并将其作为二进制文件传播,那么通过查看源代码就无法找到病毒。
答案 4 :(得分:18)
您无法自行编写编译器,因为您无需编译启动源代码。解决这个问题有两种方法。
最不受欢迎的是以下内容。您在汇编程序(yuck)中编写了一个最小的编译器,用于最小的语言集,然后使用该编译器来实现该语言的额外功能。建立自己的方式,直到你有一个编译器具有自己的所有语言功能。这是一个痛苦的过程,通常只有在你别无选择时才能完成。
首选方法是使用交叉编译器。您可以更改其他计算机上现有编译器的后端,以创建在目标计算机上运行的输出。然后你有一个很好的完整编译器并在目标机器上工作。最受欢迎的是C语言,因为有很多现有的编译器具有可插拔的可插拔后端。
一个鲜为人知的事实是GNU C ++编译器具有仅使用C子集的实现。原因是通常很容易为新的目标机器找到C编译器,然后允许您从中构建完整的GNU C ++编译器。你现在已经开始在目标机器上使用C ++编译器了。
答案 5 :(得分:14)
通常,您需要首先使用编译器的工作(如果是主要的) - 然后您可以开始考虑使其自托管。这实际上被认为是一些语言中的一个重要里程碑。
从我记得的“单声道”中,很可能他们需要添加一些东西来反思才能让它发挥作用:单声道团队一直指出有些事情根本不可能用Reflection.Emit
;当然,MS团队可能会证明他们错了。
这有一些真正的优势:对于初学者来说,这是一个相当不错的单元测试!而且你只需要担心一种语言(即C#专家可能不太了解C ++;但现在你可以修复C#编译器)。但我想知道在这里是否有一些专业的自豪感:他们只是希望它是自我托管。
不是一个编译器,但我最近一直致力于一个自托管的系统;代码生成器用于生成代码生成器...所以如果模式更改我只是自己运行它:新版本。如果有错误,我只需返回早期版本再试一次。非常方便,而且非常容易维护。
我刚刚在PDC观看了this video的Anders,并且(大约一个小时)他确实给出了一些更有效的理由 - 所有关于编译器作为服务。仅供记录。
答案 6 :(得分:4)
答案 7 :(得分:1)
答案 8 :(得分:1)
Mono项目C#编译器已经被“自托管”了很长一段时间,这意味着它是用C#本身编写的。
我所知道的是编译器是作为纯C代码启动的,但是一旦实现了ECMA的“基本”功能,他们就开始用C#重写编译器。
我不知道用同一种语言编写编译器的优点,但我确信它必须至少使用语言本身可以提供的功能(例如,C不支持对象面向编程)。
您可以找到更多信息here。
答案 9 :(得分:1)
实际上,由于上述原因,大多数编译器都是用他们编译的语言编写的。
第一个bootstrap编译器通常用C,C ++或Assembly编写。
答案 10 :(得分:0)
也许你可以写一个描述BNF的BNF。
答案 11 :(得分:0)
是的,您可以使用该语言编写一种语言的编译器。不,您不需要该语言的第一个编译器即可启动。
您需要引导的是该语言的实现。可以是编译器也可以是解释器。
从历史上看,语言通常被认为是解释性语言或编译语言。口译员仅为前者编写,而编译器仅为后者编写。因此,通常如果要为某种语言编写编译器,则第一个编译器将以其他某种语言编写以引导它,然后(可选)该编译器将针对主题语言进行重写。但是可以选择用另一种语言编写口译员。
这不只是理论上的。我碰巧目前正在自己做。我正在开发自己开发的Salmon语言编译器。我首先用C语言创建了Salmon编译器,现在我用Salmon编写了该编译器,所以我无需使用任何其他语言编写的Salmon编译器就可以使Salmon编译器正常工作。
答案 12 :(得分:0)
请注意,从技术上讲,您可以使用尚不存在的语言编写编译器。为了做到这一点,您创建了一个解释器,它是原始语言的次要版本,它通常很慢且无用,因为它在执行任何操作之前解释语言的每个语句。
如果您阅读它,它确实看起来完全像预期的语言,但它的执行过程要经过一些过程,将其转换为可执行文件的过程不止一个步骤。
这个编译器通常非常慢,因为它使用了一些适用于几乎所有现有语言的通用数学过程,但优点是下次除了在现有代码上使用生成的编译器外,你什么都不做。
这次当然不用解释了。