这是我参与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
表示在当前文件暂时找不到符号_printf
external
表示这个符号是外部可以访问
的
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的流程大概如下: