一:编译器
编译器是什么已不用多说,一句话从代码到机器码就是编译器的工作.
左边输入源码,右边输出机器码
Frontend表示前端,主要负责词法分析、语法分析、语义分析、生成中间代码.这时就会进行各种检查,会报错或者警告.
Optimizer表示优化器,负责中间代码的优化,去除冗余代码,优化结构
Backend表示后端,生成机器码,并且进行链接,也就是将不同的二进制文件合并成一个可执行文件.
1.LLVM
Xcode5之后完全使用LLVM作为编译器.
LLVM也是上面说的那种Frontend -> Optimizer ->Backend架构.
不过LLVM路子很野,可以有很多个接口,也就是前端(Frontend),每一种前端对应一种或多种语言,这些前端最终都会生成相同的中间代码,叫做LLVM IR;
优化器的从始至终只处理LLVM IR.
新增一个前端不需要对LLVM的优化器进行调整,只需要新增一个前端; 增加一个新的平台只需要增加一个后端即可.
相比较而言,GCC就支持一个新的前端或者后端就要麻烦的多,原本的GCC家族(C,C++,OC),以及Java、.NET、Python、Ruby等都可以使用LLVM编译.
LLVM IR有3种表示形式,
存在内存中.
存在硬盘中的.ll代码文件,可以阅读.
存在硬盘中的二进制文件,扩展名是.bc,也就是bitcode.
2.Clang
Clang就是一个LLVM前端,负责将C,C++,OC翻译成LLVM IR.
Clang的工作内容:
预处理, 去掉注释,头文件的引用关系,把宏定义对应到各个位置
静态分析,给出错误信息,警告信息和修复方案
词法分析,这里会把代码切成一个个 Token,括号,符号,关键字等等都被切割出来
语法分析,验证语法是否正确,将所有节点组成抽象语法树AST
生成 LLVM IR, CodeGen会负责将语法树自顶向下遍历逐步翻译成 LLVM IR
3.Swift
同样LLVM中还需要一个前端负责对 Swift 源代码进行静态分析和纠错,并转换为 LLVM IR,这个前端也叫swift.
不过swift比clang的过程要复杂一些,多了一个生成SIL的过程.
Swift的工作内容:
导入Clang模块并将它们导出的OC API 映射到相应的 Swift API
解析生成AST
生成SIL,将经过类型检查的 AST 降级为 SIL
优化SIL,为程序执行额外的高级 Swift 特定优化,包括自动引用计数优化,虚拟化和通用专业化
将 SIL 降级到 LLVM IR
二.编译流程
引用戴铭老师的例子走一遍,原文
1.编译一个main.m
装了Xcode就自带LLVM,可以直接试一试.
创建一个项目,覆盖main.m的代码
#import
#define DEFINEEight 8
#pragma 这是标记
//这是注释
int main(){
@autoreleasepool {
int eight = DEFINEEight;
int six = 6;
NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
int rank = eight + six;
NSLog(@"%@ rank %d", site, rank);
}
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, "arm64", {5}, image
第0步引入文件
第1步预编译,输出c++文件
第2步编译为LLVM IR文件
第3步输出汇编文件
第4步输出二进制文件
第5步链接各二进制文件
第6步根据架构输出对应可执行文件
执行:
clang -E main.m
输出了非常多的东西,因为导入了foundation,先看最后几行
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
# 187 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 22 "main.m" 2
#pragma 这是标记
int main(){
@autoreleasepool {
int eight = 8;
int six = 6;
NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
int rank = eight + six;
NSLog(@"%@ rank %d", site, rank);
}
return 0;
}
除了foundation,还可以看到DEFINEEight被替换成了8,注释没了,但是#pragma还在
所以预编译就做了这些事:导入文件,去除注释,替换宏定义.
这一步会生成main.cpp文件,在main.m的同一路径.几万行代码,在最后可以找到替换为C++的main函数
int main(){
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
int eight = 8;
int six = 6;
NSString* site = ((NSString * _Nullable (*)(id, SEL, const char * _Nonnull))(void *)objc_msgSend)((id)((NSString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("alloc")), sel_registerName("initWithUTF8String:"), (const char *)"starming");
int rank = eight + six;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_y4_681pp9bd3c31j1_m0jqnt4h00000gn_T_main_3eda9c_mi_0, site, rank);
}
return 0;
}
接下来是词法分析
执行:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
输出
annot_module_include '#import
#define DEFINEEight 8
#pragma 这是标记
//这是注释
int main(){
@autoreleasepool {
int eight = DEFINEEight;
int six = 6;
NSString* site = [[NSString alloc] initWithUTF8String:"starming"];
int rank = eight + six;
NSLog(@"%@ rank %d", site, rank);
}
return 0;
}
@???W?\?V?A?`' Loc=
int 'int' [StartOfLine] Loc=
identifier 'main' [LeadingSpace] Loc=
l_paren '(' Loc=
r_paren ')' Loc=
l_brace '{' Loc=
at '@' [StartOfLine] [LeadingSpace] Loc=
identifier 'autoreleasepool' Loc=
l_brace '{' [LeadingSpace] Loc=
int 'int' [StartOfLine] [LeadingSpace] Loc=
identifier 'eight' [LeadingSpace] Loc=
equal '=' [LeadingSpace] Loc=
numeric_constant '8' [LeadingSpace] Loc=>
semi ';' Loc=
int 'int' [StartOfLine] [LeadingSpace] Loc=
identifier 'six' [LeadingSpace] Loc=
equal '=' [LeadingSpace] Loc=
numeric_constant '6' [LeadingSpace] Loc=
semi ';' Loc=
identifier 'NSString' [StartOfLine] [LeadingSpace] Loc=
star '*' Loc=
identifier 'site' [LeadingSpace] Loc=
equal '=' [LeadingSpace] Loc=
l_square '[' [LeadingSpace] Loc=
l_square '[' Loc=
identifier 'NSString' Loc=
identifier 'alloc' [LeadingSpace] Loc=
r_square ']' Loc=
identifier 'initWithUTF8String' [LeadingSpace] Loc=
colon ':' Loc=
string_literal '"starming"' Loc=
r_square ']' Loc=
semi ';' Loc=
int 'int' [StartOfLine] [LeadingSpace] Loc=
identifier 'rank' [LeadingSpace] Loc=
equal '=' [LeadingSpace] Loc=
identifier 'eight' [LeadingSpace] Loc=
plus '+' [LeadingSpace] Loc=
identifier 'six' [LeadingSpace] Loc=
semi ';' Loc=
identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=
l_paren '(' Loc=
at '@' Loc=
string_literal '"%@ rank %d"' Loc=
comma ',' Loc=
identifier 'site' [LeadingSpace] Loc=
comma ',' Loc=
identifier 'rank' [LeadingSpace] Loc=
r_paren ')' Loc=
semi ';' Loc=
r_brace '}' [StartOfLine] [LeadingSpace] Loc=
return 'return' [StartOfLine] [LeadingSpace] Loc=
numeric_constant '0' [LeadingSpace] Loc=
semi ';' Loc=
r_brace '}' [StartOfLine] Loc=
eof '' Loc=
可以看出词法分析只需要处理main.m中的代码,把所有的字符串,符号,括号都拆分开,拆出来的每一个部分,叫做token.
在接下来是语法分析
执行:
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
输出
-FunctionDecl 0x15ab12390 line:26:5 main 'int ()'
`-CompoundStmt 0x15b026d48
|-ObjCAutoreleasePoolStmt 0x15b026d00
| `-CompoundStmt 0x15b026cc8
| |-DeclStmt 0x15ab12530
| | `-VarDecl 0x15ab124a8 line:28:13 used eight 'int' cinit
| | `-IntegerLiteral 0x15ab12510 'int' 8
| |-DeclStmt 0x15ab4e6f8
| | `-VarDecl 0x15ab12560 col:13 used six 'int' cinit
| | `-IntegerLiteral 0x15ab125c8 'int' 6
| |-DeclStmt 0x15b024be8
| | `-VarDecl 0x15ab4e750 col:19 used site 'NSString *' cinit
| | `-ObjCMessageExpr 0x15ab50b90 'NSString * _Nullable':'NSString *' selector=initWithUTF8String:
| | |-ObjCMessageExpr 0x15ab4eb58 'NSString *' selector=alloc class='NSString'
| | `-ImplicitCastExpr 0x15ab50b78 'const char * _Nonnull':'const char *'
| | `-ImplicitCastExpr 0x15ab50b60 'char *'
| | `-StringLiteral 0x15ab4ebc8 'char [9]' lvalue "starming"
| |-DeclStmt 0x15b0252a8
| | `-VarDecl 0x15b024c18 col:13 used rank 'int' cinit
| | `-BinaryOperator 0x15b024d20 'int' '+'
| | |-ImplicitCastExpr 0x15b024cf0 'int'
| | | `-DeclRefExpr 0x15b024c80 'int' lvalue Var 0x15ab124a8 'eight' 'int'
| | `-ImplicitCastExpr 0x15b024d08 'int'
| | `-DeclRefExpr 0x15b024cb8 'int' lvalue Var 0x15ab12560 'six' 'int'
| `-CallExpr 0x15b026c48 'void'
| |-ImplicitCastExpr 0x15b026c30 'void (*)(id, ...)'
| | `-DeclRefExpr 0x15b0252c0 'void (id, ...)' Function 0x15b024d48 'NSLog' 'void (id, ...)'
| |-ImplicitCastExpr 0x15b026c80 'id':'id'
| | `-ObjCStringLiteral 0x15b025340 'NSString *'
| | `-StringLiteral 0x15b025318 'char [11]' lvalue "%@ rank %d"
| |-ImplicitCastExpr 0x15b026c98 'NSString *'
| | `-DeclRefExpr 0x15b025360 'NSString *' lvalue Var 0x15ab4e750 'site' 'NSString *'
| `-ImplicitCastExpr 0x15b026cb0 'int'
| `-DeclRefExpr 0x15b025398 'int' lvalue Var 0x15b024c18 'rank' 'int'
`-ReturnStmt 0x15b026d38
`-IntegerLiteral 0x15b026d18 'int' 0
这一步会检查语法的正确性,给出警告,报错,以及修改建议.生成的内容叫做抽象语法树AST.
生成AST之后就可以开始生成IR代码了
执行:
clang -O3 -S -fobjc-arc -emit-llvm main.m -o main.ll
输出main.ll文件,路径和main.m相同,是可读的.
这里"-O3"是LLVM的优化策略,有-O1,-O3,-Os,也可以不设置.
如果设置了bitcode,还可以进一步优化.
接下来生成汇编
clang -S -fobjc-arc main.m -o main.s
生成目标文件
clang -fmodules -c main.m -o main.o
生成可执行文件并执行
clang main.o -o main
./main
输出
starming rank 14
2.从Xcode观察编译过程
这里的过程可以在buildsetting,Build Phases 和 Build Rules中进行配置
首先是预编译,可以看到new build system等内容.
然后是编译cocoapods的targets,包括创建framework(cocoapods使用use framework),copy头文件,以及编译.m文件
接下来是主target,其实是与cocoapods的targes一致的,这一步主target也会被打包成framework.
也是拷贝.h文件,编译swift文件,编译.m文件,编译xib文件,拷贝资源文件,
接下来执行cocoapods脚本,Build Phases的脚本.
最后拷贝swift标准库以及签名.
每一个都可以点开详情.
比如编译.m文件可以看到clang信息,这些基本是可以在build setting中进行配置的.
前面是CompileC任务描述
然后是切换路径
最后clang -x objective-c -target x86_64-apple-ios12.0-simulator ...就是编译的命令
- 编译的流程
1.处理文件信息
2.执行CocoaPod编译前脚本
3.编译.m文件(h文件是不需要编译的),执行clang命令
4.链接framework
5.拷贝和编译xib,bundle文件
6.编译 ImageAssets
7.处理 info.plist
8.执行CocoaPod脚本
9.拷贝Swift标准库
10.创建.app文件和签名
3.配置编译选项
Build settings设置在build的过程中各个阶段的选项,clang的配置就属于这个范围.
Build Phases构建可执行文件的规则,指定 target 的依赖项目,指定在target build之前需要先build的依赖.
在Compile Source中指定必须编译的文件,这些文件同样会根据Build Setting和Build Rules里的设置来处理.
在Link Binary With Libraries里会列出所有的静态库和动态库,它们会和编译生成的目标文件链接.
把静态资源拷贝到bundle里.
另外还可以通过在build phases里添加自定义脚本来做些事情,比如像CocoaPods所做的那样.Build Rules指定不同文件类型如何编译,每条build rule指定了该类型如何处理以及输出在哪,可以增加新规则对特定文件类型添加处理方法.
上面这些都是在Xcode UI中可视化的,这些信息最终需要以文件的格式保存下来,那就是.pbxproj文件,
路径在[项目名称].xcodeproj包里的project.pbxproj.
打开这个文件,在最后一行有一个
rootObject = F7036F002511EC050031CE83 /* Project object */;
搜索这个rootObject ID,可以找到PBXProject section,
这个文件就是以section为单位描述配置.
/* Begin PBXProject section */
F7036F002511EC050031CE83 /* Project object */ = {
isa = PBXProject;
attributes = {
CLASSPREFIX = XX;
LastUpgradeCheck = 1310;
ORGANIZATIONNAME = XXXX;
TargetAttributes = {
F7036F072511EC050031CE83 = {
CreatedOnToolsVersion = 11.6;
LastSwiftMigration = 1240;
};
};
};
...
这PBXProject section里找到target
targets = (
F7036F072511EC050031CE83 /* XXXXX */,
);
再搜索这个ID,就可以找到更多配置,这个.pbxproj文件就是以这种id索引的方式进行记录和查找.
比如继续顺着这个ID,可以找到更多的定义,可以看到buildPhases,buildConfiguration.
再顺着找可以看到cocoapods, copy resource等等定义.
/* Begin PBXNativeTarget section */
F7036F072511EC050031CE83 /* XXXXX */ = {
isa = PBXNativeTarget;
buildConfigurationList = F7036F212511EC080031CE83 /* Build configuration list for PBXNativeTarget "XXXXX" */;
buildPhases = (
2E9C3A1138A1AED934114EBC /* [CP] Check Pods Manifest.lock */,
F7036F042511EC050031CE83 /* Sources */,
F7036F052511EC050031CE83 /* Frameworks */,
F7036F062511EC050031CE83 /* Resources */,
34DB7EF940E3C9401AD2798F /* [CP] Embed Pods Frameworks */,
25DB3E569FEFB4DC91CF364D /* [CP] Copy Pods Resources */,
);
...