0%

编译器(翻译维基百科)

编译器

编译器,是一个计算机程序(或一些程序),它会将用某种编程语言写成的源代码(原始语言),转换成另一种计算机语言(目标语言,通常具有某种二进制形式的目标代码)。最常见的转换源代码的目的是为了生成可执行程序。

“编译器”主要指用于将高级计算机语言翻译成较低级机器语言(汇编语言、机器码等)的程序,也就是可执行文件。如果某个编译器所编译出的程序是运行在不同于编译器本身所运行的操作系统或CPU上,那么这个编译器被称作交叉编译器。将低阶语言翻译成高阶语言的程序被称作反编译器。将一种高阶语言翻译为另一种高阶语言的程序被称作语言翻译器、源代码到源代码的翻译器或语言转换器。语言重写器则只改变源代码表达式形式而不改变编写源代码语言的程序。

编译器的编译过程一般会经历以下步骤中的一部分或全部:词法分析,预处理,语法分析,语义分析(语法制导翻译),代码生成以及代码优化。

由于由编译器引起的错误往往难以察觉和弥补,因此编译器的实现者往往会做出巨大的努力来保证编译器的正确性

术语“编译器的编译器”有时指解析器生成器,用于生成词法分析器和语法分析器的工具。

教学性的编译器

编译器的构造与优化是计算机专业的大学课程。这种课程通常需要实现实现一种教学用程序语言的编译器。一个著名的例子是20世纪70年代,Niklaus Wirth用于讲解编译器的构造时使用的PL/0编译器。PL/0编译器虽然简单,但是包涵了当时该领域最流行的一些概念:

  1. 通过逐步求精方式开发程序(这也是Wirth在1971年所发表的一篇论文的题目)
  2. 使用递归下降语法解析器
  3. 使用扩展巴科斯范式来表示语法
  4. 用于生成可移植的伪代码的代码生成器
  5. 使用T型图来描述自举型问题

编译

编译器让人们能够编写机器无关的代码。在20世纪50年代,第一个高级语言FORTRAN(公式翻译器的缩写)得到发展之前,依赖于机器的汇编语言被广泛使用。虽然汇编语言比机器代码在相同架构可以产生更多的可重用和可重新定位的程序,但是如果程序是要执行在不同的计算机体系结构上,就必须被改写或重写。

由于采用了先进的高阶编程语言,如FORTRAN,COBOL,C和BASIC,程序员可以编写独立于机器的源程序。编译器则将高阶的源程序为具体的硬件翻译成由特定机器语言构成的目标程序。目标程序被生成后,即可被用户执行。

编译器的结构

编译器桥接底层硬件和由高级语言构成的源程序。编译器应该做到:

  1. 辨别程序的语法是否正确
  2. 产生正确和高效的目标代码
  3. 执行时间组织
  4. 遵守汇编器和(或)连接器约定的格式化输出

编译器由三个主要部分组成:前端,中端和后端。

前端检查程序在语法和语义上的正确性。在这里,程序是够合法会被确认。如果有错误的话,编译器用某种有意义的方法报错。类型检查也会进行,类型信息被收集。前端将源代码转换为中间表示(IR)并传递给中端。

中端会对中间表示进行优化。典型的优化方法包括去除无用或不可达的代码,常量传播,重新安排不经常执行的代码(例如循环外的代码),或基于上下文进行特殊计算。中端生成另一个IR传递给后端。大部分优化都集中在这部分。

后端是负责将中端传递来的中间表示翻译成汇编代码。每条中间表示中的指令将会生成特定的目标指令。寄存器分配将程序变量尽可能分配给处理器的寄存器。后端通过有效利用并行单元,填延迟间隙等方法有效利用硬件资源。虽然大部分优化算法属于NP,但启发式技术的良好发展有效解决了一些问题。

编译器输出

一个编译器的分类方法是按其生成的代码的执行平台。这就是所谓的目标平台。

