iOS 底层探索(二十三) LLVM (上)
iOS 底层探索(二十三) LLVM (上)
什么是编译器
Python解释器
创建helloWorld.py
的文件,内容如下
print("hello\n")
然后使用命令行进行运行
其中python
命令的意义是python语言的解释器。因为它可以直接运行处结果。
C语言编译器
创建helloWorld.c
文件,代码内容如下:
#include <stdio.h>
int main(int a, char *argv[]){
printf("hello \n");
return 0;
}
使用clang
进行编译,会得到a.out
文件,查看a.out
文件的格式,如下
由打印可知,a.out
文件是Mach-O
的可执行文件。这个文件是根据编译的电脑的设备决定的,也就是说,当前的电脑可以执行。
执行a.out
文件
解释器与编译器
- 解释器:一边读一边让CPU执行
- 编译器:编译成一个可执行文件,这个可执行文件即为CPU可执行的二进制的组合
0 - 1
组合,即把高级语言
变成CPU能看得懂的0 - 1
组合
那么在Xcode中使用CMD + B
做了什么?
将代码编译成.app
包中的可执行文件。
汇编
在早期的程序中,是直接输入01
组合进行程序编译,
如:
00001111 -- call
00000111 -- bl
比如上方,前面的二进制代表后面的指令。就相当于给这个特殊的二进制做了个标记
,这个标记就是早期的汇编。而能够将这些标记变成前面的特定二进制的指令的东西就是编译器
。
但是汇编有个非常严重的缺点:不可以跨平台
。
因为不同厂商生产的CPU对特定的指令识别不同,因此出现了一种高级语言C
语言。
跨平台:不同的CPU能够解读相同的代码。
LLVM
LLVM概述
LLVM是架构编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compiler-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。
LLVM计划启动于2000年,最初由美国UIUC大学的Chris Lattner博士主持开展。2006年Chris Lattner加盟Apple Inc.并致力于LLVM在Apple开发体系中的应用。Apple也是LLVM计划的主要自助者。
目前LLVM已经被苹果iOS开发工具、Xilinx Vivado、Facebook、Google等各大公司采用。
传统编译器设计
编译器前端(Frontend)
编译器前端的任务是解析源代码。编译阶段,它会进行:词法分析、语法分析、语义分析、检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree),LLVM的前端还会生成中间代码(intermediate representation,IR)。
调用方法,耗时操作。
函数调用栈
func() {
// 压栈
int a = 10;
int b = 20; // 局部变量
}
// 出栈
当执行到一个方法时,会申请一段栈空间(16字节对齐的内存)。
- 找到方法的代码,内存寻址。
- 开辟内存空间
- 内存空间输入值,即压栈
- 执行代码
- 栈平衡,即出栈
- 代码回跳。
目前为了程序让程序员可读,高级语言让程序员的效率提高。但如果不做优化,即将高级语言直接编译成机器语言,会浪费时间与空间。
优化器
优化器负责进行各种优化。改善代码的运行时间,例如消除冗余计算等。在LLVM中优化器接收到的代码为IR
,给到后端的代码也是IR
代码。
后端(Backend)/代码生成器(CodeGenerator)
将代码映射到目标指令集
。生成机器语言,并且进行机器相关的代码优化。
iOS的编译器架构
Objective C/C/C++使用的编译器前端是Clang
,Swift是Swift
,后端都是LLVM
。
LLVM的设计
当编译器决定支持多种源语言或多种硬件架构是,LLVM最重要的地方就来了。其他的编译器如GCC,它方法非常成功,但由于它是作为整体应用程序设计的,因此它们的用途受到了很大的限制。
LLVM设计的最重要方面是,使用通用的代码表示形式(IR),它是用来在编译器中表示代码的形式,所以LLVM可以为任何编程语言独立编写前端,并且可以为任意硬件架构独立编写后端。LLVM使用的语言为C++。
Clang
Clang是LLVM项目中的一个子项目。它是基于LLVM架构的轻量级编译器,诞生之初是为了替代GCC,提供更快的编译速度。它是负责编译C、C++、Objective-C语言的编译器,它属于整个LLVM架构中的编译器前端。对于开发者来说,研究Clang可以给我们带来很多好处。
编译流程
通过命令可以打印源码的编译阶段
创建一个macOS
的项目,在main.m
文件中输入如下内容
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
使用如下命令编译main.m
文件。
clang -ccc-print-phases main.m
从打印结果来看,编译过程一共有6步,因为第0
步为输入。
0: 输入文件:找到源文件,
1: 预处理阶段:这个过程包括宏
的替换
,头文件
的导入
2: 编译阶段:进行词法分析
、语法分析
、检测语法
是否正确,最终生成IR
3: 后端:这里LLVM会通过一个一个的Pass去优化,每个Pass做一些事情,最终生成汇编代码
4: 生成目标
文件,即.o
文件
5: 链接:链接需要的动态库和静态库,生成完整可执行文件
即.o
的集合。
6: 通过不同的架构,生成对应的可执行文件
,这时是完整的可执行文件
。
预处理阶段
修改上方的main.m
文件为如下代码
#import <stdio.h>
#define C 30
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
int b = 20;
printf("%d", a + b + C);
}
return 0;
}
然后执行如下命令查看结果
clang -E main.m >> mainE.m
这时会生成一个mainE.m
的文件,这个文件就是预处理之后倒出的文件,并不是自动生成的,打开该文件。
可知,我们自己写的代码一共不超过10行,而编译之后变成了500+行,这主要是因为#import <stdio.h>
导入文件导致的。
找到main
函数的位置,代码如下
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
int b = 20;
printf("%d", a + b + 30);
}
return 0;
}
可以发现,预处理中的宏
已经没有了,被宏的值替换
掉了。
修改main.m
代码如下:即替换int
#import <stdio.h>
#define C 30
typedef int INT_64;
int main(int argc, const char * argv[]) {
@autoreleasepool {
INT_64 a = 10;
INT_64 b = 20;
printf("%d", a + b + C);
}
return 0;
}
重新编译查看。
从结果可知,自定义的INT_64
并没有替换成int
,原因是typedef
主要是给数据类型取别名的。
在日常开发中,typedef
与define
都是为了取别名的。但是define
可以进行混淆
,比如核心类名称、类方法不想让别人知道时,即可利用define
的机制进行混淆
编译阶段
词法分析
预处理完成后就会进行词法分析。这里会把代码切割成一个个Token,比如大小括号,等于号还有字符串等。
执行如下命令:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
这个阶段也是没有生成真正的文件,并且不能导出文件查看。
从输出可以看出,这是在进行词法的短句分析,左侧为词法,右侧为该词法所在的位置。可以根据这个位置进行源代码的一一对照。
语法分析
词法分析完成之后就是语法分析,Radeon任务时验证语法是否正确。在词法分析的基础上将单词序列组合成各类语法短语,如程序
、语句
、表达式
等等,然后将所有节点组成抽象语法树
(Abstract Syntax Tree, AST)。语法分析程序判断源程序在结构上是否正确。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
如果导入头文件找不到,那么可以指定SDK
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.sdk -fmodules -fsyntax-only -Xclang -ast-dump main.m
运行之后,查看结果。生成语法树
这块的东西我们可能看不太懂,我们找到我们能看得懂的位置,即三个连加方法。如下
可以看到,标记的位置的意义为先让a + b
得出的结果再与c
相加。有兴趣可以看一下()
括号以及* /
乘除法的运算。
上方主要树状语法含义。
-
FunctionDecl
为方法, -
ParmVarDecl
参数变量 -
CompoundStmt
复合语句 -
VarDecl
定义变量 -
CallExpr
调用函数 -
BinaryOperator
运算符 -
ReturnStmt
返回值
我们手动写个语法错误,重新编译一下查看结果。
编译结果会出现错误,并且提示你在哪一行的什么错误。
生成中间代码IR(intermediate representation)
完成以上步骤后就开始生成中间代码IR了,代码生成器
(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM IR
。通过下面命令可以生成.ll
的文本文件,查看IR代码
clang -S -fobjc-arc -emit-llvm main.m
Objective-C代码在这一步会进行runtime的桥接:property合成,ARC处理等。
修改main.m
中的代码如下
#import <stdio.h>
int sum(int a, int b) {
return a + b;
}
int main(int argc, const char * argv[]) {
printf("%d", sum(10, 20) + 30);
return 0;
}
运行命令后生成main.ll
文件,使用VSCode
打开它查看。
; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"
@.str = private unnamed_addr constant [3 x i8] c"%d\00", align 1
; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @sum(i32 %0, i32 %1) #0 {
%3 = alloca i32, align 4 // -> int a
%4 = alloca i32, align 4 // -> int b
store i32 %0, i32* %3, align 4 // -> a = 10
store i32 %1, i32* %4, align 4 // -> b = 20
%5 = load i32, i32* %3, align 4 // -> 取值
%6 = load i32, i32* %4, align 4 // -> 取值
%7 = add nsw i32 %5, %6 // -> 10 + 20
ret i32 %7 // return 30
}
; Function Attrs: noinline optnone ssp uwtable
define i32 @main(i32 %0, i8** %1) #1 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8 // 8字节,指针。
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%6 = call i32 @sum(i32 10, i32 20) // 调用函数
%7 = add nsw i32 %6, 30
%8 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 %7)
ret i32 0
}
declare i32 @printf(i8*, ...) #2
attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { noinline optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}
!llvm.ident = !{!8}
!0 = !{i32 2, !"SDK Version", [3 x i32] [i32 10, i32 15, i32 6]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
!2 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!3 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!4 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!5 = !{i32 1, !"Objective-C Class Properties", i32 64}
!6 = !{i32 1, !"wchar_size", i32 4}
!7 = !{i32 7, !"PIC Level", i32 2}
!8 = !{!"Apple clang version 12.0.0 (clang-1200.0.32.21)"}
可以看到这个文件中生成了一定类似乱码的东西,但我们还是可以查看的,因为我们定义了两个方法main
与sum
函数。
IR的基本语法
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32个bit,4个字节
store 写入内存
load 读取数据
call 调用函数
ret 返回
从上面的.ll
文件中可以看到会有各种的创建以及赋值。这是硬件设备影响的,即CPU从内存中取值进行运算,运行结果传给内存进行存储,当CPU再次使用时,需要再次取值,然后在循环往复。
IR的优化
LLVM的优化级别分别是 -O0
-O1
-O2
-O3
-Os
-Ofast
-Oz
(第一个是大写英文字母O),这个级别可以在Xcode中的Optimization Level
中进行查看。
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
重新运行查看结果如下
bitCode
Xcode7以后开启bitCode,苹果会做进一步的优化。生成.bc
的中间代码,我们通过优化后的IR代码生成.bc
代码
clang -emit-llvm -c main.ll -o main.bc
在日常开发中,在接入三方库的时候回出现bitCode
的错误,就是优化的原因。
生成汇编代码
我们通过最终的.bc
或者.ll
代码生成汇编代码,
clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
clang -S -fobjc-arc main.m -o main.s
为了区分,我们分别生成mainBC.s
、mainLL.s
以及mainM.s
的文件。进行对照
从.s
文件中我们可知,mainM.s
代码为61
行,另外两种.s
文件都是54
行,之所以mainBC.s
与mainLL.s
的代码一致是因为我们写的代码相对简单。并且我们使用的.ll
文件是优化后的代码。
生成汇编代码也可以进行优化
clang -Os -S -fobjc-arc main.m -o main.s
运行后查看可知.s
文件变成了47
行,再对.ll
文件进行优化查看。
clang -Os -S -fobjc-arc main.ll -o main.s
运行后可知,.ll
文件生成的优化文件也是47
行,因此可知优化的结果都是相同的。
生成目标文件(汇编器)
目标文件的生成,是汇编器以汇编代码作为输入,将汇编代码转换为机器码,最后输出目标文件
(object file)。
clang -fmodules -c main.s -o main.o
.o
文件与其他文件都有差别
.o
已经变成了Mach-O
的文件。
通过nm
命令,查看main.o
中的符号
_printf 是一个undefined extrnal
的undefined
表示在当前文件暂时找不到符号_printf
external
表示这个符号是外部可以访问的。
生成可执行文件(链接)
连接器把编译产生的.o
文件和(.dylib
.a
)文件,生成一个Mach-O
文件
clang main.o -o main
运行后查看结果。
_printf
在运行的时候才会进行最后的绑定,但目前已经知道_printf
在哪个框架中了,即libSystem
。这时main
为可执行的Mach-O
文件
Clang插件
- 下载llvm-project项目
git clone https://github.com/llvm/llvm-project
国内网速较慢,可以使用一下框架
git clone https://gitee.com/mirrors/llvm-project.git
LLVM编译
由于最新的LLVM只支持cmake
来编译,因此我们还需要安装cmake
。
安装cmake
- 查看
brew
已安装列表,检查是否安装过cmake
,如果有,就跳过此步骤
brew list
- 如果没有,就使用
brew
安装
brew install cmake
- 如果报权限错误,则使用如下命令放开权限
sudo chown -R `whoami`:admin /usr/local/share
通过Xcode编译llvm
-
cmake
编译成Xcode
项目
cd llvm-project
mkdir build_xcode
cd build_xcode
cmake -G Xcode ../llvm
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="Release" ../llvm
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="debug" ../llvm
-
成功之后,可以在
build_xcode
文件夹中找到LLVM.xcodeproj
文件, -
打开
LLVM.xcodeproj
,并选择自动创建Schemes
-
自动创建
完成后,选择ALL_BUILD
进行编译,如果找不到,可以在Manager Schemes
中进行搜索选择。 -
至此,编译完成。
通过 ninja 编译
- 使用
ninja
进行编译则还需要安装ninja
。使用$ brew install ninja
命令即可安装ninja - 在llvm源码根目录下新建一个
build_ninja
目录,最终会在build_ninja
目录下生成build.ninja
- 在llvm源码根目录下新建一个
llvm_release
目录,最终编译文件会在llvm_release
文件夹路径下
$ cd llvm_build
$ cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=安装路径(本机为 /Users/xxx/xxx/LLVM/llvm_release,注意DCMAKE_INSTALL_PREFIX后面不能有空格)
- 依次执行编译、安装指令
$ ninja
$ ninja install
- 至此
ninja
编译完成
上一篇: 【算法百题之三十七】回文数
下一篇: Maven总结