人人都能读懂的编译器原理

人人都能读懂的编译器原理

理解编译器内部原理,可以让你更高效利用它。按照编译的工作顺序,逐步深入编程语言和编译器是怎样工作的。本文有大量的链接、样例代码和图表帮助你理解编译器。
作者注:
这是我在Medium上的第二篇文章的再版,上一版有超过21000的阅读量。很高兴我能够帮助到各位的学习,因此我根据上一版的评论,完完全全重写了。
我选择Rust作为这篇文章的主要语言。它是一种详尽的、高效的、现代的而且看起来特意使得设计编译器变得简单。我很喜欢使用它。https://www.rust-lang.org/
写这篇文章的目的主要是吸引读者的注意力,而不是提供20多页的令人头皮发麻的阅读材料。对于那些你感兴趣的更深层次的话题,文章中有许多链接会引导你找到相关的资料。大多数链接到维基百科。
感谢你的关注,我希望你能够喜欢这些我花费了超过20个小时的写出的文章。欢迎在文章底部评论处留下任何问题或者建议。
简单介绍
编译器是什么?
你口中所说的编程语言本质上只是一个软件,这个软件叫做编译器,编译器读入一个文本文件,经过大量的处理,最终产生一个二进制文件。编译器的语言部分就是它处理的文本样式。因为电脑只能读取1和0,而人们编写Rust程序要比直接编写二进制程序简单地多,因此编译器就被用来把人类可读的文本转换成计算机可识别的机器码。
编译器可以是任何可以把文本文件转换成其他文件的程序。例如,下面有一个用Rust语言写的编译器把0转换成1,把1转换成0:
//Anexamplecompilerthatturns0sinto1s,and1sinto0s.
fnmain(){
letinput=”101A1013″;
//iterateovereverycharacter`c`ininput
letoutput:String=input.chars().map(|c|
ifc==’1′{‘0’}
elseifc==’0′{‘1’}
else{c}//ifnot0or1,leaveitalone
).collect();
println!(“{}”,output);//010A0103
}
编译器是做什么的?
简言之,编译器获取源代码,产生一个二进制文件。因为从复杂的、人类可读的代码直接转化成0/1二进制会很复杂,所以编译器在产生可运行程序之前有多个步骤:
从你给定的源代码中读取单个词。
把这些词按照单词、数字、符号、运算符进行分类。
通过模式匹配从分好类的单词中找出运算符,明确这些运算符想进行的运算,然后产生一个运算符的树(表达式树)。
最后一步遍历表达式树中的所有运算符,产生相应的二进制数据。
尽管我说编译器直接从表达式树转换到二进制,但实际上它会产生汇编代码,之后汇编代码会被汇编/编译到二进制数据。汇编程序就好比是一种高级的、人类可读的二进制。更多关于汇编语言的阅读资料在这里。
解释器是什么?
解释器非常像编译器,它也是读入编程语言的代码,然后处理这些代码。尽管如此,解释器会跳过了代码生成,然后即时编译并执行AST。解释器较大的优点就在于在你debug期间运行程序所消耗的时间。编译器编译一个程序可能在一秒到几分钟不等,然而解释器可以立即开始执行程序,而不必编译。解释器较大的缺点在于它必须安装在用户电脑上,程序才可以执行。
虽然这篇文章主要是关于编译器的,但是对于编译器和解释器之间的区别和编译器相关的内容一定要弄清楚。
1.词法分析
第一步是把输入一个词一个词的拆分开。这一步被叫做词法分析,或者说是分词。这一步的关键就在于我们把字符组合成我们需要的单词、标识符、符号等等。词法分析大多都不需要处理逻辑运算像是算出2+2–其实这个表达式只有三种标记:一个数字:2,一个加号,另外一个数字:2。
让我们假设你正在解析一个像是12+3这样的字符串:它会读入字符1,2,+,和3。我们已经把这些字符拆分开了,但是现在我们必须把他们组合起来;这是分词器的主要任务之一。举个例子,我们得到了两个单独的字符1和2,但是我们需要把它们放到一起,然后把它们解析成为一个整数。至于+也需要被识别为加号,而不是它的字符值–字符值是43。
如果你可以阅读过上面的代码,并且弄懂了这样做的含义,接下来的Rust分词器会组合数字为32位整数,加号就最后了标记值Plus(加)。
rustplayground
你可以点击Rustplaygroud左上角的“Run”按钮来编译和执行你浏览器中的代码。
在一种编程语言的编译器中,词法解析器可能需要许多不同类型的标记。例如:符号,数字,标识符,字符串,操作符等。想知道要从源文件中提取怎样的标记完全取决于编程语言本身。
intmain(){
inta;
intb;
a=b=4;
returna-b;
}
Scannerproduction:
[Keyword(Int),Id(“main”),Symbol(LParen),Symbol(RParen),Symbol(LBrace),Keyword(Int),Id(“a”),Symbol(Semicolon),Keyword(Int),Id(“b”),Symbol(Semicolon),Id(“a”),Operator(Assignment),Id(“b”),
Operator(Assignment),Integer(4),Symbol(Semicolon),Keyword(Return),Id(“a”),Operator(Minus),Id(“b”),Symbol(Semicolon),Symbol(RBrace)]
C语言的样例代码已经进行过词法分析,并且输出了它的标记
2.解析
解析器确实是语法解析的核心。解析器提取由词法分析器产生的标记,并尝试判断它们是否符合特定的模式,然后把这些模式与函数调用,变量调用,数学运算之类的表达式关联起来。解析器逐词地定义编程语言的语法。
inta=3和a:int=3的区别在于解析器的处理上面。解析器决定了语法的外在形式是怎样的。它确保括号和花括号的左右括号是数量平衡的,每个语句结尾都有一个分号,每个函数都有一个名称。当标记不符合预期的模式时,解析器就会知道标记的顺序不正确。
你可以写好几种不同类型的解析器。最常见的解析器之一是从上到下的,递归降解的解析器。递归降解的解析器是用起来最简单也是最容易理解的解析器。我写的所有解析器样例都是基于递归降解的。
解析器解析的语法可以使用一种语法表示出来。像EBNF这样的语法就可以描述一个解析器用于解析简单的数学运算,像是这样12+3:
expr=additive_expr;
additive_expr=term,(‘+’|’-‘),term;
term=number;
简单加法和减法表达式的EBNF语法
请记住语法文件并不是解析器,但是它确实是解析器的一种表达形式。你可以围绕上面的语法创建一个解析器。语法文件可以被人使用并且比起直接阅读和理解解析器的代码要简单许多。
那种语法的解析器应该是expr解析器,因为它直接与所有内容都相关的顶层。有效的输入必须是任意数字,加号或减号,任意数字。expr需要一个additive_expr,这主要出现在加法和减法表达式中。additive_expr首先需要一个term(一个数字),然后是加号或者减号,最后是另一个term。
解析12+3产生的样例AST
解析器在解析时产生的树状结构被称为抽象的语法树,或者称之为AST。ast中包含了所有要进行操作。解析器不会计算这些操作,它只是以正确的顺序来收集其中的标记。
我之前补充了我们的词法分析器代码,以便它与我们的语法想匹配,并且可以产生像图表一样的AST。我用//BEGINPARSER//和//ENDPARSER//的注释标记出了新的解析器代码的开头和结尾。
rustplayground
我们可以再深入一点。假设我们想要支持只有数字没有运算符的输入,或者添加除法和乘法,甚至添加优先级。只要简单地修改一下语法文件,这些都是完全有可能的,任何调整都会直接反映在我们的解析器代码中。
expr=additive_expr;
additive_expr=multiplicative_expr,{(‘+’|’-‘),multiplicative_expr};
multiplicative_expr=term,{(“*”|”/”),term};
term=number;
新的语法
https://play.rust-lang.org/?gist=1587a5dd6109f70cafe68818a8c1a883&version=nightly&mode=debug&edition=2018
针对C语言语法编写的解析器(又叫做词法分析器)和解析器样例。从字符序列的开始“if(net>0.0)total+=net(1.0+tax/100.0);”,扫描器组成了一系列标记,并且对它们进行分类,例如,标识符,保留字,数字,或者运算符。后者的序列由解析器转换成语法树,然后由其他的编译器分阶段进行处理。扫描器和解析器分别处理C语法中的规则和与上下文无关的部分。引自:JochenBurghardt.来源.
3.生成代码
代码生成器接收一个AST,然后生成相应的代码或者汇编代码。代码生成器必须以递归下降的顺序遍历AST中的所有内容-就像是解析器的工作方式一样-之后生成相应的内容,只不过这里生成的不再是语法树,而是代码了。
https://godbolt.org/z/K8416_
如果复制并打开上面的链接,你就可以看到左侧样例代码产生的汇编代码。汇编代码的第三行和第四行展示了编译器在AST中遇到常量的时候是怎样为这些常量生成相应的代码的。
GodboltCompilerExplorer是一个很棒的工具,允许你用高级语言编写代码,并查看它产生的汇编代码。你可以有点晕头转向了,想知道产生的是哪种代码,但不要忘记给你的编程语言编译器添加优化选项来看看它到底有多智能。(对于Rust是-O)
如果你对于编译器是在汇编语言中怎样把一个本地变量保存到内存中感兴趣的话,这篇文章(“代码生成”部分)非常详细地解释了堆栈的相关知识。大多数情况下,当变量不是本地变量的时候,高级编译器会在堆区为变量分配空间,并把它们保存到堆区,而不是栈区。你可以从这个StackOverflow的回答上阅读更多关于变量存储的内容。
因为汇编是一个完全不同的,而且复杂的主题,因此这里我不会过多地讨论它。我只是想强调代码生成器的重要性和它的作用。此外,代码生成器不仅可以产生汇编代码。Haxe编译器有一个可以产生6种以上不同的编程语言的后端:包括C++,Java,和Python。
后端指的是编译器的代码生成器或者表达式解析器;因此前端是词法分析器和解析器。同样也有一个中间端,它通常与优化和IR有关,这部分会在稍后解释。后端通常与前端无关,后端只关心它接收到的AST。这意味着可以为几种不同的前端或者语言重用相同的后端。大名鼎鼎的GNUCompilerCollection就属于这种情况。
我找不到比我的C编译器后端更好的代码生成器示例了;你可以在这里查看。
在生成汇编代码之后,这些汇编代码会被写入到一个新的汇编文件中(.s或.asm)。然后该文件会被传递给汇编器,汇编器是汇编语言的编译器,它会生成相应的二进制代码。之后这些二进制代码会被写入到一个新的目标文件中(.o)。
目标文件是机器码,但是它们并不可以被执行。为了让它们变成可执行文件,目标文件需要被链接到一起。链接器读取通用的机器码,然后使它变为一个可执行文件、共享库或是静态库。更多关于链接器的知识在这里。
链接器是因操作系统而不同的应用程序。随便一个第三方的链接器都应该可以编译你后端产生的目标代码。因此在写编译器的时候不需要创建你自己的链接器。
编译器可能有中间表示,或者简称IR。IR主要是为了在优化或者翻译成另一门语言的时候,无损地表示原来的指令。IR不再是原来的代码;IR是为了寻找代码中潜在的优化而进行的无损简化。循环展开和向量化都是利用IR完成的。更多关于IR相关的优化可以在这个PDF中找到。
总结
当你理解了编译器的时候,你就可以更有效地使用你的编程语言。或许有一天你会对创建你自己的编程语言感兴趣?我希望这能够帮到你。

