基础
解释型语言 & 编译型语言
- 解释型语言:读到代码即执行
- 编译型语言:需要先将代码翻译成cpu能读懂的二进制文件再执行
LLVM概述
- LLVM是构架编译器的框架系统,以C++编写而成
- 用以优化以任意程序语言编写的程序的编译时间、链接时间、运行时间、空闲时间
- 对开发者保持开放,并兼容已有脚本
编译器设计
传统
graph LR
A((Source Code)) --> B(Fronted前端)
subgraph 编译器
B --> C(Optimizer优化器)
C --> D(Backend后端/代码生成器)
end
D --> E((Machine Code机器码))
- 前端:解析源代码。词法分析、语法分析、语义分析,检查源代码是否存在错误,然后构建抽象语法树(LLVM的前端还会生成IR中间代码)
- 优化器:进行各种优化。改善代码的运行时间,比如消除冗余计算等。
- 后端/代码生成器:将代码映射到目标指令集,生成机器语言,并进行机器语言的优化。
iOS的编译器架构
- LLVM使用通用的代码表现形式(IR),可以为任何编程语言独立编写前端,并为任意硬件架构独立编写后端。
- OC/C/C++的编译器前端是Clang,Swift是SwiftC
graph LR
A(OC/C/C++) --> |Clang|B(LLVM Optimizer)
C(Swift) --> |SwiftC|B
B --> C(LLVM Code Generator)
编译流程
int main(int argc, const char * argv[]){
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
- 传入文件
- 预处理阶段
- 编译生成IR(前端)
- 后端,通过一个个
pass进行相应优化,生成assembler汇编代码 - 链接器,链接需要的动态库和静态库。
- 通过不同的架构,生成对应的可执行文件(此时为x86_64架构)
预处理阶段
我们加上如下代码
#import <stdio.h>
#define NUM 30
int main(int argc, const char * argv[]){
printf("%d",NUM);
return 0;
}
执行指令:clang -E main.m并查看一下
···
// 头文件的导入
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/AvailabilityInternal.h" 1 3 4
# 137 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/Availability.h" 2 3 4
# 70
···
int main(int argc, const char * argv[]){
printf("%d",30); // 宏的替换
return 0;
}
可以看出这个阶段主要做了:
- 导入头文件
- 宏的替换
编译阶段
词法分析
对上述代码执行指令clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m查看结果:
可以看出这个阶段主要做了:
- 根据"{","}","","=","char","const"等字符,将代码切成一个个Token
语法分析
对上述代码执行指令clang -fmodules -fsyntax-only -Xclang -ast-dump main.m查看结果
我们以
main函数为例
`-FunctionDecl 0x7ff49f05ae00 <line:4:1, line:7:1> line:4:5 main 'int (int, const char **)'
# 参数
|-ParmVarDecl 0x7ff49e0342d0 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x7ff49e034418 <col:20, col:38> col:33 argv 'const char **':'const char **'
`-CompoundStmt 0x7ff49f05b570 <col:40, line:7:1>
# 函数调用
|-CallExpr 0x7ff49f05b4e0 <line:5:5, col:20> 'int'
# 函数指针
| |-ImplicitCastExpr 0x7ff49f05b4c8 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7ff49f05b3c8 <col:5> 'int (const char *, ...)' Function 0x7ff49f05af50 'printf' 'int (const char *, ...)'
| |-ImplicitCastExpr 0x7ff49f05b528 <col:12> 'const char *' <NoOp>
| | `-ImplicitCastExpr 0x7ff49f05b510 <col:12> 'char *' <ArrayToPointerDecay>
| | `-StringLiteral 0x7ff49f05b430 <col:12> 'char [3]' lvalue "%d"
# 这里可以看到,我们通过这个指令生成的AST是已经经过了预处理的,宏定义已经被替换
| `-IntegerLiteral 0x7ff49f05b450 <line:2:13> 'int' 30
# 返回值
`-ReturnStmt 0x7ff49f05b560 <line:6:1, col:8>
`-IntegerLiteral 0x7ff49f05b540 <col:8> 'int' 0
这个阶段主要做了:
- 验证语法是否正确。在词法分析的基础上将单词序列组合成各类语法短语,如“程序”、“语句”、“表达式”等等
- 将所有节点组成抽象语法树AST
生成中间代码IR(Intermediate Representation)
首先我们学习一点IR的基本语法:
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32个bit,4个字节
store 写入内存
load 读取数据
call 调用函数
ret 返回
代码如下:
#import <stdio.h>
int testFunc(int a,int b) {
return a + b;
}
int main(int argc, const char * argv[]){
int c = testFunc(1,2);
printf("%d",c);
return 0;
}
指令clang -S -fobjc-arc -emit-llvm main.m查看生成的main.ll文件(截取部分)
···
; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @testFunc(i32 %0, i32 %1) #0 {
// 开辟两个32位的内存空间并进行内存对齐
%3 = alloca i32, align 4
%4 = alloca i32, align 4
// 将传入的参数a0和a1存储到上面开辟的a3和a4中
store i32 %0, i32* %3, align 4
store i32 %1, i32* %4, align 4
// 将a3和a4的值又放到a5和a6中【奇奇怪怪】
%5 = load i32, i32* %3, align 4
%6 = load i32, i32* %4, align 4
// 执行加法运算,将结果放到a7中
%7 = add nsw i32 %5, %6
// 返回32位(4字节)的返回值a7
ret i32 %7
}
; Function Attrs: noinline optnone ssp uwtable
define i32 @main(i32 %0, i8** %1) #1 {
···
// call调用testFunc方法,传入两个32位的参数1和2
%7 = call i32 @testFunc(i32 1, i32 2)
···
ret i32 0
}
···
IR的优化
- 可以看到上面的处理过程中,其实是有一些冗余操作的,这是因为我们目前的代码优化等级为
None,也就是Xcode -> Build Settings -> Optimization Level - 我们可以在指令中指定优化等级,如
clang -Os -S -fobjc-arc -emit-llvm main.m,此时的testFunc和main就变成了
define i32 @testFunc(i32 %0, i32 %1) local_unnamed_addr #0 {
%3 = add nsw i32 %1, %0
ret i32 %3
}
// main函数中更是连调用testFunc都没有了,直接算出了结果3
define i32 @main(i32 %0, i8** nocapture readnone %1) local_unnamed_addr #1 {
%3 = tail call i32 (i8*, ...) @printf(i8* nonnull dereferenceable(1) getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 3) #3, !clang.arc.no_objc_arc_exceptions !9
ret i32 0
}
bitcode
- XCode7之后,开启bitcode苹果会做进一步的优化,生成.bc中间代码。
- 根据不同架构生成对应.bc。
- 我们通过IR代码生成.bc代码。
clang -emit-llvm -c main.ll -o main.bc
生成汇编
- 我们可以通过.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.bc -o main.s
生成目标文件【汇编器】
- 目标文件的生成,是汇编器以汇编代码作为输入,汇编代码 -> 机器代码 -> 目标文件objcet file
- 这个部分只是clang提供了指令,但完成是后端去完成的
- 指令:
clang -fmodules -c main.s -o main.o - 生成.o文件后,我们可以通过
xcrun nm -nm main.o指令查看该文件中的符号
(undefined) external _printf
0000000000000000 (__TEXT,__text) external _testFunc
000000000000000a (__TEXT,__text) external _main
会发现找不到printf这个方法,这就需要我们下面的步骤了
undefined:当前文件找不到该符号external:该符号可以被外部访问
生成可执行文件【链接器】
- 链接器把编译生成的.o文件和库(.dylib,.a)文件,生成可执行文件
- 指令:
clang main.o -o main - 再通过
xcrun nm -nm main指令查看符号
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100003f54 (__TEXT,__text) external _testFunc
0000000100003f5e (__TEXT,__text) external _main
0000000100008008 (__DATA,__data) non-external __dyld_private
链接:要知道外部符号是在哪个库里(相当于打个标记,要去找哪个库)
绑定:在执行的时候,将外部函数地址和符号绑定在一起,所以说只要链接就一定有dyld_stub_binder这个外部函数