编译器架构 ( Compiler Architecture )

2021-06-29 11:32:46 浏览数 (1)

什么是编译器?

简单讲,编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。一个现代编译器的主要工作流程:

源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器 (Linker) → 可执行程序 (executables)

高级计算机语言便于人编写,阅读交流,维护。机器语言是计算机能直接解读、运行的。编译器将汇编或高级计算机语言源程序(Source program)作为输入,翻译成目标语言(Target language)机器代码的等价程序。源代码一般为高级语言 (High-level language), 如Pascal、C、C 、Java、汉语编程等或汇编语言,而目标则是机器语言的目标代码(Object code),有时也称作机器代码(Machine code)。

对于C#、VB等高级语言而言,此时编译器完成的功能是把源码(SourceCode)编译成通用中间语言(MSIL/CIL)的字节码(ByteCode)。最后运行的时候通过通用语言运行库的转换,编程最终可以被CPU直接计算的机器码(NativeCode)。

我们平时所说的程序,是指双击后就可以直接运行的程序,这样的程序被称为可执行程序(Executable Program)。在 Windows 下,可执行程序的后缀有 .exe 和 .com(其中 .exe 比较常见);在类 UNIX 系统(Linux、Mac OS 等)下,可执行程序没有特定的后缀,系统根据文件的头部信息来判断是否是可执行程序。

可执行程序的内部是一系列计算机指令和数据的集合,它们都是二进制形式的,CPU 可以直接识别,毫无障碍;但是对于程序员,它们非常晦涩,难以记忆和使用。

例如,在屏幕上输出“VIP会员”,C语言的写法为:

puts("VIP会员");

二进制的写法为:

你感受一下,直接使用二进制是不是想撞墙,是不是受到一吨重的伤害?

在计算机发展的初期,程序员就是使用这样的二进制指令来编写程序的,那个拓荒的年代还没有编程语言。

直接使用二进制指令编程对程序员来说简直是噩梦,尤其是当程序比较大的时候,不但编写麻烦,需要频繁查询指令手册,而且除错会异常苦恼,要直接面对一堆二进制数据,让人眼花缭乱。另外,用二进制指令编程步骤繁琐,要考虑各种边界情况和底层问题,开发效率十分低下。

这就倒逼程序员开发出了编程语言,提高自己的生产力,例如汇编、C语言、C 、Java、Python、Go语言等,都是在逐步提高开发效率。至此,编程终于不再是只有极客能做的事情了,不了解计算机的读者经过一定的训练也可以编写出有模有样的程序。

C语言代码由固定的词汇按照固定的格式组织起来,简单直观,程序员容易识别和理解,但是对于CPU,C语言代码就是天书,根本不认识,CPU只认识几百个二进制形式的指令。这就需要一个工具,将C语言代码转换成CPU能够识别的二进制指令,也就是将代码加工成 .exe 程序;这个工具是一个特殊的软件,叫做编译器(Compiler)。

编译器能够识别代码中的词汇、句子以及各种特定的格式,并将他们转换成计算机能够识别的二进制形式,这个过程称为编译(Compile)。

编译也可以理解为“翻译”,类似于将中文翻译成英文、将英文翻译成象形文字,它是一个复杂的过程,大致包括词法分析、语法分析、语义分析、性能优化、生成可执行文件五个步骤,期间涉及到复杂的算法和硬件架构。对于学计算机或者软件的大学生,“编译原理”是一门专业课程,有兴趣的读者请自行阅读《编译原理》一书,这里我们不再展开讲解。

注意:不了解编译原理并不影响我们学习C语言,我也不建议初学者去钻研编译原理,贪多嚼不烂,不要把自己绕进去。

C语言的编译器有很多种,不同的平台下有不同的编译器,例如:

  • Windows 下常用的是微软编译器(cl.exr),它被集成在 Visual Studio 或 Visual C 中,一般不单独使用;
  • Linux 下常用的是 GUN 组织开发的 GCC,很多 Linux 发行版都自带 GCC;
  • Mac 下常用的是 LLVM/Clang,它被集成在 Xcode 中(Xcode 以前集成的是 GCC,后来由于 GCC 的不配合才改为 LLVM/Clang,LLVM/Clang 的性能比 GCC 更加强大)。

