大家都能明白的编译原理

 2024-02-26 05:02:13  阅读 0

了解编译器的内部结构可以让您更有效地使用它。 遵循编译工作顺序,逐步了解编程语言和编译器的工作原理。 本文提供了大量链接、示例代码和图表来帮助您了解编译器。

作者注:

这是我在网上发表的第二篇文章的转载,该文章的阅读量已超过 21,000 次。 很高兴能够帮助大家学习,所以我根据之前版本的评论完全重写了一遍。

我选择 Rust 作为本文的主要语言。 它详尽、高效、现代,似乎是有意设计的,旨在使编译器的设计变得容易。 我喜欢使用它。

本文的目的主要是为了吸引读者的注意力,而不是提供 20 多页令人麻木的阅读材料。 对于您感兴趣的更深入的主题,文章中的许多链接将引导您找到相关材料。 大多数链接到维基百科。

感谢您的关注,希望您喜欢我花了 20 多个小时写的这些文章。 有任何问题或建议请随时在文章底部的评论中留下。

简单介绍

什么是编译器?

你所说的编程语言本质上只是一个软件。 该软件称为编译器。 编译器读取一个文本文件,经过大量处理,最终生成一个二进制文件。 编译器的语言部分是它处理的文本样式。 由于计算机只能读取 1 和 0,而且人们编写 Rust 程序比直接编写二进制程序容易得多,因此使用编译器将人类可读的文本转换为计算机可以识别的机器代码。

编译器可以是任何可以将文本文件转换为其他文件的程序。 例如,下面是一个用 Rust 编写的编译器,它将 0 转换为 1,将 1 转换为 0:

// 将 0 转为 1,将 1 转为 0。

