这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战
1. LLVM概述
1.1 解释语言和编译语言
在我们日常开发中,比如iOS开发,使用Xcode编译成功才能运行,这个过程就是编译过程由LLVM编译器工作完成。计算机不能直接的理解高级语言,只能直接理解机器语言,所以必须要把高级语言翻译成机器语言,计算机才能执行高级语言的编写的程序。翻译的方式有两种,一个是编译,一个是解释。两种方式只是翻译的时间不同。解释型语言读取代码就会执行,而编译型语言要先翻译成cpu可以读的二进制代码。我们日常开发的OC或swift就是编译语言,python就是编译语言,可以直接通过python解释器进行运行,而oc需要通过编译器编译成可执行二进制进行执行。
1.2 LLVM介绍
LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开话,并兼容已有脚本。
LLVM计划启动于2000年,最初由由美国UIUC大学的Chris Lattner博士主持开展。2006年Chris Lattner加盟Apple Inc.并致力LLVM在Apple开发体系中的应用。Apple也是LLVM计划的主要资助者。
传统编译器设计:
编译器前端(Frontend)
编译器前端的任务是解析源代码。它会进行:词法分析、语法分析、检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree AST) ,LLVM的前端还会生成中间代码(intermediate representation,IR)
优化器(Optimizer)
优化器负责进行各种优化。改善代码的运行时间,例始消除冗余计算等。
后端(Backend)/代码生成器(CodeGenerator)
将代码映财到目标指令集。生成机器语言,并且进行机器相关的代码优化。
iOS的编译器架构
Objcective C/C/C++使用的编译器前端是Clang,Swift是Swift,后端都是LLVM。
LLVM的设计
当编译器决定支持多种源语言或多种硬架构时,LLVM的最重要的地方就来了。
其它的编对器如GCC,它方法非常成功,但由于它是作为整体应用程序设计的,因此它的用途受到了很大的限制。
LLVM设计的最重要方便是,使用通用的代码表示形式(IR ),它是用来在编译器中表示代码的形式。所以LLVM可以为任何编译语言独立编写前端,并且可以为任意硬件架构独立编写后端。
- 这种设计的思想就是中间层模式,我们添加一个中间层,新增语言的话只要添加对应的模式即可。
- Clang
clang是LLVM项目中的一个子项目,它是基于LLVM架构图的轻量级编译器,诞生之初是为了替代GCC,提供更快的编译速度,它是负责C、C++、OC语言的编译器,属于整个LLVM架构中的编译器前端,对于开发者来说,研究Clang可以给我们带来很多好处
2. LLVM编译流程
新建文件.m文件
int test(int a,int b){
return a + b + 3;
}
int main(int argc, const char * argv[]) {
int a = test(1,2);
printf("%d",a);
return 0;
}
通过命令可以打印源码的编译流程
clang -ccc-print-phases main.m
+- 0: input, "main.m", objective-c
+- 1: preprocessor, {0}, objective-c-cpp-output
+- 2: compiler, {1}, ir
+- 3: backend, {2}, assembler
+- 4: assembler, {3}, object
+- 5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image
看下具体解释:
0:输入文件,即:找到源文件。
1:预处理阶段,即:处理包括宏的处理,头文件的导入。
2:编译阶段,即:进行词法分析,语法分析,检测语法是否正确,最终生成IR。
3:后端,即:这里LLVM会通过一个一个节点pass去优化,每个pass做一些事情,最终生成汇编代码。
4:汇编代码生成目标文件。
5:链接:之前dyld时候说过,链接动态库和静态库,生成可执行文件。
6:绑定,即:根据不同架构,生成对应的可执行文件。
2.1 预处理阶段
我们在main.m文件中添加宏
#define C 30
typedef int KB_INT_64;
int test(int a,int b){
return a + b + C;
}
执行clang -E main.m在终端查看结果宏被替换了。
执行clang -E main.m >> main1.m查看生成对应文件生成替换后的源码
首先进行头文件导入,之后进行宏的替换。
- 值得注意的是
typedef不具备替换
运行
clang -E main.m >> main1.m
2.2 编译阶段
进行词法分析,语法分析,检查语法是否正确生成中间代码IR。
2.1 词法分析
执行clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m,进行词法分析这里会把代码分割一个个Token,比如大小括括号,等于号,字符串等。
2.2 语法分析
词法分析完成后就是语法分析,它的任务是验证语法是否正确,在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等,然后将所有节点组成抽像语法树(Abstract Syntax Tree,AST)。语法分析程序判断源程序在结构上是否正确。
我们执行clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
- 如果导入头文件找不到,可以指定SDK
clang -isysroot (自己SDK路径) -fmodules -fsyntax-only -Xclang -ast-dump main.m
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator14.5.sdk/ -fmodules -fsyntax-only -Xclang -ast-dump main.m
我们把代码改错一下少个}
再次编译检查语法报错
- 分析下
int test(int a,int b){}的词法分析- FunctionDecl:表示函数代表:used test 'int (int, int)'
- ParmVarDecl:表示
参数:used a 'int' - ParmVarDecl:表示
参数:used b 'int',2个参数是同一个层级 - CompoundStmt:表示括号的作用区域即
{}区域 - DeclStmt:描述下面的定义参数
- VarDecl:参数定义:used d 'KB_INT_64':'int' cinit
- IntegerLiteral:integer类型的常量
- BinaryOperator:操作符。这里表示+
树状结构翻译下类型于:return
20+ (30+(a+b))并不是我们想的那样按加法顺序实现,乘除会优先计算。
- ParmVarDecl:表示
- FunctionDecl:表示函数代表:used test 'int (int, int)'
2.3 生成中间代码IR
完成以上步骤后,就开始生成中间代码IR了,代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM IR,
- 可以通过下面命令可以生成
.ll的文本文件,查看IR代码。
clang -S -fobjc-arc -emit-llvm main.m
OC代码在这一步会进行runtime桥接,:property合成、ARC处理等
//以下是IR基本语法
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32bit,4个字节
store 写入内存
load 读取数据
call 调用函数
ret 返回
简单解释下
这就是test函数IR代码,这是没有经过优化的。
当然,IR文件在OC中是可以进行优化的,一般设置是在target - Build Setting - Optimization Level(优化器等级)中设置。LLVM的优化级别分别是-O0 -O1 -O2 -O3 -Os(第一个是大写英文字母O),下面是带优化的生成中间代码IR的命令
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
代码量大大减少
- xcode7以后开启bitcode,苹果会做进一步优化,生成.bc的中间代码,我们通过优化后的IR代码生成.bc代码
clang -emit-llvm -c main.ll -o main.bc//根据.ll生成.bc
2.4 生成汇编代码
- 我们通过最终的
.bc或者.ll代码生成汇编代码
clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
- 生成汇编代码也可以进行优化
clang -Os -S -fobjc-arc main.m -o main.s
代码量减少了些
2.5 生成目标文件
目标文件生成,将汇编代码作为输入,生成机器语言,输出目标文件
clang -fmodules -c main.s -o main.o
可以通过nm命令,查看下main.o中的符号
$xcrun nm -nm main.o
_printf函数是一个是undefined 、external的undefined表示在当前文件暂时找不到符号_printfexternal表示这个符号是外部可以访问的
2.6 链接link
之前在dyld中讲了关于链接的具体流程,这里链接主要链接需要的动态库和静态库,之后生成可执行文件。
- 静态库和可执行文件进行合并。
- 动态库独立存在。
连接器把编译生成的
.o文件和 .dyld .a文件链接,生成一个mach-o文件
clang main.o -o main//这里为啥不是用llvm,因为clang启动
查看链接之后的符号
$xcrun nm -nm main
这里有两个外部符号
_printf和dyld_stub_binder。
变成了可执行文件。
2.7 绑定
当我们的程序进入内存的的时候,外部函数会立即跟dyld_stub_binder绑定,这个dyld是强制执行,链接是打个标记,符号在哪个库中(编译期),绑定是在执行的时候把外部函数地址和符号进行绑定(运行期),一定会有dyld_stub_binder这个符号,先绑定这个符号,其它函数的绑定由dyld_stub_binder执行,最后通过不同的架构,生成相对应的mach-o可执行文件。
3. 总结
LLVM设计就是为了解决日益增加的架构和平台,通过中间层生成中间代码IR,提高了扩展性,降低了耦合性,整个LLVM的流程大概如下: