简单说来,一个 编译器 compiler 不过是一个可以翻译其他程序的程序。传统的编译器可以把源代码翻译成你的计算机能够理解的可执行机器代码。(一些编译器将源代码翻译成别的程序语言,这样的编译器称为源到源翻译器或 转化器 transpilers 。)LLVM 是一个广泛使用的编译器项目,包含许多模块化的编译工具。
传统的编译器设计包含三个部分:
- 前端 Frontend 将源代码翻译为 中间表示 intermediate representation (IR)* 。clang 是 LLVM 中用于 C 家族语言的前端工具。
- 优化器 Optimizer 分析 IR 然后将其转化为更高效的形式。opt 是 LLVM 的优化工具。
- 后端 Backend 通过将 IR 映射到目标硬件指令集从而生成机器代码。llc 是 LLVM 的后端工具。
注:LLVM 的 IR 是一种和汇编类似的低级语言。然而,它抽离了特定硬件信息。
Hello, Compiler
下面是一个打印 “Hello, Compiler!” 到标准输出的简单 C 程序。C 语法是人类可读的,但是计算机却不能理解,不知道该程序要干什么。我将通过三个编译阶段使该程序变成机器可执行的程序。
// compile_me.c
// Wave to the compiler. The world can wait.
#include
int main() {
printf("Hello, Compiler!\n");
return 0;
}
前端
正如我在上面所提到的,clang
是 LLVM 中用于 C 家族语言的前端工具。Clang 包含 C 预处理器 C preprocessor 、 词法分析器 lexer 、 语法解析器 parser 、 语义分析器 semantic analyzer 和 IR 生成器 IR generator 。
C 预处理器在将源程序翻译成 IR 前修改源程序。预处理器处理外部包含文件,比如上面的 #include
。 它将会把这一行替换为 stdio.h
C 标准库文件的完整内容,其中包含 printf
函数的声明。
通过运行下面的命令来查看预处理步骤的输出:
clang -E compile_me.c -o preprocessed.i
词法分析器(或 扫描器 scanner 或 分词器 tokenizer )将一串字符转化为一串单词。每一个单词或 记号 token ,被归并到五种语法类别之一:标点符号、关键字、标识符、文字或注释。
compile_me.c 的分词过程:
语法分析器确定源程序中的单词流是否组成了合法的句子。在分析记号流的语法后,它会输出一个 抽象语法树 abstract syntax tree (AST)。Clang 的 AST 中的节点表示声明、语句和类型。
compile_me.c 的语法树:
语义分析器会遍历抽象语法树,从而确定代码语句是否有正确意义。这个阶段会检查类型错误。如果 compile_me.c
的 main 函数返回 "zero"
而不是 0
, 那么语义分析器将会抛出一个错误,因为 "zero"
不是 int
类型。
IR 生成器将抽象语法树翻译为 IR。
对 compile_me.c 运行 clang 来生成 LLVM IR:
clang -S -emit-llvm -o llvm_ir.ll compile_me.c
在 llvm_ir.ll
中的 main 函数:
; llvm_ir.ll
@.str = private unnamed_addr constant [18 x i8] c"Hello, Compiler!\0A\00", align 1
define i32 @main() {
%1 = alloca i32, align 4 ;