如何编写一个非常基本的编译器


214

高级编译器喜欢gcc根据代码的编写语言(例如C,C ++等)将代码编译为机器可读文件。实际上,它们根据相应语言的库和功能来解释每个代码的含义。如果我错了纠正我。

我希望通过编写一个非常基本的编译器(可能使用C语言)来编译静态文件(例如,文本文件中的Hello World)来更好地理解编译器。我尝试了一些教程和书籍,但所有这些都是针对实际案例的。它们处理具有与相应语言相关的含义的动态代码的编译。

如何编写基本的编译器以将静态文本转换为机器可读文件?

下一步将变量引入编译器。想象一下,我们想编写一个只编译某种语言功能的编译器。

引入实用的教程和资源受到高度赞赏:-)



您是否尝试过lex / flex和yacc / bison?
mouviciel 2012年

15
@mouviciel:这不是学习构建编译器的好方法。这些工具为您完成了大量的辛苦工作,因此您永远不会真正做到并了解其完成方式。
梅森惠勒

11
@Mat有趣的是,您的第一个链接提供404,而第二个链接现已标记为该问题的重复项。
Ruslan

Answers:


326

介绍

典型的编译器执行以下步骤:

  • 解析:将源文本转换为抽象语法树(AST)。
  • 对其他模块的引用的解析(C将此步骤推迟到链接)。
  • 语义验证:清除语法上不正确的语句,例如,无法访问的代码或重复的声明。
  • 等效转换和高级优化:AST被转换为表示具有相同语义的更有效的计算。例如,这包括早期计算公共子表达式和常量表达式,消除过多的局部分配(另请参见SSA)等。
  • 代码生成:具有跳转,寄存器分配等功能,将AST转换为线性低级代码。在此阶段可以内联一些函数调用,展开某些循环等。
  • 窥孔优化:扫描低级代码以查找简单的本地效率低下的现象,并将其消除。

大多数现代编译器(例如gcc和clang)都重复最后两个步骤。他们使用中间的低级但与平台无关的语言来生成初始代码。然后,将该语言转换为平台特定的代码(x86,ARM等),并以平台优化的方式做大致相同的事情。这包括例如在可能的情况下使用向量指令,对指令进行重新排序以提高分支预测效率等。

之后,目标代码已准备好进行链接。大多数本机代码编译器都知道如何调用链接器以生成可执行文件,但这本身不是编译步骤。在Java和C#等语言中,链接可能是完全动态的,由VM在加载时完成。

记住基础

  • 让它起作用
  • 使其美丽
  • 提高效率

这个经典序列适用于所有软件开发,但需要重复。

专注于序列的第一步。创建可能可行的最简单的方法。

看书!

阅读Aho和Ullman撰写的《龙书》。这是经典之举,今天仍然非常适用。

现代编译器设计也受到称赞。

如果现在这些东西对您来说太难了,请先阅读一些有关解析的介绍。通常,解析库包括简介和示例。

确保您习惯使用图,尤其是树。这些东西是程序在逻辑级别上构成的。

很好地定义您的语言

使用所需的任何符号,但请确保您对语言有完整且一致的描述。这包括语法和语义。

现在是时候用您的新语言编写代码片段,作为将来编译器的测试用例。

使用您喜欢的语言

完全可以使用Python或Ruby或任何您喜欢的语言编写编译器。使用简单的算法,您会很好理解。第一个版本不必快速,高效或功能完善。它只需要足够正确并且易于修改。

如果需要的话,也可以用不同的语言编写编译器的不同阶段。

准备编写很多测试

您的整个语言都应包含在测试用例中;有效地将由他们来定义。熟悉您首选的测试框架。从第一天开始编写测试。专注于接受正确代码的“正”测试,而不是检测错误代码。

定期运行所有测试。在继续之前,请修复损坏的测试。最终使用无法接受有效代码的定义不明确的语言而感到遗憾。

创建一个好的解析器

解析器生成器很多。选择任何你想要的。您也可以从头开始编写自己的解析器,但它只是值得的,如果你的语言的语法是死的简单。

解析器应检测并报告语法错误。编写大量的测试用例,包括正数和负数;重用您在定义语言时编写的代码。

解析器的输出是一个抽象语法树。

如果您的语言具有模块,则解析器的输出可能是您生成的“目标代码”的最简单表示。有很多简单的方法可以将树转储到文件中并快速将其重新加载。

创建一个语义验证器

您的语言很可能允许在语法上正确的构造,这些构造在某些情况下可能毫无意义。一个示例是相同变量的重复声明或传递错误类型的参数。验证器将在树上检测到此类错误。

验证程序还将解析对使用您的语言编写的其他模块的引用,加载这些其他模块并在验证过程中使用。例如,此步骤将确保从另一个模块传递给函数的参数数量正确。

同样,编写并运行许多测试用例。在故障排除中,琐碎的案例与智能和复杂一样不可或缺。

产生程式码

使用您知道的最简单的技术。通常,直接将语言结构(如if语句)转换为轻量化的代码模板是可以的,与HTML模板不同。

同样,忽略效率,而专注于正确性。

定位与平台无关的低层虚拟机

我想除非您对特定于硬件的细节非常感兴趣,否则您将忽略低级内容。这些细节是复杂的。

您的选择:

  • LLVM:通常针对x86和ARM,允许高效的机器代码生成。
  • CLR:针对.NET,主要是基于x86 / Windows的;具有良好的准时性。
  • JVM:面向Java世界,相当多平台,具有良好的JIT。