本地的或托管的编译器是指编译器输出的目标程序与编译器本身运行在相同类型的计算机和操作系统中。而一个交叉编译器的输出可运行在不同的平台上。交叉编译器通常使用在不支持开发环境的嵌入式系统的软件开发中。

由于为虚拟机(VM)生成的代码的编译器的所生成的目标程序既可以在与编译器的相同平台上运行,又可以在不同的平台运行,因而,这样的编译器通常既不被认为是本机也不被认为是交叉编译器。

作为编译器的目标语言的较低阶语言可能本身一种高层次的编程语言。C语言由于往往被视为某种形式的可移植汇编,也可以作为编译器的目标语言。例如:Cfront,C++的原始编译器,使用C语言作为目标语言。这样的编译器所创建的C语言代码通常是不适合在人类阅读和维护的。它不具有良好的缩进风格。一些C语言的特性使得C语言成为一种很好的目标语言。例如:C语言的预处理(#开头的指令)可以帮助生成支持调试的代码。

编译型语言与解释型语言

一种高级编程语言留给人的第一印象通常是它是编译型的语言还是解释型语言。然而,实际上有语言必须是编译型或解释型的,担任我们也可能设计出运行时重新解释的语言。这种分类方法通常反映一种语言最流行或最普遍的实现方式。例如,Basic通常被称为一种解释语言,而C则是编译型的,尽管存在的Basic编译器和C解释器。

解释型语言不会完全取代编译型语言。它仅仅是将编译的那一部分在用户面前隐藏起来。即使一个解释器可以解释其本身,在底层(如机器语言)还是需要可以直接执行的程序。现代的潮流是即时编译和字节码技术,这些技术模糊了编译器和解释器的界限。

一些语言规范指明它的实现必须是某些编译型或解释型。Common Lisp是一个很好的例子,Common Lisp的语法定义,使得它很难不被设计为非解释型的。其他一些语言,由于具有某些特性,是很容易被实现为解释型,而很难实现为编译型。例如,APL,SNOBOL4以及其他许多脚本语言,由于允许使用常规的字符串在运行时构造源代码,然后通过一个特殊的eval函数把字符串当做代码执行,如果要编译实现这些特性,程序通常必须附带一个包括编译器的运行时库。

硬件编译

一些编译器可能是输出针对计算机硬件在一个非常低阶的程序,例如可编程门阵列(FPGA)或结构化的应用专用集成电路(ASIC)。这样的编译器被称作硬件编译器或综合工具,因为它们有效地控制了硬件的配置与运行。这种编译的输出并不是顺序执行的指令序列 ——只是代表晶体管的互连或查找表。例如,XST是Xilinx用于配置FPGA的综合工具。Altera公司,Synplicity公司,Synopsys公司和其他供应商也提供类似的工具。

编译器构成

在早期,编译器的设计受制于处理问题的复杂性,设计者的经验,和可利用的资源。

针对个人及相对简单的语言所编写的编译器可能是一个单一而庞大的软件。当源代码变得庞大而复杂使,人们需要高质量的输出,编译过程可能会被分割成若干相对独立的阶段。这意味着软件开发可以分成很多小块,并分配给不同的人。这也使得改进某一阶段或插入新的阶段(例如,额外的优化)变得更加容易。

卡内基 - 梅隆大学的高质量编译器生成器项目(PQCC)阶段这一概念。该项目引入前端,中端和后端三个概念。

几乎所有的编译器都有两个以上的阶段。然而,这些阶段一般会被归入前端或后端。前段和后端的交汇点引起过公开讨论。目前,前端已被普遍包括是语法和语义处理,及翻译源代码到一个较低阶表示的翻译过程。

中端的作用通常被设计为对的源代码或机器码以外的某种中间表示进行优化。这种中间表示的独立性,保证优化可以对不同的目标代码和处理器起作用。

后端需要中端的输出。它可能会对特定的计算机进行更多的分析,转换和优化。然后,它为特定的处理器和操作系统生成代码。

这种前段、中端和后端的设计方法使得编译器可以为不同的语言采用不同的前端,为不同的CPU采用不同的后端。采用这种方法例子是GNU编译器集,LLVM和阿姆斯特丹编译器套件,他们都有多个前端,采用共同的中端分析和拥有多个后端。

一遍扫描和多遍扫描的编译器

根据编译器扫描遍数分类的背景是编译受计算机的硬件资源限制。编译需要进行大量的工作,早期的计算机没有足够的内存来包含一整个程序,所有这些工作。所以编译器不得不被划分成一个个更小的程序,每个程序对源代码(或中间表示)扫描一遍并进行一些必要的分析和翻译。

而一遍扫描的编译器有一个非常大的好处,那就是它简化了编写编译器工作和并且比多遍扫描的编译器效率更高。因此,部分原因受是早期系统资源的限制,许多早期的语言专门设计为能够通过一遍扫描编译(例如,pascal)。

在某些情况下,一种语言的某些特性可能需要多遍扫码源代码。例如,有些源代码在第20行上出现的声明会影响第10行的翻译结果。在这种情况下,第一遍扫描需要收集所用信息并计算它们影响了哪些语句,而实际的翻译发生在随后的扫描过程中。

一遍扫描的缺点是,它是很难执行许多生成高质量的代码所需要的复杂优化。一个执行了很多优化的编译器通常会扫描很多遍。例如,不同编译阶段的优化可以分析一个表达式很多次。

将编译器分割为小程序的技术通常被某些研究人的所使用,因为他们对生产的的编译器的正确性感兴趣。通过这种方法,只需验证一个个小程序的正确性,比起验证一个更大的,单一的程序的正确性,往往只需更少的努力。

虽然典型的多遍扫描编译器会最终输出机器码,但还有其他几种类型:

  1. 一个“源到源的编译器”是一种输入高级语言的并输出一个高级语言的编译器。例如,自动并行化编译器通常将高级语言程序作为输入,然后变换代码给它加上并行化的标签(如OpenMP)或改变它的语言构造(如Fortran语言的DOALL语法)。
  2. 阶段编译器将源代码编译成某种虚拟机器的抽象代码,例如某些Prolog的实现:
    1. 这种Prolog的虚拟机被称为沃伦抽象机(WAM)。
    2. Java,Python的字节码。
  3. 实时编译器,这种编译器被Smalltalk和Java使用,也被微软的.NET使用——它使用“通用中间语言”(CIL)。这种程序在编译阶段被编译成字节码,在运行时才被实时编译为本地代码。

前端

编译器对源代码的前端分析是用来构建程序的内部表示,称为中间表示或IR。它同时管理符号表,这是一种数据结构,把每个符号映射到源代码中的相关信息,如位置,类型和作用域。 它包括以下这些阶段:

  1. 线性重构:对于关键字和标示符可以相同或允许在标识符内插入空格的语言在解析前加入一个线性重构阶段,将输入字符序列转换为解析器可接受的规范形式。而在20世纪60年代开始使用的自顶向下的递归下降的使用符号表解析器并不需要一个单独的线性重构阶段。阿特拉斯自动编码和imp(及ALGOL和Coral 66的一些实现)的编译器都拥有线性重建阶段。
  2. 词法分析:词法分析将源代码文本分块,每一块称为一个记号。每个记号是计算机语言的一个院子单元,比如说一个关键字、标识符或符号名称。记号语法是一个典型的正则语言,因此,它可以被正则表达式构成的有限状态自动机识别。此阶段也被称为词法分析或扫描阶段,做词法分析的软件被称为词法分析程序或扫描器。词法分析可能不是一个独立的阶段——在某些情况下,解析是逐字符进行的,而不是记号级别的,无需词法分析。
  3. 预处理:有些语言,例如C语言,由于支持宏替换和条件编译,需要预处理。预处理阶段通常发生在语法及语义分析之前。对于C语言,预处理器变换词法记号,而不是语法形式。然而,一些语言,如Scheme支持基于语法形式的宏替换。
  4. 语法分析:语法分析涉及解析记号序列并识别程序的语法结构。这个阶段编译器会通常根据形式语法所定义语法规则构建一个树状的数据结构称作解析树,用于取代线性的记号序列。在之后的阶段中,解析树经常被分析,被增强和被变换。
  5. 语义分析:语义分析阶段编译器会将语义信息添加到解析树中,并建立符号表。此阶段会执行语义检查,如类型检查(检查类型错误)、对象绑定(将变量和函数引用与其定义关联)、明确赋值分析(在使用前初始化所有局部变量),同时编译器会拒绝不正确的程序或发出警告。语义分析通常需要一个完整的解析树,这意味着,这一阶段在逻辑上应紧跟语法分析阶段,并在逻辑上位于代码生成阶段之前,但它在实现中往往是可以和其他的一个或多个阶段合并。

后端

后端因为与代码生成器在生成汇编代码上发生了功能上的重叠,有时会被混淆。一些文献使用“中端”这一术语,代表机器无关的分析和优化。而机器相关的代码生成等则属于后端。

后端主要包括一下几个阶段:

  1. 分析:分析阶段从输入的中间表示来信息收集。典型的分析包括用于建立使用-定义链的数据流分析,相关性分析,别名分析,指针分析,转义分析等。准确的分析是任何编译器优化的基础。过程调用图和控制流程图通常也在分析阶段创建。
  2. 优化:在优化阶段,中间表示被转换为功能相同,但更快(或更短小)的形式。主流的优化包括内联扩展,死代码消除,常量传播,循环变换,寄存器分配,以及更高级的自动并行化。
  3. 代码生成:代码生成阶段,被转化的中间表示将被翻译成目标语言。这种目标语言通常是本地系统可执行的语言。这通常涉及到计算资源和存储资源的分配,比如决定哪些变量应该被放入哪些寄存器和存储器,选择合适的机器指令以及与他们相关的寻址模式(塞西-乌尔曼算法)。为了调试方便,可能需要生成调试数据。

编译器分析是优化的是,他们紧密地结合在一起工作。例如,依赖分析是循环改造的必要前提。

此外,编译器分析和优化可以发生在变化相当大的范围内,小到基本块范围,大到过程/函数范围,甚至有的编译器可以在在整个程序范围内优化。显然,编译器可能在更广的范围内做更好的优化。但这是有代价的:大范围的分析和优化会消耗大量的编译时间和内存空间,其中过程间分析和优化特别能反应这个特点。

在现代,惠普,IBM,SGI,英特尔,微软和Sun公司的商业编译器都支持过程间分析和优化。很长一段时间里,开源的GCC由于缺乏强大的间优化的遭受了批评,但它在这方面正在改善。另一个开源编译器Open64由于支持全面的分析和优化,许多组织将其用于研究或商业用途。

由于编译器分析和优化需要额外的时间和空间,一些编译器默在认情况下会跳过它们。用户必须使用编译选项来明确地告诉编译器应该启用优化。

编译器正确性

编译器的正确性是软件工程的一个分支,该分支试图验证一个编译器的行为符合被编译的语言规范。这些技术包括对现有的编译器使用形式化方法和严格的测试(编译器验证)。

相关技术

汇编语言是一种低阶语言。编译汇编语言的程序被称作汇编器,逆编译(机器码)的程序被称为反汇编器。

将低阶语言翻译为高阶语言的程序叫做反编译器。

将一种高阶语言翻译为另一种高阶语言的程序被称作语言翻译器,源-源翻译器,语言转换器或语言重写器。最后一个术语通常指用于重写同种语言的程序。

如果某个编译器所编译出的程序是运行在不同于编译器本身所运行的操作系统或CPU上,那么这个编译器被称作交叉编译器。交叉编译器通常用于嵌入式开发。

国际会议与组织

每年,欧洲软件理论与实践联合会(ETAPS)赞助国际编译器结构会议(CC),该会议接受学术界和工业界的论文。