苹果官方资源opensource
相关文献:
iOS 分析dyld
本章节探究:
1.了解LLVM架构
2.编译流程
3.IR与bitcode
4.静态链接
一、相关概念
1.编译器
编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。
一个现代编译器的主要工作流程:源代码 (source code)
→ 预处理器(preprocessor)
→ 编译器 (compiler)
→ 目标代码 (object code)
→ 链接器(Linker)
→ 可执行程序
2.传统编译器的设计
编译器前端(Frontend)
任务是解析源代码。词法分析
、语法分析
、语义分析
、检查源代码是否存在错误
、构建抽象语法树AST(Abstract Syntax Tree)
。
如果是LLVM前端
(Clang/Swift
)还会生成中间代码(intermediate representation)
。优化器(Optimizer)
各种优化。改善代码运行时间,例如消除冗余计算等。(类似Xcode里设置编译优化等级)后端(Backend)/代码生成器(CodeGenerator)
将代码映射到目标指令集,生成机器语言,并进行机器相关代码优化。
(64位arm的指令集就是arm64,64位x86的指令集就是x86_64)
ps:传统编译器的前端优化器后端都属一个整体。它能适配的源语言与CPU能识别的二进制数据很有限。
二、了解LLVM
1.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已经被Apple、Microsoft、Google、Facebook等各大公司采用。
2.iOS的编译器架构
Objective-C/C/C++使用的编译器前端是Clang
,Swift的编译器前端是Swift
,它们全部后端是LLVM
。
3.LLVM的架构设计
当编译器决定支持多种源语言或多种硬件架构时,LLVM最重要的地方就来了。其它的编译如GCC
,它的方法非常成功,但由于它是作为整体应用程序设计的,因此它们的用途受到了很大的限制。
LLVM
设计的最重要的方面是,使用通用的代码表示形式(IR
),它是用来在编译器中表示代码的形式。所以LLVM
可以作为任何编译语言独立编写前端,并且可以为任意硬件框架独立编写后端。
适配不同的语言只需要开发与之对应的编译器前端,通过源代码生成IR;
适配不同的cpu架构只需要开发与之对应的编译器后端,通过IR输出成可执行文件。
注意:生成出来的IR代码都是一样的语法形式。
4.了解Clang
Clang
是LLVM
项目中的一个子项目。它是基于LLVM架构的轻量级编译器,诞生之初是为了替代GCC
,提供更快的编译速度。它负责C
、C++
、Objective-C
语言的编译器,它属于整个LLVM
框架中的编译器前端。
二、编译流程
首先看看源文件: main.m
#import
typedef int My_int; // 别名语法
#define A 10
int main(int argc, const char * argv[]) {
My_int b = 15;
printf("%d",A + 20); // A是宏定义
return 0;
}
通过命令打印源代码的编译阶段
$ cd 源代码main.m的目录
$ clang -ccc-print-phases main.m
+- 0: input, "main.m", objective-c // 读取objective-c代码
+- 1: preprocessor, {0}, objective-c-cpp-output // 预处理阶段:删除注释;替换宏定义;展开头文件等
+- 2: compiler, {1}, ir // 编译阶段:词法分析;语法分析;语义分析;检查源代码是否正确;生成IR代码
+- 3: backend, {2}, assembler // 编译后端:每个Pass优化,最终生成汇编代码
+- 4: assembler, {3}, object // 生成目标文件.o
+- 5: linker, {4}, image // 静态链接:链接需要的动态库和静态库,生成可执行文件
6: bind-arch, "x86_64", {5}, image // 校验不同架构,生成对应的可执行文件
编译流程解释:
0 - 输入文件:找到源文件。
1 - 预处理阶段:删除注释、替换宏定义、导入头文件。
2 - 编译阶段:词法分析、语法分析、语义分析、检查源代码是否正确、生成IR代码 .ll 。
3 - 编译后端:LLVM会通过一个个Pass去优化每个Pass做的事,最终生成汇编代码 .s 。
4 - 执行汇编程序:生成目标文件 .o 。
5 - 静态链接阶段:链接需要的动态库和静态库,所有.o黏合最后生成可执行文件。
6 - 根据不同的cpu架构去生成对应的可执行文件 exec 。
接下来了解一下编译的各个阶段。
1.预处理阶段
执行这条命令,可以看到:删除注释、替换宏定义、导入头文件
$ clang -E main.m
预处理阶段主要处理 以 #
号开头 的预处理命令。
2.编译阶段
- a. 词法分析
$ clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
把代码切成一个个Token
,比如大小括号、等于号、字符串、类型、关键字... 等等。
这就是为什么我们在源代码里多打空格也没有关系,它不会切空格。
- b. 语法分析
任务是检验语法是否正确。
$ clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
在词法分析的基础上将单词序列组合各类语法短语 (如 程序/语句/表达式 等等),然后将所有节点组成抽象语法树 AST(Abstract Syntax Tree)
main.m修改一段错误的代码(多加了一个括号)进行词法分析
#import
typedef int My_int;
#define A 10
int main(int argc, const char * argv[]) {
My_int b = 15;
printf("%d",A + 20));
return 0;
}
如果导入头文件找不到,那么可以指定SDK
$ clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk (自动的SDK路径) -fmodules -fsyntax-only -Xclang -ast-dump main.m
这个过程包含了语义分析。
- c. 语义分析
在语法分析的时候会结合代码的规则去进行语义分析 。
- d. 生成IR中间代码 (intermediate representation)
完成abc步骤后,开始生成中间代码IR。代码生成器
(Code Generation
)会讲语法树自上而下遍历逐步翻译成LLVM IR
。
把main.m
的代码修改一下
#import
int my_add(int a, int b) {
return a+b;
}
int main(int argc, const char * argv[]) {
printf("%d",my_add(2,3));
return 0;
}
通过下面命令生成.ll
的文本文件:
$ clang -S -fobjc-arc -emit-llvm main.m
Objective-C代码会在这一步进行runtime
桥接:@property合成、ARC处理..等等。
IR
的基本语法:
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32个bit位 / 4字节
store 写入内存
load 读取数据
call 调用函数
ret 返回
IR
代码的优化:
LLVM的优化等级分别是 -O0
、-O1
、-O2
、-O3
、-Os
$ clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
注意:不管是什么语言,OC
也好Swift
也罢,翻译出来的IR代码
都是一样的形式。然后再通过编译器后端根据不同的cpu框架
去生成对应的可执行文件。
bitcode
是什么
Xcode7
以后开启bitcode
后,苹果会做进一步优化。生成.bc
的中间代码。
可以理解成.bc
是.ll
的另一种表现形式,只是做了进一步优化,它和IR
一样都会根据不同cpu架构
去生成对应的汇编代码。
通过优化后的IR
代码生成.bc
代码
$ clang -emit-llvm -c main.ll -o main.bc
- e. 生成汇编代码
.s
通过最终的.ll代码
或者.bc代码
生成汇编代码.s
$ 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
- f. 生成目标文件
.o
通过汇编代码.s作为输入转换为机器代码,最后输出目标文件 .o
$ clang -fmodules -c main.s -o main.o
查看.o
文件中的符号
$ xcrun nm -nm main.o
- g. 静态链接 - 生成可执行文件
exec
静态链接器把编译产生的.o文件
和.a文件(一堆.o)
,生成一个mach-o文件
$ clang main.o -o main
查看链接后的符号
$ xcrun nm -nm main
静态链接重点:
在编译的静态链接过程中,静态链接器会对于一些外部的符号进行标记该符号属于哪个库的,再生成可执行文件。
在程序启动 进行加载可执行文件时,通过dyly动态链接器进行rebase
和bind
对外部符号进行动态绑定函数实现地址。比如
(undefined) external dyld_stub_binder (from libSystem)
外部符号是dyld_stub_binder
标记在libSystem
动态库里。