编译器的组成部分
传统的编译器通常分为三个部分,分别为:前端(frontEnd)
,优化器(Optimizer)
和后端(backEnd)
,在编译过程中,各自执行不同的功能:
- 前端(frontEnd)主要负责词法分析,语法分析和语义分析,将源代码转化为抽象语法树(AST),最后生成中间代码;
- 优化器(Optimizer)则是在前端的基础上,对生成的中间代码进行优化;
- 后端(backEnd)则是将已经优化的中间代码,根据不同平台架构转化为各自平台的机器代码;
编译器的种类
GCC
- GCC(GNU Compiler Collection,GNU编译器套装),是一套由 GNU 开发的编程语言编译器,GCC 原名为 GNU C 语言编译器,因为它原本只能处理 C语言,GCC 快速演进,变得可处理 C++、Fortran、Pascal、Objective-C、Java, 以及 Ada 等他语言;
LLVM
- LLVM是一个完整的编译器(compiler)框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本;
- 在理解LLVM时,我们可以认为它包括了一个狭义的LLVM和一个广义的LLVM;
- 广义的LLVM其实就是指整个LLVM编译器架构,包括了前端、后端、优化器、众多的库函数以及其他的模块;
- 狭义的LLVM其实就是聚焦于编译器后端功能(代码生成、代码优化、JIT等)的一系列模块和库;
Clang
- Clang是LLVM编译系统的前端,是GCC的替代品,其可以看成是LLVM的子集,相比于GCC编译器Clang功能更加强大;
- 其速度快,占用内存小,诊断信息可读性强,兼容性好,Clang有静态分析而GCC没有,Clang使用BSD许可证,GCC使用GPL许可证;
- 下面用一张图来表示Clang与LLVM之间的关系:
- 从图中可以看出Clang其实大致上可以看成是LLVM编译器架构的前端,主要处理一些和具体机器无关的针对语言的分析操作;编译器的优化器部分和后端部分其实就是我们之前谈到的LLVM后端(狭义的LLVM),而整体的Compiler架构就是LLVM架构;
iOS编译过程
- Xcode的默认使用的编译器是 clang,clang首先会对 Objective-C 代码做预处理,分析检查,然后将其转换为低级的类汇编代码;
- 下面通过具体的代码来演示iOS的编译过程:
第一步:预处理(preprocessor)
- C语言代码如下所示:
#import
#define DEBUG 1
int main(int argc, const char * argv[]) {
@autoreleasepool {
//insert code ...
#ifdef DEBUG
printf("hello debug\n");
#else
printf("hello world\n");
#endif
NSLog(@"Hello, World!");
}
return 0;
}
- 终端cd到指定文件路径,然后执行
xcrun clang -E main.m
,最后终端输出的代码如下所示:
int main(int argc, const char * argv[]) {
@autoreleasepool {
printf("hello debug\n");
NSLog(@"Hello, World!");
}
return 0;
}
- 可以看到,
在代码预处理的时候,注释被删除,条件编译指令被处理
; - 代码预处理完成之后的具体编译流程如下图所示:
第二步:词法分析
- 词法分析:通过
词法分析器
读入源文件的字符流,然后将他们组织成有意义的词素(lexeme)序列,对于每个词素,词法分析器会生成对应的词法单元(token)作为输出,测试代码如下:
#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
int b = 20;
int c = a + b;
NSLog(@" c = %d",c);
}
return 0;
}
- 终端cd到指定文件路径,执行词法分析命令:
xcrun clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
最后终端输出的代码如下所示:
annot_module_include '#import
int main(int argc, const char * argv[]) {
@autoreleasepool {
int a = 10;
int b' 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=
l_brace '{' [LeadingSpace] Loc=
at '@' [StartOfLine] [LeadingSpace] Loc=
identifier 'autoreleasepool' Loc=
l_brace '{' [LeadingSpace] Loc=
int 'int' [StartOfLine] [LeadingSpace] Loc=
identifier 'a' [LeadingSpace] Loc=
equal '=' [LeadingSpace] Loc=
numeric_constant '10' [LeadingSpace] Loc=
semi ';' Loc=
int 'int' [StartOfLine] [LeadingSpace] Loc=
identifier 'b' [LeadingSpace] Loc=
equal '=' [LeadingSpace] Loc=
numeric_constant '20' [LeadingSpace] Loc=
semi ';' Loc=
int 'int' [StartOfLine] [LeadingSpace] Loc=
identifier 'c' [LeadingSpace] Loc=
equal '=' [LeadingSpace] Loc=
identifier 'a' [LeadingSpace] Loc=
plus '+' [LeadingSpace] Loc=
identifier 'b' [LeadingSpace] Loc=
semi ';' Loc=
identifier 'NSLog' [StartOfLine] [LeadingSpace] Loc=
l_paren '(' Loc=
at '@' Loc=
string_literal '" c = %d"' Loc=
comma ',' Loc=
identifier 'c' 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=
- 看到词法分析器将代码中的
关键字,操作符,变量,分号,括号
全部分割开来成为独立的词法单元
;
第三步:语法分析
- 词法分析获取的词法单元(Token)流,会被解析成一棵抽象语法树(abstract syntax tree - AST),且会对语法树进行代码静态分析,校验语法是否错误;
- 终端执行语法分析命令:
clang -Xclang -ast-dump -fsyntax-only main.m
,终端输出结果如下所示:
第四步:生成中间代码文件
- 终端执行命令:
clang -O3 -S -emit-llvm main.m -o main.ll
,本地生成一个main.ll文件,用Xcode打开其内容如下:
; 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.15.0"
%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }
@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [8 x i8] c" c = %d\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 ([8 x i8], [8 x i8]* @.str, i32 0, i32 0), i64 7 }, section "__DATA,__cfstring", align 8
; Function Attrs: ssp uwtable
define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
%3 = tail call i8* @llvm.objc.autoreleasePoolPush() #1
notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*), i32 30)
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
attributes #0 = { ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "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" "probe-stack"="___chkstk_darwin" "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" "darwin-stkchk-strong-link" "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" "probe-stack"="___chkstk_darwin" "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.17)"}
第五步:优化器(Optimizer) 优化中间代码
- 全局变量优化、循环优化、尾递归优化等,如果开启了
bitcode
苹果会做进一步的优化;
第六步:生成汇编文件
- 终端执行命令:
clang -S -fobjc-arc main.m -o main.s
,本地生成一个main.s文件,用Xcode打开其内容如下:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15 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 $48, %rsp
movl $0, -4(%rbp)
movl %edi, -8(%rbp)
movq %rsi, -16(%rbp)
callq _objc_autoreleasePoolPush
leaq L__unnamed_cfstring_(%rip), %rsi
movl $10, -20(%rbp)
movl $20, -24(%rbp)
movl -20(%rbp), %edi
addl -24(%rbp), %edi
movl %edi, -28(%rbp)
movl -28(%rbp), %edi
movl %edi, -32(%rbp) ## 4-byte Spill
movq %rsi, %rdi
movl -32(%rbp), %esi ## 4-byte Reload
movq %rax, -40(%rbp) ## 8-byte Spill
movb $0, %al
callq _NSLog
movq -40(%rbp), %rdi ## 8-byte Reload
callq _objc_autoreleasePoolPop
xorl %eax, %eax
addq $48, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz " c = %d"
.section __DATA,__cfstring
.p2align 3 ## @_unnamed_cfstring_
L__unnamed_cfstring_:
.quad ___CFConstantStringClassReference
.long 1992 ## 0x7c8
.space 4
.quad L_.str
.quad 7 ## 0x7
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
第七步:生成目标文件(二进制文件) main.o
- 终端执行命令:
xcrun clang -fmodules -c main.m -o main.o
,本地生成一个main.o文件;
第八步:链接器 链接多个目标文件最终生成一个可执行的Mach-O文件
- 链接器把编译生成的所有 .o 文件和(dylib、a、tbd)文件结合,生成一个Mach-O文件(可执行文件);
- 使用终端命令:
xcrun clang main.o -o main
; - 在编译时,链接器的主要工作任务:
- 符号绑定:完成变量名与其地址,函数名与其地址之间的绑定;
- Mach-O文件中的主要内容是数据和代码,数据就是全局变量,代码就是函数, 全局变量和函数都存储在指定的位置,计算机指令通过符号去操作数据和调用函数,首先必须要进行符号的绑定,才能根据符号去操作数据和调用函数;
- 静态库的链接:将静态库文件直接打包进入Mach-O文件中;
- 生成的多个Mach-O文件最终合成一个Mach-O文件,项目中各个文件之间的函数存在相互调用,也就是说存在相互依赖,单个的Mach-O文件是无法执行成功的,因为当前文件中需要调用其他文件中的函数方法,最终由于找不到对应的函数而终止执行;所以我们需要把所有的Mach-O文件合并,最终生成一个可执行的Mach-O文件;
- 对于Mach-O文件中的动态库,在编译期只是对其进行引用并没有加载,等到了App运行时,dyld会对Mach-O文件中所有动态库的引用进行扫描,逐一加载链接,但不会打包进Mach-O文件中,这是与静态库最大的区别;
编译多个文件 -- 实战演练
- 首先创建一个C语言工程如下所示:
- 在终端依次输入:
xcrun clang -c main.m
xcrun clang -c YYPerson.m
- 在本地会生成两个目标文件 main.o 与YYPerson.o;
- 将多个编译之后的目标文件通过链接器进行链接,生成a.out可执行文件;
- 在终端中输入:xcrun clang main.o YYPerson.o -Wl,
xcrun —show-sdk-path
/System/Library/Frameworks/Foundation.framework/Foundation
- 在终端中输入以下命令进行查看:
xcrun nm -nm a.out
;
- undefined 符号表示的是该文件类未定义,所以在目标文件和 Foundation framework 动态库做链接处理时,链接器会尝试解析所有的undefined 符号;
- dylib 这种格式,表示是动态链接的,编译的时候不会被编译到执行文件中,在程序执行的时候才 link,这样就不用算到包大小里,而且不更新执行程序就能够更新库;