直观地说,Foo语言的编译器本身似乎不能用Foo来编写。更具体地说,Foo语言的第一个编译器不能用Foo编写,但任何后续的编译器都可以为Foo编写。

但这是真的吗?我隐约记得读过一种语言,它的第一个编译器是用“自身”编写的。这可能吗?如果可能,如何实现?


当前回答

当你为C编写第一个编译器时,你是用其他语言编写的。现在,你有一个C语言的编译器,比如说,汇编器。最终,您将不得不解析字符串,特别是转义序列。您将编写代码将\n转换为十进制代码10(和\r转换为13,等等)的字符。

编译器准备好后,你将开始用c重新实现它,这个过程被称为“引导”。

字符串解析代码将变成:

...
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”作为普通密码。

如果你编译它并以二进制文件的形式传播,就无法通过查看源代码来找到病毒。

这个想法的最初来源是:https://web.archive.org/web/20070714062657/http://www.acm.org/classics/sep95/

其他回答

GNAT是GNU Ada编译器,需要完全构建Ada编译器。当将其移植到一个没有现成的GNAT二进制文件的平台时,这可能是一个麻烦。

在之前的答案上增加好奇心。

这里引用了Linux from Scratch手册中的一段话,说明了从源代码开始构建GCC编译器的步骤。(Linux From Scratch是一种安装Linux的方式,它与安装发行版完全不同,因为你必须编译目标系统的每一个二进制文件。)

做引导 “bootstrap”目标不只是编译GCC,而是多次编译它。它使用在第一个编译的程序 进行第二次编译,然后再次进行第三次。然后比较第二和第三个 编译以确保它能够完美地复制自己。这也意味着它是正确编译的。

使用“bootstrap”目标的动机是,用于构建目标系统工具链的编译器可能与目标编译器的版本不完全相同。在目标系统中,以这种方式进行操作一定会获得一个可以编译自身的编译器。

我记得我听过一个软件工程广播播客,其中Dick Gabriel谈到了如何在纸上用LISP编写一个最简单的版本,然后手工将其组装成机器代码,从而引导最初的LISP解释器。从那时起,其余的LISP特性都是用LISP编写和解释的。

实际上,大多数编译器都是用它们所编译的语言编写的,原因如上所述。

第一个引导编译器通常是用C、c++或Assembly编写的。

一般来说,你需要先让编译器工作(如果是原始的),然后你才能开始考虑让它自托管。在某些语言中,这实际上被认为是一个重要的里程碑。

从我对“mono”的印象来看,他们很可能需要给反射添加一些东西来让它工作:mono团队一直指出,有些东西根本不可能用reflection . emit;当然,微软团队可能会证明他们错了。

这有一些真正的优点:对于初学者来说,这是一个相当好的单元测试!你只需要担心一种语言(也就是说,c#专家可能不太懂c++;但是现在你可以修复c#编译器)。但我想知道,这里是否存在某种职业自豪感:他们只是希望它能够自我托管。

不完全是编译器,但我最近一直在研究一个自托管系统;代码生成器用于生成代码生成器…所以如果模式改变了,我简单地运行它本身:新版本。如果有错误,我就返回到早期版本并重试。非常方便,也很容易维护。


更新1

我刚刚看了Anders在PDC的视频,(大约一个小时后)他给出了一些更合理的理由——都是关于编译器即服务的。只是为了记录。