忽略优化

优化很难。优化几乎总是过早的。生成效率低下但正确的代码。在尝试优化结果代码之前,请实现整个语言。

当然,可以引入简单的优化。但是,在编译器稳定之前,请避免使用任何狡猾多毛的东西。

所以呢?

如果这一切对您来说都不是太吓人,请继续!对于简单的语言,每个步骤可能比您想象的要简单。

从编译器创建的程序中看到“ Hello world”可能是值得的。


45
这是我见过的最好的答案之一。
gahooa

11
我认为您错过了一部分问题... OP希望编写一个非常基本的编译器。我认为您超出了这里的基础知识。
marco-fiset 2012年

22
相反,@ marco-fiset,我认为这是一个出色的答案,确实告诉了OP如何做一个非常基本的编译器,同时指出了避免和定义更高级阶段的陷阱。
smci 2013年

6
这是我在整个Stack Exchange领域中见过的最好的答案之一。荣誉!
安德烈·泰拉

3
从编译器创建的程序中看到“ Hello world”可能是值得的。-独立
slier

27

Jack Crenshaw的“ 让我们构建一个编译器 ”虽然未完成,但却是引人入胜的引言和教程。

Nicklaus Wirth的《编译器构造》是一本很好的教科书,介绍了简单编译器构造的基础知识。他专注于自上而下的递归下降,让我们面对现实,这比lex / yacc或flex / bison更容易。他的小组编写的原始PASCAL编译器是通过这种方式完成的。

其他人也提到过各种龙书。


1
Pascal的优点之一是,必须在使用之前定义或声明所有内容。因此,可以一次性编译。Turbo Pascal的3.0就是这样一个例子,有很多关于内部文件的位置
tcrosley 2015年

1
PASCAL在设计时特别考虑了一次编译和链接。Wirth的编译器书提到了多遍编译器,并补充说他知道一个PL / I编译器需要70(是70)遍。
John R. Strohm 2015年

使用前的强制性声明可以追溯到ALGOL。当试图建议添加默认类型规则(与FORTRAN相似)时,ALGOL委员会将Tony Hoare的注意力固定住了。他们已经知道可能会产生的问题,名称中的印刷错误和默认规则会产生有趣的错误。
John R. Strohm 2015年

1
这是原始作者本人提供的这本书的更新和更完整的版本: stack.nl/~marcov/compiler.pdf 请编辑您的答案并添加以下内容:)
sonnet

16

我实际上是从为Brainfuck编写编译器开始的。这是一种相当晦涩难懂的语言,但是只有8条指令可以实现。它尽可能地简单,如果发现语法不正确,则其中包含等效的C指令,可用于所涉及的命令。


7
但是,一旦准备好BF编译器,就必须在其中编写代码:(
500-Internal Server Error

@ 500-InternalServerError使用C子集方法
世界工程师

12

如果您真的只想编写机器可读的代码,而不是针对虚拟机,那么您将必须阅读英特尔手册并了解

  • 一种。链接和加载可执行代码

  • b。COFF和PE格式(对于Windows),或者了解ELF格式(对于Linux)

  • C。了解.COM文件格式(比PE更容易)
  • d。了解组装商
  • e。了解编译器和编译器中的代码生成引擎。

比说的难得多。我建议您阅读C ++的编译器和解释器作为起点(作者Ronald Mak)。另外,Crenshaw的“让编译器编译”也可以。

如果您不想这样做,也可以编写自己的VM并编写针对该VM的代码生成器。

提示:首先学习Flex和Bison。然后继续构建自己的编译器/ VM。

祝好运!


7
我认为针对LLVM而非真正的机器代码是当今最好的方法。
9000 2012年

我同意,我已经有一段时间关注LLVM了,应该说这是我多年来针对它所需要的程序员努力中最好的事情之一!
Aniket Inge 2012年

2
MIPS并使用spim运行它呢?还是MIX

@MichaelT我还没有使用过MIPS,但是我相信它会很好。
Aniket Inge 2012年

@PrototypeStark RISC指令集,当今仍在使用的真实世界处理器(请注意它将可以转换为嵌入式系统)。完整的说明集在Wikipedia上。从网上看,有很多示例,并且在许多学术课程中都将其用作机器语言编程的目标。SO上有一些活动。

10

简单编译器的DIY方法可能如下所示(至少这就是我的uni项目的外观):

  1. 定义语言的语法。上下文无关。
  2. 如果您的语法还不是LL(1),请立即执行。请注意,在普通CF语法中看起来不错的一些规则可能看起来很丑。也许您的语言太复杂了...
  3. 编写Lexer,将文本流切成标记(单词,数字,文字)。
  4. 为您的语法编写自上而下的递归下降解析器,该解析器接受或拒绝输入。
  5. 将语法树生成添加到解析器中。
  6. 从语法树中编写机器代码生成器。
  7. Profit&Beer,或者您可以开始思考如何做更聪明的解析器或生成更好的代码。

应该有大量的文献详细描述每个步骤。


第七点是OP的要求。
Florian Margaine 2012年

7
1-5是无关紧要的,因此不应引起如此密切的关注。6是最有趣的部分。不幸的是,在臭名昭著的龙书之后,大多数书都遵循相同的模式,对语法的分析过于关注,而使代码转换超出了范围。
SK-logic
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.