用自己的语言编写一个编译器
直观地看来, Foo
语言的编译器似乎不能在Foo中编写。 更具体地说,语言Foo
的第一个编译器不能在Foo中编写,但是任何后续的编译器都可以为Foo
编写。
但这是真的吗? 对于第一个编译器本身写成的语言,我有一些非常模糊的回忆。 这是可能的,如果是的话如何?
这被称为“引导”。 您必须首先使用其他语言(通常是Java或C)为您的语言构build编译器(或解释器)。 一旦完成,您可以用Foo语言编写新版本的编译器。 您使用第一个引导程序编译器来编译编译器,然后使用此编译的编译器来编译其他所有内容(包括其自身的未来版本)。
大多数语言确实是以这种方式创build的,部分原因在于语言devise者喜欢使用他们正在创build的语言,另外也因为一个不重要的编译器常常成为语言“完整”的有用基准。
一个例子就是Scala。 它的第一个编译器是由Martin Odersky的实验语言Pizza创build的。 从版本2.0开始,编译器完全重写在Scala中。 从那时开始,由于新的Scala编译器可以用于编译自己的未来迭代,所以旧的Pizza编译器可以被完全抛弃。
我记得听过一个软件工程电台的播客,其中迪克·加布里埃尔(Dick Gabriel)谈到了通过在LISP 上写一个简单的版本来引导原来的LISP解释器,并将其组装成机器码。 从那时起,其余的LISPfunction都被LISP写入并解释。
对以前的答案增加好奇心。
下面是Linux From Scratch手册中的一个引用,在这个步骤中,从源头开始构buildGCC编译器。 (Linux From Scratch是一种安装Linux的方法,与安装发行版本截然不同,因为您必须真正编译目标系统的每个二进制文件。)
make bootstrap
“引导”目标不仅仅是编译GCC,而是编译几次。 它使用第一轮编译的程序进行第二次编译,然后再进行第三次编译。 然后比较这些第二和第三编译,以确保它可以完美地重现自己。 这也意味着编译正确。
“bootstrap”目标的使用是由于用来构build目标系统工具链的编译器可能没有目标编译器的相同版本。 以这种方式进行,一定要在目标系统中获得一个可以自行编译的编译器。
当你为C编写你的第一个编译器时,你用其他语言编写它。 现在,你已经有了一个C编译器,比如汇编器。 最后,你会来到你必须parsingstring的地方,特别是转义序列。 您将编写代码将\n
转换为十进制代码为10(和\r
为13等)的字符。
在编译器准备就绪后,您将开始在C中重新实现它。这个过程被称为“ bootstrapping ”。
stringparsing代码将变成:
... 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源代码将inheritance这个信息。 如果编译器自己编译,它会将这些知识传递给它的后代。 从这一点来看,从源头上看,编译器将无法做到什么。
如果你想在某些程序的源代码中隐藏病毒,你可以这样做:获取编译器的源代码,find编译函数的函数,并用下面的代码replace它:
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
的源代码,包括病毒,可能以某种方式encryption,所以从search结果二进制文件中不明显。 这可以确保编译器自身将保留病毒注入代码。
B与我们想用我们的病毒replace的function是一样的。 例如,可能是来自Linux内核的源文件“login.c”中的“login”函数。 除了正常的密码之外,我们可以用接受root帐户密码“joshua”的版本replace它。
如果将其编译并将其作为二进制文件传播,则无法通过查看源代码find病毒。
想法的原始来源: http : //cm.bell-labs.com/who/ken/trust.html
你不能编写自己的编译器,因为你没有任何东西可以编译你的起始源代码。 有两种方法可以解决这个问题。
最不喜欢的是以下。 您在汇编语言(yuck)中编写了一个最小化的编译器,以获得最less的语言集合,然后使用该编译器来实现语言的额外function。 build立你的方式,直到你有一个具有所有语言function的编译器。 通常只有在没有其他select的情况下才能完成一个痛苦的过程。
首选的方法是使用交叉编译器。 您在另一台机器上更改现有编译器的后端,以创build在目标机器上运行的输出。 然后你有一个很好的完整的编译器,并在目标机器上工作。 最受欢迎的是C语言,因为现有的编译器有很多可插拔的后端可以被换出。
一个鲜为人知的事实是,GNU C ++编译器有一个只使用C子集的实现。 原因是通常很容易为新的目标机器find一个C编译器,从而允许您从中构build完整的GNU C ++编译器。 现在你已经开始在目标机器上安装一个C ++编译器了。
一般来说,你需要有一个工作(如果主要的)编译器工作的第一个 – 那么你可以开始考虑使自己托pipe。 这实际上被认为是一些语言的重要里程碑。
从我记得的“单声道”中,他们可能需要添加一些东西来反思才能使其工作:单声道团队不断指出有些事情根本不可能用Reflection.Emit
; 当然,MS团队可能会certificate他们错了。
这有几个真正的优势:对于初学者来说这是一个相当不错的unit testing! 而且你只有一种语言可以担心(例如,C#专家可能不太了解C ++;但是现在可以修复C#编译器)。 但是,我不知道这里的工作是否有一定的专业自豪感:他们只是希望它能够自我托pipe。
不是一个编译器,但我最近一直在做一个自我托pipe的系统; 代码生成器用于生成代码生成器…所以如果模式更改,我只是简单地运行它本身:新版本。 如果有错误,我只是回到较早的版本,然后再试一次。 非常方便,而且很容易维护。
更新1
我刚刚在PDC上观看了这个 Anders的video ,(大概一个小时),他提供了更多有效的理由 – 所有关于编译器的服务。 只是为了logging。
在编译器理论中,可以使用T图来描述引导过程。 例如,看到这里 。
在我的学士论文中,我使用这些T图来描述从不同平台以不同格式存储大量电子文档时转换和显示文档的过程。
这是一个转储(难以search的主题,实际上):
-
短暂聊天
-
C
这也是PyPy和Rubinius的想法:
(我认为这也可能适用于Forth ,但我对Forth一无所知。)
GNU,即GNU Ada编译器,需要完全构buildAda编译器。 当将其移植到没有GNAT二进制文件的平台上时,这可能会很痛苦。
实际上,由于上述原因,大多数编译器都是用他们编译的语言编写的。
第一个引导程序编译器通常用C,C ++或Assembly编写。
Mono项目的C#编译器现在已经“自我托pipe”了很长一段时间,这意味着它是用C#编写的。
我所知道的是编译器是作为纯C代码启动的,但是一旦ECMA的“基本”function被实现,他们开始用C#重写编译器。
我没有意识到用相同的语言编写编译器的好处,但是我相信它至less要用到语言本身可以提供的特性(例如,C不支持面向对象的编程) 。
你可以在这里find更多的信息。
也许你可以写一个BNF来描述BNF。