fn 主() {

让输入=“1 0 1 A 1 0 1 3”;

// 输入中的每个 `c`

让 := input.chars().map(|c|

如果 c == '1' { '0' }

否则如果 c == '0' { '1' }

else { c } // 如果不是 0 或 1,则不管它

).();

!("{}", ); // 0 1 0 A 0 1 0 3

编译器有什么作用?

简而言之,编译器获取源代码并生成二进制文件。 由于将复杂的、人类可读的代码直接转换为 0/1 二进制文件很复杂,因此编译器在生成可运行的程序之前需要执行多个步骤:

从给定的源代码中读取一个单词。

将这些单词分类为单词、数字、符号和运算符。

通过模式匹配从分类词中找到运算符,明确这些运算符要执行的操作,然后生成运算符树(表达式树)。

最后一步遍历表达式树中的所有运算符并产生相应的二进制数据。

虽然我说编译器直接从表达式树转换为二进制,但它实际上会生成汇编代码,然后将其汇编/编译为二进制数据。 汇编器就像一个高级的、人类可读的二进制文件。 更多关于汇编语言的内容请阅读这里。

编译原理词法分析器代码_c语言词法分析器代码简短_c语言词法分析器代码

什么是口译员?

解释器非常类似于编译器,它读取编程语言的代码,然后处理该代码。 然而,解释器会跳过代码生成并动态编译和执行 AST。 解释器的最大优点是调试时运行程序所需的时间。 编译器可能需要一秒到几分钟的时间来编译程序,而解释器可以立即开始执行程序而无需编译。 解释器的最大缺点是必须先将其安装在用户的计算机上,然后才能执行程序。

编译原理词法分析器代码_c语言词法分析器代码_c语言词法分析器代码简短

虽然本文主要是关于编译器的,但是了解编译器和解释器之间的区别以及编译器相关的内容也很重要。

1. 词法分析

第一步是逐字分割输入。 这一步称为词法分析,或分词。 这一步的关键是我们将字符组合成我们需要的单词、标识符、符号等。 词法分析大多不需要处理诸如计算2+2之类的逻辑运算——事实上,这个表达式只有三个标记:一个数字:2、一个加号和另一个数字:2。

假设您正在解析像 12+3 这样的字符串:它读取字符 1、2、+ 和 3。我们已经将这些字符分开,但现在必须将它们组合起来; 这是分词器的主要任务之一。 例如,我们得到两个单独的字符1和2,但是我们需要将它们放在一起并解析为一个整数。 至于+,它也需要被识别为加号,而不是它的字符值——即43。

如果你能阅读上面的代码并理解其中的含义,下面的 Rust 会将数字组合成 32 位整数,并且加号将在末尾标记值 Plus(加号)。

您可以点击Rust左上角的“运行”按钮,在浏览器中编译并执行代码。

在编程语言的编译器中,词法分析器可能需要许多不同类型的标记。 例如:符号、数字、标识符、字符串、运算符等。知道从源文件中提取什么标记完全取决于编程语言本身。

int main() {

整数a;

整数b;

a = b = 4;

a - b;

:

[(Int), Id("main"), (), (), (), (Int), Id("a"), (), (Int), Id("b"), (), Id (“a”),(),Id(“b”),

(), (4), (), (),Id("a"), (减), Id("b"), (), ()]

C语言示例代码已进行词法分析并输出其标记

2. 分析

解析器确实是语法解析的核心。 解析器获取词法分析器生成的标记,并尝试确定它们是否匹配特定模式。 然后,它将这些模式与函数调用、变量调用和数学运算等表达式相关联。 解析器逐字定义编程语言的语法。

int a = 3 和 a: int = 3 的区别在于解析器的处理。 解析器决定语法的外部形式。 它确保左右括号和大括号的数量均衡,每个语句末尾都有分号,并且每个函数都有一个名称。 当标记与预期模式不匹配时,解析器就知道标记的顺序错误。

您可以编写几种不同类型的解析器。 最常见的解析器之一是自上而下的递归退化解析器。 递归降级解析器是最简单使用和最容易理解的。 我编写的所有解析器示例都是基于递归降级。

解析器解析的语法可以使用语法来表示。 像 EBNF 这样的语法可以描述一个用于解析简单数学运算的解析器,比如 12+3:

表达式 = ;

= 术语, ('+' | '-'), 术语;

术语 = ;

简单加法和减法表达式的 EBNF 语法

请记住,语法文件不是解析器,而是解析器的表达式。 您可以围绕上述语法创建一个解析器。 语法文件可供人类使用,并且比直接阅读和理解解析器代码简单得多。

该语法的解析器应该是 expr 解析器,因为它直接位于一切相关的顶层。 有效输入必须是任意数字,正负号,任意数字。 expr 需要一个,主要出现在加法和减法表达式中。 首先它需要一个术语(一个数字),然后是一个加号或减号,最后是另一个术语。

编译原理词法分析器代码_c语言词法分析器代码简短_c语言词法分析器代码

解析12+3生成的样本AST

解析器在解析过程中生成的树结构称为抽象语法树,简称AST。 ast 包含所有要执行的操作。 解析器不会计算这些操作,它只是以正确的顺序收集其中的标记。

我之前补充了我们的词法分析器代码,使其与我们的语法相匹配,并且可以生成类似图形的 AST。 我用 // BEGIN // 和 // END // 注释标记了新解析器代码的开始和结束。

我们可以再深入一点。 假设我们想要支持只有数字而没有运算符的输入,或者添加除法和乘法,甚至添加优先级。 这完全可以通过对语法文件的简单修改来实现,并且任何调整都将直接反映在我们的解析器代码中。

表达式 = ;

= , { ('+' | '-'), };

= 术语, { ("*" | "/"), 术语 } ;

术语 = ;

新语法

c语言词法分析器代码_编译原理词法分析器代码_c语言词法分析器代码简短

为 C 语言语法编写的解析器(也称为词法分析器)和解析器示例。 从字符序列“if(net>0.0)total+=net(1.0+tax/100.0);”的开头开始,扫描器组成一系列标记并将它们分类为例如标识符、保留字、数字、或操作员。 后一个序列由解析器转换为语法树,然后由其他编译器分阶段处理。 扫描器和解析器分别处理 C 语法的常规部分和上下文无关部分。 引自:.来源。

3.生成代码

代码生成器接收AST,然后生成相应的代码或汇编代码。 代码生成器必须以递归降序遍历 AST 中的所有内容——就像解析器的工作方式一样——然后生成相应的内容,只不过这里它不再是语法树,而是代码。

如果复制并打开上面的链接,您可以看到左侧示例代码生成的汇编代码。 汇编代码的第三行和第四行显示了编译器在 AST 中遇到常量时如何为常量生成相应的代码。

是一个很棒的工具,允许您用高级语言编写代码并查看它生成的汇编代码。 您可能会有点困惑,想知道正在生成什么样的代码,但不要忘记向您的编程语言编译器添加优化选项,以了解它到底有多智能。 (对于 Rust 来说是 -O )

如果您对编译器如何以汇编语言将局部变量保存到内存中感兴趣,本文(“代码生成”部分)非常详细地解释了堆栈。 大多数情况下,当变量不是局部变量时,高级编译器会在堆区为变量分配空间,并将其保存到堆区而不是栈区。 您可以在此答案中阅读有关变量存储的更多信息。

因为装配是一个完全不同且复杂的主题,所以我不会在这里过多讨论。 我只是想强调代码生成器的重要性及其用途。 此外,代码生成器不仅仅可以生成汇编代码。 Haxe 编译器的后端可以生成 6 种以上不同的编程语言:包括 C++、Java 和 .

后端是指编译器的代码生成器或表达式解析器; 因此前端是词法分析器和解析器。 还有一个中端,通常和优化、IR相关,后面会解释。 后端通常与前端无关,后端只关心它接收到的 AST。 这意味着相同的后端可以重复用于多种不同的前端或语言。 著名的GNU 就是这种情况。

我找不到比我的 C 编译器后端更好的代码生成器示例; 你可以在这里查看。

生成汇编代码后,将汇编代码写入新的汇编文件(.s 或.asm)。 然后该文件被传递到汇编器(一种汇编语言编译器),它生成相应的二进制代码。 然后,这些二进制代码将被写入新的目标文件 (.o)。

目标文件是机器代码,但它们不可执行。 为了使它们可执行,需要将目标文件链接在一起。 链接器读取通用机器代码并将其转换为可执行文件、共享库或静态库。 有关链接器的更多信息请参见此处。

链接器是一个根据操作系统而有所不同的应用程序。 任何第三方链接器都应该能够编译后端生成的目标代码。 因此,在编写编译器时无需创建自己的链接器。

编译原理词法分析器代码_c语言词法分析器代码简短_c语言词法分析器代码

编译器可能有一个中间表示,简称 IR。 IR主要用于在优化或翻译成另一种语言时无损地表示原始指令。 IR不再是原来的代码; IR 是一种无损简化,用于发现代码中潜在的优化。 循环展开和矢量化都是使用 IR 完成的。 有关 IR 相关优化的更多信息可以在此 PDF 中找到。

总结

当您了解编译器时,您可以更有效地使用您的编程语言。 也许有一天您会对创建自己的编程语言感兴趣? 我希望这有帮助。

如本站内容信息有侵犯到您的权益请联系我们删除,谢谢!!


Copyright © 2020 All Rights Reserved 京ICP5741267-1号 统计代码