你的代码语法正确与否,编译器说了才算,我们学习C语言,从某种意义上说就是学习如何使用编译器,让编译器生成可执行程序(例如 Windows 下的 .exe 程序)。

编译器可以 100% 保证你的代码从语法上讲是正确的,因为哪怕有一点小小的错误,编译也不能通过,编译器会告诉你哪里错了,便于你的更改。

编译过程

根据编译方式,编译器大致可以分为两个阶段。

Analysis Phase

作为编译器的前端,编译器的分析阶段读取源程序,将其划分为核心部分,然后检查词法、语法和语法错误分析阶段生成源程序和符号表的中间表示,应将其作为输入馈送到合成阶段。

Synthesis Phase

作为编译器的后端,综合阶段通过中间源代码表示和符号表生成目标程序。

编译器可以有许多阶段和过程。

Pass:Pass是指编译器在整个程序中的遍历。

Phase:编译器的一个阶段是一个可区分的阶段,它接受前一阶段的输入,处理并产生可作为下一阶段输入的输出。Pas可以有多个相位。

编译过程是一系列不同的阶段。每个阶段从其前一阶段获取输入,有自己的源程序表示,并将其输出馈送到编译器的下一阶段。让我们了解编译器的各个阶段。

词语分析 Lexical Analysis

扫描器的第一阶段是作为文本扫描器工作的。这个阶段将源代码作为字符流进行扫描,并将其转换为有意义的词素。词汇分析器以标记的形式表示这些词汇:

<token-name, attribute-value>

Syntax Analysis句法分析

下一个阶段称为语法分析或解析。它将词法分析生成的标记作为输入,并生成一个解析树(或语法树)。在此阶段,根据源代码语法检查标记排列,即解析器检查标记生成的表达式在语法上是否正确。

Semantic Analysis

语义分析检查构造的解析树是否遵循语言规则。例如,值的赋值是在兼容的数据类型之间进行的,并将字符串添加到整数中。此外,语义分析器跟踪标识符、它们的类型和表达式;标识符是否在使用前声明等。语义分析器生成带注释的语法树作为输出。

Intermediate Code Generation

中间代码生成

在语义分析之后,编译器为目标机器生成源代码的中间代码。它代表一个抽象机器的程序。它介于高级语言和机器语言之间。此中间代码的生成方式应使其更易于转换为目标机器代码。

代码优化Code Optimization

下一阶段将对中间代码进行代码优化。优化可以假设为删除不必要的代码行,并安排语句序列,以便在不浪费资源(CPU、内存)的情况下加快程序执行。

代码生成Code Generation

在此阶段,代码生成器获取中间代码的优化表示,并将其映射到目标机器语言。代码生成器将中间代码转换为(通常)可重新定位的机器代码序列。机器代码的指令序列像中间代码那样执行任务。

符号表Symbol Table

它是一个贯穿编译器所有阶段的数据结构。所有标识符的名称及其类型都存储在这里。符号表使编译器更容易快速搜索和检索标识符记录。符号表也用于范围管理。

词法分析是编译器的第一个阶段。它从以句子形式编写的语言预处理器中获取经过修改的源代码。词法分析器通过删除源代码中的任何空格或注释,将这些语法分解为一系列标记。

如果词法分析器发现标记无效,它将生成一个错误。词法分析器与语法分析器密切合作。它从源代码中读取字符流,检查合法令牌,并在需要时将数据传递给语法分析器。

Tokens令牌

词素被称为符号中的字符序列(字母数字)。对于每个要标识为有效令牌的词素,都有一些预定义的规则。这些规则是由语法规则通过模式定义的。模式解释什么可以是标记,这些模式是通过正则表达式定义的。

在编程语言中,关键字、常量、标识符、字符串、数字、运算符和标点符号可以看作是标记。

例如,在C语言中,变量声明行

int value = 100;

包含标记:

int (keyword), value (identifier), = (operator), 100 (constant) and ; (symbol).

Specifications of Tokens