主题测试文章,只做测试使用。发布者:最新稳定辅助网,转转请注明出处:https://www.744broad.com/13903.html

(0)
上一篇 2023年3月4日 上午1:58
下一篇 2023年3月4日 上午2:04

相关推荐

  • openEuler20.09发布 支持StratoVirt 虚拟机运行

    openEuler20.09发布 支持StratoVirt 虚拟机运行 openEuler官方发布,经过社区贡献者的共同努力,openEuler正式发布了openEuler20.09版本。根据版本计划,openEuler20.09版本属于创新版本而非LTS(LongTermSupport)版本。该版本的LinuxKernel使用4.19.140版本,修复了自…

    RUST资讯 2023年2月19日
    70
  • 葡媒:葡萄牙华人店被扔“硫酸炸弹”店主吁提高警惕

    葡媒:葡萄牙华人店被扔“硫酸炸弹”店主吁提高警惕 据葡萄牙《葡华报》微信公众号消息,当地时间12月1日晚8点左右,葡萄牙里斯本一华人商铺被扔进了“硫酸炸弹”,幸好店主反应快,没有造成人员伤亡。案件发生在晚上8点左右,正是商铺接近打烊的时候,该华人店主站在店门口,看到一名行动鬼祟的青年,戴着压低帽檐的帽子,在商铺门口转悠。突然,该青年向店铺内扔进一个里面装有少…

  • 好程序员前端分享什么是Deno,它与Node.js的区别

    好程序员前端分享什么是Deno,它与Node.js的区别 好程序员前端分享什么是Deno,它与Node.js的区别,Node.js的创建者RyanDahl花了一年半的时间研究deno,这是一个新的JavaScript运行时,可以解决Node的所有固有问题。不要误解我的意思,Nodejs它本身就是一个很棒的服务器端JavaScript运行时,主要是因为它拥有很…

    RUST资讯 2023年2月26日
    30
  • IT成人教育遍地开花,回归本质才是正道

    IT成人教育遍地开花,回归本质才是正道 近年来,随着区块链、智能制造、数据安全、人才培养、元宇宙云计算、大数据、人工智能、区块链、5G等技术的发展升级,我国IT行业扩张势头非常强劲,对于数字化人才的需求也日益提升,这也催生更多的IT职位需求。在IT行业庞大的人才需求下,IT职业教育应运而生。但是随着IT行业高速发展,人才缺失这一问题逐渐暴露出来。根据中国信息…

    RUST资讯 2023年2月18日
    90
  • 品牌大咖《时尚基地》,教您童装尺码怎么挑选!选购童装的注意事

    品牌大咖《时尚基地》,教您童装尺码怎么挑选!选购童装的注意事 童装尺码怎么挑选:1、观察童装的外观童装的外观是选择童装的第一步,选择气质适合宝宝年龄的童装尤为重要。能否展现孩子的童真,感受时尚,是衡量是否值得购买的标尺。此外,妈妈们还需要注意童装是否对称,颜色是否统一。2、闻衣服的味道新衣服有一种浆洗衣服的味道,是厂家为了避免衣服出厂后起皱而留下的,这很正常…

  • 拓展 Swift 应用领域

    拓展 Swift 应用领域 作者:terhechte,原文链接,原文日期:2018-05-03译者:BigLuo;校对:Cee,numbbbbb;定稿:Forelax我想大家应该都会同意Swift是一门优秀的语言,很好的处理了那些简单与复杂的问题。理论上讲,它将会成为重要的编程语言之一。目前,Swift的使用仅限于苹果开发领域(外加少量服务端Swift以及近…

  • 使用rustlings练习Rus

    使用rustlings练习Rust 通过前面的学习,我们对Rust已经有了初步的认识,对Rust的语法也有了一定的了解。接下来我们可以通过https://github.com/rust-lang/rustlings这个项目对之前学习的知识进行练习和巩固。这个项目主要是一些无法通过编译的Rust代码,我们的目的是修改让其通过编译。进入https://gitpo…

  • 中国重工业之困的解决之道

    中国重工业之困的解决之道 Memoriesofthe1990sarehauntingChina’srust-beltcitiesasstrappedstate-ownedemployerslookatcuttingjobs,withWuhanIron&Steelthelatestcompanysaidtobelayingoffthousandsofw…

    RUST资讯 2023年3月11日
    10
  • 盘点:2019年最赚钱的10种编程语言

    盘点:2019年最赚钱的10种编程语言 GitHub逐渐成为一个中心,超过4000万开发人员使用GitHub来分享项目的代码,无论是个人的、行业的还是其他的。在去年,因为与Google和Amazon的竞争,微软以75亿美元的价格收购了GitHub,这很快成为它吸引云开发人员的关键。作为开发人员的最大活跃站点之一,GitHub是追踪开发人员中最流行的最佳场所。…

  • 揭秘铁血战士的社会等级划分,从菜鸟到王者

    揭秘铁血战士的社会等级划分,从菜鸟到王者 #头条群星10月榜#来和大家谈谈铁血战士的8个等级划分[狗头]从出生到铁血战士母星的最高统帅,并没有所谓的十几个等级。这里说的等级是指参与狩猎的铁血战士,其实他们的星球还有很多额外的职能,比如格雷巴克长老部落的神殿守卫者(铁血战士2片尾登场),定位更像士兵不是猎人。从出生的那一刻一旦确定了发展方向,他想要成为一名猎人…

    RUST资讯 2023年2月27日
    40
关注微信