我们都知道iOS的编译器是LLVM
,本篇我们就探索llvm
的编译流程。
解释型语言和编译型语言区别
解释型语言
解释型语言的特征是:它的执行机制是使用一个解释器
来执行,解释器
对程序一句一句翻译
成机器语言
来一句一句执行。例如:shell
、python
等。
比如我们执行一段pythion
代码:
1 | ///hello.py |
我们执行上面的代码只需要:
python hello.py
编译型语言
编译型语言的特征是:它的执行机制使用编译器
来编译成机器语言
,然后就可以直接执行编译后的可执行文件
。例如:c
、java
等。
比如我们执行一段c
代码:
1 | ///hello.c |
我们首先要对上面的文件进行编译:
1 | clang hello.c |
执行clang
指令后,会生成一个.out
的可执行文件,然后执行可执行文件就可以打印出hello c
。
传统编译器的设计
编译器是由三部分构成的编译器前端
、优化器
、编译器后端
。
编译器前端
编译器前端
主要做词法分析,语法分析,语义分析,检查源代码是否存在错误,构建抽象语法树
(Abstract Syntax Tree,AST),最后前端会生成中间代码
(intermediate representation,IR)。
优化器
优化器
主要负责中间代码的优化。改善代码运行时间,例如消除冗余的计算。
编译器后端
编译器后端
主要是将中间代码转换成机器码
(二进制),并且进行机器相关的代码优化。
llvm架构编译器的设计
Objective C/C/C++使用的编译器前端是Clang
,Swift的前端是Swift
,后端是LLVM
。示例图如下:
编译流程
我们使用main.m文件进行测试查看各阶段编译的结果
1 |
|
通过命令我们可以打印源码的编译阶段
1 | clang -ccc-print-phases main.m |
1 | 0: input, "main.m", objective-c |
其中含义为:
0:输入文件:找到源文件。
1:预处理阶段:处理包括宏的替换,头文件的导入。
2:编译阶段:进行词法分析、语法分析、检查语法是否正确,最终生成IR.
3:后端:LLVM会通过一个一个的Pass去优化,每个Pass做一些事情,最终生成汇编代码。
4:生成目标文件。
5:链接:链接需要的动态库和静态库,生成可执行文件。
6:通过不同的架构,生成对应的可执行文件。
预处理阶段
执行下面命令
1 | clang -E main.m |
执行完成后会看到头文件的导入和宏的替换。
编译阶段
词法分析
预处理完成后会进行词法分析。代码会被切成一个个Token,比如大小括号、等于号、字符串等。
1 | clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m |
语法分析
词法分析后就是语法分析,它的任务是验证语法是否正确。再次发分析的基础上将单词序列组合成各类语法短语,如”程序”,”语句”,”表达式”等,然后将所有节点组成抽象语法树
(AST),语法分析阶段程序判断源程序在结构上是否正确。
1 | clang -fmodules -fsyntax-only -Xclang -ast-dump main.m |
生成中间代码IR(intermediate representation)
完成上面的步骤后就开始生成中间代码IR了,代码生成器(Code Gemeration)会将语法树自定向下便利逐步翻译成LLVM IR。通过下面命令,查看IR代码
1 | clang -S -fobjc-arc -emit-llvm main.m |
Objective C代码这一步会进行runtime的桥接,property合成,ARC处理等。
这一步会生成main.ll
文件,内容如下:
1 | ; ModuleID = 'main.m' |
IR的基本语法
@全局标识
%局部标识
alloca开辟空间
align内存对齐
i32 32bit,4字节
store 写入内存
load 读取数据
call 调用函数
ret 返回
IR的优化
LLVM的优化分别是-O0 -O1 -O2 -O3 -Os
1 | clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll |
可以看到优化后的代码:
1 | ; ModuleID = 'main.m' |
bitCode(非必须)
生成.bc的中间代码。
1 | clang -emit-llvm -c main.ll -o main.bc |
1 | dec0 170b 0000 0000 1400 0000 0c0b 0000 |
生成汇编代码
我们通过最终的.bc或.ll代码生成汇编代码
1 | clang -S -fobjc-arc main.bc -o main.s |
生成的汇编代码如下:
1 | .section __TEXT,__text,regular,pure_instructions |
汇编代码也可以优化,参数和IR相同
1 | clang -Os -S -fobjc-arc main.m -o main.s |
优化后的汇编代码如下:
1 | .section __TEXT,__text,regular,pure_instructions |
生成目标文件
目标文件生成,汇编器以汇编代码作为输出,将汇编代码转为机器代码,最后输出目标文件(object file)。
1 | clang -fmodules -c main.s -o main.o |
.o文件的内容:
1 | cffa edfe 0c00 0001 0000 0000 0100 0000 |
生成可执行文件(链接)
链接器把编译产生的.o文件和.dylib/.a文件,生成一个mach-o文件。
1 | clang main.o -o main |
s
总结
本篇我们主要研究了编译器的编译流程,从源代码
到可执行文件
- 预处理:处理包括宏的替换,头文件的导入等。
- 词法分析:根据一些标识符对源文件进行切割,词法分析产生的记号一般可以分为如下几类:关键字、标识符、字面量和特殊符号。注意这一步是不会检查代码是否有错误。
- 语法分析:语法分析器对词法分析后的记号进行语法分析,产生抽象语法树(AST),这一步会检查语法是否正确
- 生成中间代码(IR):将上面生成的语法树转换成中间代码。中间代码使得编译器可以范围内前端和后端。编译器的前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。
- 生成汇编代码:将中间代码准换成汇编。
- 生成目标文件(.o):将汇编语言转换成目标文件。
- 链接目标文件,生成可执行文件:这一步主要是处理多个库依赖的情况。