了解编译器的内部原理可以让你更高效地使用它。根据编译的工作顺序,深入研究编程语言和编译器是如何工作的。本文有很多链接、示例代码和图表来帮助您理解编译器。
作者备注:
这是我第二篇关于媒介的文章的转载。上一版有21000多名读者。很高兴能帮到你的学习,所以我完全按照上一版的评论重新写了一遍。
我选择了Rust作为本文的主要语言。它详细、高效、现代,似乎让设计编译器变得很容易。我很喜欢用。https://www.rust-lang.org/
写这篇文章的主要目的是吸引读者的注意力,而不是提供20多页让人头皮发麻的阅读材料。对于那些你感兴趣的更深层次的主题,文章中有许多链接可以引导你找到相关信息。维基百科的大部分链接。
谢谢大家的关注。我花了20多个小时写的这些文章,希望你会喜欢。请在文章底部的评论部分留下任何问题或建议。
简单介绍一下什么是编译器?
你口中的编程语言本质上只是一个软件。这个软件叫做编译器。编译器读取一个文本文件,经过大量处理,最终生成一个二进制文件。编译器的语言部分是它处理的文本样式。因为计算机只能读取1和0,而且人们编写Rust程序比直接编写二进制程序简单得多,所以使用编译器将人类可读的文本转换成计算机可以识别的机器码。
编译器可以是任何可以将文本文件转换成其他文件的程序。例如,有一个用Rust语言编写的编译器,可以将0转换为1,将1转换为0:
一个
2
三
四
五
六
七
八
九
10
11
12
13
14
//将0转换为1,将1转换为0的示例编译器。
fn main(){
let input = " 1 0 1 A 1 0 1 3
//迭代输入中的每个字符` c '
让输出:String= input.chars()。地图(|c|
ifc = = ' 1 ' { ' 0 '
elseifc = = ' 0 ' { ' 1 '
else{c}//如果不是0或1,就不要管它
).collect();
普林。(" {} ",输出);// 0 1 0 A 0 1 0 3
}
编译器是做什么的?简而言之,编译器获取源代码并生成一个二进制文件。因为将复杂的、人类可读的代码直接转换成0/1二进制非常复杂,所以编译器在生成可运行的程序之前有几个步骤:
从你给定的源代码中读取单个词。把这些词按照单词、数字、符号、运算符进行分类。通过模式匹配从分好类的单词中找出运算符,明确这些运算符想进行的运算,然后产生一个运算符的树(表达式树)。最后一步遍历表达式树中的所有运算符,产生相应的二进制数据。虽然我说编译器直接从表达式树转换成二进制,但实际上它会生成汇编代码,然后汇编代码会被汇编/编译成二进制数据。汇编程序就像一种高级的、人类可读的二进制系统。这里有更多关于汇编语言的阅读材料。
什么是翻译?
解释器很像编译器。他们还阅读编程语言的代码,然后进行处理。但是,解释器跳过代码生成,然后立即编译并执行AST。解释器的最大优点是在调试过程中运行程序所需的时间。编译器可能在一秒到几分钟内编译一个程序,但是解释器可以不编译而立即开始执行程序。解释器最大的缺点是必须安装在用户的计算机上才能执行程序。
虽然本文主要是关于编译器的,但是需要明确编译器和解释器的区别,以及与编译器相关的内容。
1.词汇分析
第一步是逐字拆分输入。这一步叫做词法分析,或者分词。这一步的关键是我们把字符组合成单词、标识符、符号等等。词法分析大多不需要处理逻辑运算,比如计算2+2——其实这个表达式只有三个标签:一个数字:2,一个加号,另一个数字:2。
让我们假设您正在解析像12+3这样的字符串:它读入字符1、2、+和3。我们已经分裂了这些人物,但现在必须把他们结合起来;这是分词器的主要任务之一。例如,我们得到两个独立的字符1和2,但是我们需要将它们放在一起,并将其解析为一个整数。至于+,也需要识别为加号而不是它的字符值——字符值为43。
如果你能看懂上面的代码并理解这么做的意义,那么下一个Rust单词分隔符会把数字组合成32位整数,加号最后会标记出值Plus。
铁锈操场
您可以点击Rust playgroud左上角的“运行”按钮,在您的浏览器中编译并执行代码。
在编程语言的编译器中,词法分析器可能需要许多不同类型的标记。例如:符号、数字、标识符、字符串、运算符等。从源文件中提取什么标签完全取决于编程语言本身。
一个
2
三
四
五
六
七
八
九
10
intmain(){
inta
intb
a = b = 4;
返回-b;
}
扫描仪生产:
【关键字(Int),Id(“main”),符号(LParen),符号(RParen),符号(LBrace),关键字(Int),Id(“a”),符号(分号),关键字(Int),Id(“b”),符号(分号),Id(“a”),运算符(赋值),Id(“b”),
运算符(赋值)、整数(4)、符号(分号)、关键字(返回)、标识(“a”)、运算符(减)、标识(“b”)、符号(分号)、符号(跑道)]
对C语言的样例代码进行了词法分析,并输出了其标记
2.分析
解析器真的是语法解析的核心。解析器提取词法分析器生成的标记,试图判断它们是否符合特定的模式,然后将这些模式与表达式(如函数调用、变量调用和数学运算)相关联。解析器逐字定义编程语言的语法。
int a = 3和a: int = 3的区别在于解析器处理。语法分析器决定语法的外部形式。它确保括号和花括号的数量是平衡的,每个语句以分号结束,每个函数都有一个名称。当标签不符合预期模式时,解析器将知道标签的顺序不正确。
您可以编写几种不同类型的解析器。最常见的解析器之一是自顶向下、递归降级的解析器。递归退化解析器是最简单最容易理解的。我写的所有解析器示例都是基于递归降级的。
解析器解析的语法可以用语法来表示。像EBNF这样的语法可以描述解析简单数学运算的解析器,例如12+3:
一个
2
三
expr = additive _ expr
additive_expr= term,('+'| '-'),term;
术语=数字;
简单加减表达式的EBNF语法
请记住,语法文件不是解析器,而是解析器的表达式。您可以围绕上面的语法创建一个解析器。语法文件可供人们使用,比直接阅读和理解解析器代码简单得多。
该语法的解析器应该是expr解析器,因为它与所有内容的顶层直接相关。唯一有效的输入必须是任何数字,加号或减号,任何数字。Expr需要一个加法_expr,主要出现在加减表达式中。Additive_expr首先需要一个项(一个数),然后是一个加号或减号,最后是另一个项。
分析12+3产生的样品AST
解析器生成的树结构称为抽象语法树,简称AST。Ast包含要执行所有操作。解析器不计算这些操作,它只是以正确的顺序收集标签。
我之前补充了我们的词法分析器代码,让它和我们的语法匹配,可以像图形一样产生AST。我用//BEgin PARSOR//和//END PARSOR//的注释标记了新解析器代码的开始和结束。
铁锈操场
我们可以更进一步。假设我们要支持无运算符的数字输入,或者加除法和乘法,甚至加优先级。可以简单地修改语法文件,任何调整都会直接反映在我们的解析器代码中。
一个
2
三
四
expr = additive _ expr
加法_expr=乘法_expr,{('+'| '-'),乘法_ expr };
乘法_expr= term,{("*"| "/"),term };
术语=数字;
新语法
https://play.rust-lang.org/? gist = 1587 a5dd 6109 f 70 cafe 68818 A8 C1 a 883 & amp;版本=夜间& amp模式=调试& ampedition=2018
为C语言语法编写的解析器(也称为词法分析器)和样本解析器。" if(net >:0.0)total+= net(1.0+税/100.0);"扫描仪形成一系列标签并对它们进行分类,如标识符、保留字、数字或运算符。后一个序列由解析器转换成语法树,然后由其他编译器分阶段处理。扫描器和解析器分别处理规则和独立于上下文的C语法部分。引自:Jochen Burghardt。来源。
3. 生成代码代码生成器接收一个AST,然后生成相应的代码或汇编代码。代码生成器必须以递归降序遍历AST中的所有内容——就像解析器工作一样——然后生成相应的内容,只是这里生成的不再是语法树,而是代码。
https://godbolt.org/z/K8416_
如果您复制并打开上面的链接,您可以在左侧看到示例代码生成的程序集代码。汇编代码的第三和第四行显示了编译器在AST中遇到常量时如何为常量生成相应的代码。
Godbolt编译器Explorer是一个很棒的工具,可以让你用高级语言写代码,查看它生成的汇编代码。您可能会有点困惑,想知道生成了什么样的代码,但不要忘记在您的编程语言编译器中添加优化选项,看看它有多智能。(-0代表生锈)
如果您对编译器如何用汇编语言在内存中保存局部变量感兴趣,本文(“代码生成”)将详细解释堆栈。大多数情况下,当变量不是局部变量时,高级编译器会在堆区为变量分配空,并保存在堆区而不是栈区。你可以从这个StackOverflow的回答中读到更多关于变量存储的内容。
因为编译是一个完全不同的复杂学科,这里就不多讨论了。我只想强调代码生成器的重要性和它的作用。另外,代码生成器不仅可以生成汇编代码。Haxe编译器有一个后端,可以生成六种以上不同的编程语言,包括C++、Java和Python。
后端指编译器的代码生成器或表达式解析器;所以前端是词法分析器和解析器。还有一个中间端,通常和优化和IR有关,后面会解释。后端通常独立于前端,只关心接收到的AST。这意味着相同的后端可以被多个不同的前端或语言重用。著名的GNU编译器集合就是这种情况。
找不到比我的C编译器后端更好的代码生成器例子;你可以在这里查看。
汇编代码生成后,会写入一个新的汇编文件(。s或。asm)。然后文件会传递给汇编器,汇编器是汇编语言的编译器,它会生成相应的二进制代码。之后,这些二进制代码将被写入新的目标文件(。o)。
目标文件是机器码,但不能执行。为了使它们成为可执行文件,目标文件需要链接在一起。链接器读取通用机器代码,然后将其转换为可执行文件、共享库或静态库。更多关于linkers的知识在这里。
链接器是根据操作系统而不同的应用程序。任何第三方链接器都应该能够编译后端生成的目标代码。所以写编译器的时候不需要自己创建链接器。
编译器可能有中间表示,简称IR。IR主要是在优化或翻译成另一种语言时,无损地表示原始指令。IR不再是原码;红外是无损简化,在代码中寻找潜在的优化。循环展开和矢量化都是通过红外完成的。更多关于IR相关优化可以在此PDF中找到。
总结
当你理解了编译器,你就可以更有效地使用你的编程语言。也许有一天你会对创造自己的编程语言感兴趣?希望这对你有帮助。
∑编辑|双子
来源|博乐在线
1.《编译程序 人人都能读懂的编译器原理》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。
2.《编译程序 人人都能读懂的编译器原理》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。
3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/guoji/1601113.html