让我们了解一下语言理论是如何承担下列条件的:

字母表Alphabets

任何有限的符号集合{0,1}是一组二进制字母,{0,1,2,3,4,5,6,7,8,9,a,B,C,D,E,F}是一十六进制字母,{a-z,a-z}是一组英语字母。

字符串

任何有Strings

限的字母序列都称为字符串。字符串长度是字母表出现的总数,例如,字符串tutorialspoint的长度为14,用| tutorialspoint |=14表示。没有字母表的字符串,即长度为零的字符串称为空字符串,用ε(epsilon)表示。

特殊符号Special Symbols

典型的高级语言包含以下内容符号:-

语言 language

一种语言被认为是一组有限的字符串覆盖在一组有限的字母表上。计算机语言被认为是有限集,可以对其进行数学上的集合运算。有限语言可以用正则表达式来描述。

Longest Match Rule最长匹配规则

当词法分析器读取源代码时,它逐字扫描代码;当遇到空白、运算符符号或特殊符号时,它决定一个单词完成。

例如:

int value;

当扫描两个词素到“int”时,词法分析器无法确定它是关键字int还是标识符int值的首字母。

最长匹配规则规定,扫描的词素应根据所有可用令牌中最长匹配来确定。

词法分析器还遵循规则优先级,其中语言的保留字(例如关键字)比用户输入的优先级高。也就是说,如果词法分析器找到与任何现有保留字匹配的词素,它应该生成一个错误。

词法分析器只需要扫描和识别属于当前语言的有限的有效字符串/令牌/词素集。它搜索由语言规则定义的模式。

正则表达式能够通过定义符号的有限字符串的模式来表示有限语言。由正则表达式定义的语法称为正则语法。由正则语法定义的语言称为正则语言。

正则表达式是指定模式的重要符号。每个模式都匹配一组字符串,因此正则表达式用作一组字符串的名称。编程语言标记可以用常规语言来描述。正则表达式的规范是递归定义的一个例子。常规语言易于理解并具有高效的实现。

正则表达式遵循许多代数定律,这些定律可用于将正则表达式处理为等价形式。

Operations

The various operations on languages are:

  • Union of two languages L and M is written as

L U M = {s | s is in L or s is in M}

  • Concatenation of two languages L and M is written as

LM = {st | s is in L and t is in M}

  • The Kleene Closure of a language L is written as

L* = Zero or more occurrence of language L.

Notations

If r and s are regular expressions denoting the languages L(r) and L(s), then

  • Union : (r)|(s) is a regular expression denoting L(r) U L(s)
  • Concatenation : (r)(s) is a regular expression denoting L(r)L(s)
  • Kleene closure : (r)* is a regular expression denoting (L(r))*
  • (r) is a regular expression denoting L(r)

Precedence and Associativity优先性和关联性

  • *, concatenation (.), and | (pipe sign) are left associative
    • has the highest precedence
  • Concatenation (.) has the second highest precedence.
  • | (pipe sign) has the lowest precedence of all.

在正则表达式中表示语言的有效标记

If x is a regular expression, then:

  • x* means zero or more occurrence of x.

i.e., it can generate { e, x, xx, xxx, xxxx, … }

  • x means one or more occurrence of x.

i.e., it can generate { x, xx, xxx, xxxx … } or x.x*

  • x? means at most one occurrence of x

i.e., it can generate either {x} or {e}.

[a-z] is all lower-case alphabets of English language.

[A-Z] is all upper-case alphabets of English language.

[0-9] is all natural digits used in mathematics.

用正则表达式表示符号的出现

letter = [a – z] or [A – Z]

digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 or [0-9]

sign = [ | - ]

使用正则表达式表示语言标记

Decimal = (sign)?(digit)

Identifier = (letter)(letter | digit)*

词汇分析器剩下的唯一问题是如何验证用于指定语言关键字模式的正则表达式的有效性。一个公认的解决方案是使用有限自动机进行验证。

参考文档

http://www.tutorialspoint.com/compiler_design/compiler_design_architecture.htm

https://www.cnblogs.com/wujianming-110117/p/13181658.html

0 人点赞