前言
我们知道,编程语言分为编译语言和解释语言。两者的执行过程不同。
编译语言是通过编译器将代码直接编写成机器码,然后直接在CPU上运行机器码的,这样能使得我们的app和手机都能效率更高,运行更快。C,C++,OC等语言,都是使用的编译器,生成相关的可执行文件。
解释语言使用的是解释器。解释器会在运行时解释执行代码,获取一段代码后就会将其翻译成目标代码(就是字节码(Bytecode)),然后一句一句地执行目标代码。也就是说是在运行时才去解析代码,比直接运行编译好的可执行文件自然效率就低,但是跑起来之后可以不用重启启动编译,直接修改代码即可看到效果,类似热更新,可以帮我们缩短整个程序的开发周期和功能更新周期。
iOS编译器
把一种编程语言(原始语言)转换为另一种编程语言(目标语言)的程序叫做编译器
编译器的组成:前端和后端
- 前端负责词法分析,语法分析,生成中间代码;
- 后端以中间代码作为输入,进行行架构无关的代码优化,接着针对不同架构生成不同的机器码;
Objective C/C/C++使用的编译器前端是clang,后端都是LLVM
编译过程
先看下流程
我先写端代码
#import
#define DEBUG 1
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
#ifdef DEBUG
printf("hello debug\n");
#else
printf("hello world\n");
#endif
NSLog(@"Hello, World!");
}
return 0;
}
一、预处理(preprocessor)
使用命令:
xcrun clang -E main.m
生成代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
printf("hello debug\n");
NSLog(@"Hello, World!");
}
return 0;
}
可以看到,在预处理的时候,注释被删除,条件编译被处理。
二、词法分析(lexical anaysis).
词法分析器读入源文件的字符流,将他们组织称有意义的词素(lexeme)序列,对于每个词素,此法分析器产生词法单元(token)作为输出。
$ xcrun clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m 生成代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
// ins' Loc=
int 'int' [StartOfLine] Loc=
identifier 'main' [LeadingSpace] Loc=
l_paren '(' Loc=
int 'int' Loc=
identifier 'argc' [LeadingSpace] Loc=
comma ',' Loc=
const 'const' [LeadingSpace] Loc=
char 'char' [LeadingSpace] Loc=
star '*' [LeadingSpace] Loc=
identifier 'argv' [LeadingSpace] Loc=
l_square '[' Loc=
r_square ']' Loc=
r_paren ')' Loc=
...
看出词法分析多了Loc来记录位置
`-FunctionDecl 0x106c203f0 line:11:5 main 'int (int, const char **)'
|-ParmVarDecl 0x106c20220 col:14 argc 'int'
|-ParmVarDecl 0x106c202e0 col:33 argv 'const char **':'const char **'
`-CompoundStmt 0x106c206f8
|-ObjCAutoreleasePoolStmt 0x106c206b0
| `-CompoundStmt 0x106c20690
| |-CallExpr 0x106c20520 'int'
| | |-ImplicitCastExpr 0x106c20508 'int (*)(const char *, ...)'
| | | `-DeclRefExpr 0x106c20498 'int (const char *, ...)' Function 0x7fd6618d23b0 'printf' 'int (const char *, ...)'
| | `-ImplicitCastExpr 0x106c20560 'const char *'
| | `-ImplicitCastExpr 0x106c20548 'char *'
| | `-StringLiteral 0x106c204b8 'char [13]' lvalue "hello debug\n"
| `-CallExpr 0x106c20650 'void'
| |-ImplicitCastExpr 0x106c20638 'void (*)(id, ...)'
| | `-DeclRefExpr 0x106c20578 'void (id, ...)' Function 0x7fd661b80ff0 'NSLog' 'void (id, ...)'
| `-ImplicitCastExpr 0x106c20678 'id':'id'
| `-ObjCStringLiteral 0x106c205c0 'NSString *'
| `-StringLiteral 0x106c20598 'char [14]' lvalue "Hello, World!"
`-ReturnStmt 0x106c206e8
`-IntegerLiteral 0x106c206c8 'int' 0
这一步是把词法分析生成的标记流,解析成一个抽象语法树(abstract syntax tree -- AST),同样地,在这里面每一节点也都标记了其在源码中的位置。
四、静态分析
把源码转化为抽象语法树之后,编译器就可以对这个树进行分析处理。静态分析会对代码进行错误检查,如出现方法被调用但是未定义、定义但是未使用的变量等,以此提高代码质量。当然,还可以通过使用 Xcode 自带的静态分析工具(Product -> Analyze)
- 类型检查在此阶段clang会做检查,最常见的是检查程序是否发送正确的消息给正确的对象,是否在正确的值上调用了正常函数。如果你给一个单纯的 NSObject* 对象发送了一个 hello 消息,那么 clang 就会报错,同样,给属性设置一个与其自身类型不相符的对象,编译器会给出一个可能使用不正确的警告。
- 其他分析ObjCUnusedIVarsChecker.cpp是用来检查是否有定义了,但是从未使用过的变量。ObjCSelfInitChecker.cpp是检查在 你的初始化方法中中调用 self 之前,是否已经调用 [self initWith...] 或 [super init] 了。
更多请参考:clang 静态分析
五、中间代码生成和优化
使用命令:
clang -O3 -S -emit-llvm main.m -o main.ll
生成main.ll文件,打开并查看转化结果
ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.14.0"
%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }
@__CFConstantStringClassReference = external global [0 x i32]
@.str.1 = private unnamed_addr constant [14 x i8] c"Hello, World!\00", section "__TEXT,__cstring,cstring_literals", align 1
@_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 1992, i8* getelementptr inbounds ([14 x i8], [14 x i8]* @.str.1, i32 0, i32 0), i64 13 }, section "__DATA,__cfstring", align 8
@str = private unnamed_addr constant [12 x i8] c"hello debug\00", align 1
; Function Attrs: ssp uwtable
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
%3 = tail call i8* @llvm.objc.autoreleasePoolPush() #1
%4 = tail call i32 @puts(i8* getelementptr inbounds ([12 x i8], [12 x i8]* @str, i64 0, i64 0))
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*))
tail call void @llvm.objc.autoreleasePoolPop(i8* %3)
ret i32 0
}
; Function Attrs: nounwind
declare i8* @llvm.objc.autoreleasePoolPush() #1
declare void @NSLog(i8*, ...) local_unnamed_addr #2
; Function Attrs: nounwind
declare void @llvm.objc.autoreleasePoolPop(i8*) #1
; Function Attrs: nounwind
declare i32 @puts(i8* nocapture readonly) local_unnamed_addr #1
attributes #0 = { ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { nounwind }
attributes #2 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+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", [2 x i32] [i32 10, i32 15]}
!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 11.0.0 (clang-1100.0.33.12)"}
接下来 LLVM 会对代码进行编译优化,例如针对全局变量优化、循环优化、尾递归优化等,最后输出汇编代码。
六、生成汇编
xcrun clang -S -o - main.m | open -f 生成代码如下:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 14 sdk_version 10, 15
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
movl $0, -4(%rbp)
movl %edi, -8(%rbp)
movq %rsi, -16(%rbp)
callq _objc_autoreleasePoolPush
leaq L_.str(%rip), %rdi
movq %rax, -24(%rbp) ## 8-byte Spill
movb $0, %al
callq _printf
leaq L__unnamed_cfstring_(%rip), %rsi
movq %rsi, %rdi
movl %eax, -28(%rbp) ## 4-byte Spill
movb $0, %al
callq _NSLog
movq -24(%rbp), %rdi ## 8-byte Reload
callq _objc_autoreleasePoolPop
xorl %eax, %eax
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "hello debug\n"
L_.str.1: ## @.str.1
.asciz "Hello, World!"
.section __DATA,__cfstring
.p2align 3 ## @_unnamed_cfstring_
L__unnamed_cfstring_:
.quad ___CFConstantStringClassReference
.long 1992 ## 0x7c8
.space 4
.quad L_.str.1
.quad 13 ## 0xd
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输出目标文件(object file)。
xcrun clang -fmodules -c main.m -o main.o
里面都是二进制文件
七、链接
连接器把编译产生的.o文件和(dylib,a,tbd)文件,生成一个mach-o文件。
$ xcrun clang main.o -o main
就生成一个mach o格式的可执行文件 我们执行下:
Mac-mini-2:测试mac jxq$ file main
main: Mach-O 64-bit executable x86_64
Mac-mini-2:测试mac jxq$ ./main
hello debug
2020-01-15 15:10:32.430 main[4269:156652] Hello, World!
Mac-mini-2:测试mac jxq$
在用nm命令,查看可执行文件的符号表:
Mac-mini-2:测试mac jxq$ nm -nm main
(undefined) external _NSLog (from Foundation)
(undefined) external ___CFConstantStringClassReference (from CoreFoundation)
(undefined) external _objc_autoreleasePoolPop (from libobjc)
(undefined) external _objc_autoreleasePoolPush (from libobjc)
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000ef0 (__TEXT,__text) external _main
至此,编译过程全部结束,生成了可执行文件Mach-O
那么
编译时链接器做了什么
Mach-O 文件里面的内容,主要就是代码和数据:代码是函数的定义;数据是全局变量的定义,包括全局变量的初始值。不管是代码还是数据,它们的实例都需要由符号将其关联起来。
为什么呢?因为 Mach-O 文件里的那些代码,比如 if、for、while 生成的机器指令序列,要操作的数据会存储在某个地方,变量符号就需要绑定到数据的存储地址。你写的代码还会引用其他的代码,引用的函数符号也需要绑定到该函数的地址上。
链接器的作用,就是完成变量、函数符号和其地址绑定这样的任务。而这里我们所说的符号,就可以理解为变量名和函数名。
为什么要进行符号绑定
- 如果地址和符号不做绑定的话,要让机器知道你在操作什么内存地址,你就需要在写代码时给每个指令设好内存地址。
- 可读性和可维护性都会很差,修改代码后对需要对地址的进行维护
- 需要针对不同的平台写多份代码,本可以通过高级语言一次编译成多份
- 相当于直接写汇编
为什么还要把项目中的多个 Mach-O 文件合并成一个
项目中文件之间的变量和接口函数都是相互依赖的,所以这时我们就需要通过链接器将项目中生成的多个 Mach-O 文件的符号和地址绑定起来。
没有这个绑定过程的话,单个文件生成的 Mach-O
文件是无法正常运行起来的。因为,如果运行时碰到调用在其他文件中实现的函数的情况时,就会找不到这个调用函数的地址,从而无法继续执行。
链接器在链接多个目标文件的过程中,会创建一个符号表,用于记录所有已定义的和所有未定义的符号。
- 链接时如果出现相同符号的情况,就会出现“ld: dumplicate symbols”的错误信息;
- 如果在其他目标文件里没有找到符号,就会提示“Undefined symbols”的错误信息。
链接器如何去除无用函数,保证Mach-O大小
链接器在整理函数的调用关系时,会以 main 函数为源头,跟随每个引用,并将其标记为 live。跟随完成后,那些未被标记 live 的函数,就是无用函数。然后,链接器可以通过打开 Dead code stripping 开关,来开启自动去除无用代码的功能。并且,这个开关是默认开启的。
总结
ios编译过程就是生成mach—o文件的过程,在这个过程中,进行了一系列的语法检查,代码优化,符号绑定等工作
文章原作者 点击这里