Compiler
编译器是将源编程语言中的代码翻译成目标编程语言的程序。 通常,目标编程语言的级别低于源编程语言-编译器的目标语言,如C、字节码、汇编或原始机器代码。
语言以另一种高级语言(如C)为目标的一个普遍原因是编译器可以完全专注于翻译语言,同时获得生产质量C编译器的所有世界级优化。 对于动态语言来说,这个过程要困难得多。
大多数编译器输出某种形式的程序集,并将其传递给汇编程序。 更罕见的是,一些编译器可能直接生成字节码或机器码,而无需第三方汇编程序。 (JIT-Just In Time即时编译器是一类特殊的嵌入式编译器,可直接生成机器代码。)
其它工具
下面是一些其它常用工具
汇编程序
编译器从最终将要执行的底层机器指令中提取高级代码。
汇编程序获取汇编代码并输出机器代码。 汇编代码是机器指令的一对一表示。 通常,汇编器所做的唯一转换是填写标签(或保留“修复”位置的列表,当多个目标链接在一起时,这些位置会被填写)。更高级的汇编程序可能有宏或某种预处理器,但这些宏通常生成机器指令,因此没有像高级语言那样从底层指令中抽象出来。
汇编程序可以生成可执行的二进制文件(非常适合运行)或目标文件。 目标文件不同于可执行二进制文件,因为它通常包括导出符号和(或)修复地址的列表。 目标文件可以是指可以在运行时动态加载的文件(动态库),也可以是要与其它目标文件合并以共享其标签并生成最终二进制文件的文件。
连接程序(Linkers)
链接器获取多个目标文件,并将它们组合成单个目标文件或最终的二进制文件。
当你希望将代码与库链接时,或者当你的程序有多个源文件,每个源文件都单独编译,并且你希望生成一个二进制文件时,通常使用链接器。
每个目标文件都包含一个导出符号及其所在位置的列表,以及一个“修复”位置列表及其要查找的符号。然后,链接器尝试用其它文件中的符号填充修复位置。
阶段
编译器有多个阶段。 每个阶段采用前一阶段创建的数据结构,并转换为新的数据结构。 支持多种输入语言或多种输出语言的高级编译器通常分为前端、中间端和后端,中间端和后端可以互换。 只支持一种输入语言和一种目标语言的更简单的编译器通常不需要将这些端点分开。
前端
编译器的前端是指编译器中读取输入语言的部分。 在像GCC和LLVM这样支持多种语言的大型编译器中,前端和后端可以混合和匹配以支持不同的语言,同时共享同一个中间端。
词汇分析(Lexical Analysis)
词法分析(Lexical analysis)读取源文件并将其拆分为令牌。 标记通常有一个类型,可以是诸如FOR、INT、VOID等符号。 标记也可以是附加了额外数据(例如文本的值或标识符的名称)的文本,例如字符串、标识符、浮点值。 lexer通常处理跳过注释。
lexer可以手工编写,也可以使用“lexer生成器”生成,这是一种接受词法语法并生成lexer代码的工具。
解析(Parsing)
解析阶段从lexer读取标记,并在内存中构建某种形式的代码表示。 表示通常是一个抽象语法树。
解析器可以手工编写(请研究 递归下降语法分析器-Recursive descent parser)或使用“语法分析器生成器”(parser generator)生成,这是一种接受语言语法并生成语法分析器代码的工具。 正是在这一阶段,局部变量和对象名在遇到时被注册。
一些解析器生成器一次性生成解析器和词法分析器,称为“编译编译器(compiler compilers)”。
抽象语法树(Abstract syntax tree)
抽象语法树(AST-abstract syntax tree)是以树格式表示的程序,通常是以树格式表示的源代码。 抽象语法树通常与输入语言紧密相连,因此是前端的一部分,但有时它可能是中间端的一部分。 然后将抽象语法树转换为独立于语言的中间表示。
语义图(Semantical graph)
编程语言使用标识符引用在其它地方声明的实体。 在执行进一步的分析之前,应该解析标识符-连接到它们所表示的声明。 这将AST转换为图形。 图形是使用称为符号表或上下文的辅助数据结构构建的。
在某些编译器中,AST和语义图使用相同的数据结构。 在这种情况下,解析器可以将声明指针设置为NULL,或者标识符解析可以集成到解析过程中。
语义分析(Semantical analysis)
此阶段计算表达式和声明的属性,并执行各种有效性检查:确定表达式类型、解析函数重载、检查重写函数的协方差、检查常量变量是否未发生变化、检查所有枚举元素是否在switch语句中处理等。
中间端
中间端充当前端和后端之间的粘合剂。 这是一个很好的例子[1]这与语言无关,大多数优化都在这里完成。 并非所有编译器都有中间端,有时抽象语法树直接生成机器代码并跳过这些步骤。
字节码(Bytecode)
有些编译器有一种内部字节码语言(可能不同于字节码目标),用于前端和后端之间的通信。 这些字节码可以是基于寄存器的,也可以是基于堆栈的(基于堆栈的字节码是非常容易从抽象语法树生成)。
单一静态赋值(Single static assignment)
抽象语法树或字节码可以翻译成[2],或SSA,形式。 SSA表单是代码的中间表示形式,其中所有变量只分配给一次。 SSA形式非常适合优化,例如常数折叠(constant folding)和死代码消除。 它也是一种简单的形式,可以很好地映射到后端的寄存器。
后端
编译器的后端将代码输出到目标语言中。后端通常执行特定于目标的优化。
解释器
解释器不输出代码,所以从技术上讲,它们不是编译器,但它们的前端和中间端的实现方式与编译器类似。 它们可以执行即时表示、抽象语法树,或者在语言解析时执行—完全绕过中间端。 JIT(just-in-time 即时)编译器在内存中生成机器代码并立即执行。
字节码
编译器可能以字节码为目标,如WebAssembly。 字节码被设计为在虚拟机内部执行,并且通常比汇编更容易定位(因为不必分配有限数量的寄存器,而且它的级别更高)。
汇编
编译器可能以汇编语言为目标。 这类似于定位字节码,只是编译器必须分配寄存器,并且经常处理寄存器和堆栈分配,以及[3]。
通过输出汇编语言,编译器让汇编器实现目标和可执行格式,计算代码位置、标签,并用机器代码对指令进行编码。
机器码
编译器可以生成机器代码(如JIT编译器)。当编译器拥有所需的所有输入代码并且不需要将输出文件与其它输出文件合并时,通常会执行此操作。 输出机器代码的过程与输出汇编代码非常相似,但编译器现在必须解析标签位置,编码机器指令,并以可执行格式处理输出。