iOS编译过程及原理

前言

一般可以将编程语言分为两种,编译语言和直译式语言。
像C++,Objective C都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码,机器码可以直接在CPU上执行,所以执行效率较高。
像JavaScript,Python都是直译式语言。直译式语言不需要经过编译的过程,而是在执行的时候通过一个中间的解释器将代码解释为CPU可以执行的代码。所以,较编译语言来说,直译式语言效率低一些,但是编写的更灵活,也就是为啥JS大法好。
iOS开发目前的常用语言是:Objective和Swift。二者都是编译语言,换句话说都是需要编译才能执行的。二者的编译都是依赖于Clang + LLVM. 篇幅限制,本文只关注Objective C,因为原理上大同小异。

Clang和LLVM

不管是OC还是Swift,都是采用Clang作为编译器前端,LLVM(Low level vritual machine)作为编译器后端。所以简单的编译过程如下:

Clang编译过程

预处理: 预处理器会处理源文件中的宏定义,将代码中的宏用其对应定义的具体内容进行替换,删除注释,展开头文件,产生 .i 文件。

词法分析:预处理完成了以后,开始词法分析,这里会把代码切成一个个 Token,比如大小括号,等于号还有字符串等。

语法分析: 语法分析,在 Clang 中由 Parser 和 Sema 两个模块配合完成,验证语法是否正确,根据当前语言的语法,生成语意节点,并将所有节点组合成抽象语法树 AST。

静态分析: 一旦编译器把源码生成了抽象语法树,编译器可以对这棵树做分析处理,以找出代码中的错误,比如类型检查:即检查程序中是否有类型错误。例如:如果代码中给某个对象发送了一个消息,编译器会检查这个对象是否实现了这个消息(函数、方法)。此外,clang 对整个程序还做了其它更高级的一些分析,以确保程序没有错误。

类型检查:一般会把类型检查分为两类:动态的和静态的。动态的在运行时做检查,静态的在编译时做检查。以往,编写代码时可以向任意对象发送任何消息,在运行时,才会检查对象是否能够响应这些消息。由于只是在运行时做此类检查,所以叫做动态类型。至于静态类型,是在编译时做检查。当在代码中使用 ARC 时,编译器在编译期间,会做许多的类型检查:因为编译器需要知道哪个对象该如何使用。

目标代码的生成与优化: CodeGen 负责将语法树 AST 丛顶至下遍历,翻译成 LLVM IR 中间码,LLVM IR 中间码编译过程的前端的输出后端的输入。编译器后端主要包括代码生成器、代码优化器。代码生成器将中间代码转换为目标代码,代码优化器主要是进行一些优化,比如删除多余指令,选择合适寻址方式等,如果开启了 bitcode 苹果会做进一步的优化,有新的后端架构还是可以用这份优化过的 bitcode 去生成。优化中间代码生成输出汇编代码,把之前的 .i 文件转换为汇编语言,产生 .s 文件.

LLVM编译过程

汇编: 目标代码需要经过汇编器处理,把汇编语言文件转换为机器码文件,产生 .o 文件。

链接: 对 .o 文件中的对于其他的库的引用的地方进行引用,生成最后的可执行文件(同时也包括多个 .o 文件进行 link)。链接又分为静态链接和动态链接。

  • 静态链接:在编译链接期间发挥作用,把目标文件和静态库一起链接形成可执行文件.
  • 动态链接:链接过程推迟到运行时再进行.

如果多个程序都用到了一个库,那么每个程序都要将其链接到可执行文件中,非常冗余,动态链接的话,多个程序可以共享同一段代码,不需要在磁盘上存多份拷贝,但是动态链接发生在启动或运行时,增加了启动时间,造成一些性能的影响。
静态库不方便升级,必须重新编译,动态库的升级更加方便。

代码案列

上面总结了编译的流程,接下来我们用实际的代码来看看具体的转化流程.首先创建一个main.m文件

#import 
//来个注释
#define DEBUG 1
int main(){
    #ifdef DEBUG
    NSLog(@"DEBUG模式");
    #else
    NSLog(@"RELEASE模式");
    #endif
    return 0;
}
预处理

预处理器会处理源文件中的宏定义,将代码中的宏用其对应定义的具体内容进行替换,删除注释,展开头文件,产生 .i 文件。
'#import '这一行是告诉预处理器将这行用Foundation.h中的内容替换.这个过程是递归的,因为Foundation.h中也import了其他文件.使用clang查看预处理结果

xcrun clang -E main.m

与处理后的文件会有很多代码.其中基本上都是引用的其他文件然后被递归替换的内容.划到最底部可以看到main函数.

int main(){
    NSLog(@"DEBUG模式");
    return 0;
}

同时,我们也可以发现,在这个阶段,我们所写的注释被删除,条件编译也被处理了.

词法分析

词法分析器读入源文件的字符流,将他们组织称有意义的词素(lexeme)序列,对于每个词素,此法分析器产生词法单元(token)作为输出。

$ xcrun clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

输出

annot_module_include '#import 
//'     Loc=
int 'int'    [StartOfLine]  Loc=
identifier 'main'    [LeadingSpace] Loc=
l_paren '('     Loc=
r_paren ')'     Loc=
l_brace '{'     Loc=
identifier 'NSLog'   [StartOfLine] [LeadingSpace]   Loc=
l_paren '('     Loc=
at '@'      Loc=
string_literal '"DEBUG模式"'      Loc=
r_paren ')'     Loc=
semi ';'        Loc=
return 'return'  [StartOfLine] [LeadingSpace]   Loc=
numeric_constant '0'     [LeadingSpace] Loc=
semi ';'        Loc=
r_brace '}'  [StartOfLine]  Loc=
eof ''      Loc=

Loc=标示这个token位于源文件main.m的第2行,从第1个字符开始。保存token在源文件中的位置是方便后续clang分析的时候能够找到出错的原始位置。

语法分析

语法分析,在 Clang 中由 Parser 和 Sema 两个模块配合完成,验证语法是否正确,根据当前语言的语法,生成语意节点,并将所有节点组合成抽象语法树 AST.简单点来说,就是将词法分析的Token流会被解析成一颗抽象语法树.

$ xcrun clang -fsyntax-only -Xclang -ast-dump main.m | open -f

得到的AST结构,部分如下

�[0;34m|       |-�[0m�[0;32mBuiltinType�[0m�[0;33m 0x7fa22903ae60�[0m �[0;32m'void'�[0m
�[0;34m|       |-�[0m�[0;32mAttributedType�[0m�[0;33m 0x7fa22a204fc0�[0m �[0;32m'id _Nullable'�[0m sugar
�[0;34m|       | |-�[0m�[0;32mTypedefType�[0m�[0;33m 0x7fa22a204310�[0m �[0;32m'id'�[0m sugar
�[0;34m|       | | |-�[0m�[0;1;32mTypedef�[0m�[0;33m 0x7fa22903b898�[0m�[0;1;36m 'id'�[0m
�[0;34m|       | | `-�[0m�[0;32mObjCObjectPointerType�[0m�[0;33m 0x7fa22903b840�[0m �[0;32m'id'�[0m
�[0;34m|       | |   `-�[0m�[0;32mObjCObjectType�[0m�[0;33m 0x7fa22903b810�[0m �[0;32m'id'�[0m
�[0;34m|       | `-�[0m�[0;32mTypedefType�[0m�[0;33m 0x7fa22a204310�[0m �[0;32m'id'�[0m sugar
�[0;34m|       |   |-�[0m�[0;1;32mTypedef�[0m�[0;33m 0x7fa22903b898�[0m�[0;1;36m 'id'�[0m
�[0;34m|       |   `-�[0m�[0;32mObjCObjectPointerType�[0m�[0;33m 0x7fa22903b840�[0m �[0;32m'id'�[0m
�[0;34m|       |     `-�[0m�[0;32mObjCObjectType�[0m�[0;33m 0x7fa22903b810�[0m �[0;32m'id'�[0m
�[0;34m|       `-�[0m�[0;32mAttributedType�[0m�[0;33m 0x7fa22a3925f0�[0m �[0;32m'NSError * _Nullable'�[0m sugar
�[0;34m|         |-�[0m�[0;32mObjCObjectPointerType�[0m�[0;33m 0x7fa22a3925b0�[0m �[0;32m'NSError *'�[0m
�[0;34m|         | `-�[0m�[0;32mObjCInterfaceType�[0m�[0;33m 0x7fa22a103b30�[0m �[0;32m'NSError'�[0m
�[0;34m|         |   `-�[0m�[0;1;32mObjCInterface�[0m�[0;33m 0x7fa22a527de0�[0m�[0;1;36m 'NSError'�[0m
�[0;34m|         `-�[0m�[0;32mObjCObjectPointerType�[0m�[0;33m 0x7fa22a3925b0�[0m �[0;32m'NSError *'�[0m
�[0;34m|           `-�[0m�[0;32mObjCInterfaceType�[0m�[0;33m 0x7fa22a103b30�[0m �[0;32m'NSError'�[0m
�[0;34m|             `-�[0m�[0;1;32mObjCInterface�[0m�[0;33m 0x7fa22a527de0�[0m�[0;1;36m 'NSError'�[0m

有了抽象语法树,Clang就可以对这个树进行分析,找出代码中的错误。Clang Static Analyzer是开源编译器前端clang中内置的针对C,C++和Objective-C源代码的静态分析工具,能提供普通warning之外的检查,涵盖内存操作,安全等方面。这部分功能可通过clang --analyze命令或者库文件等方式调用.由于需要实现checker.这一步我们先过掉.有兴趣的话可以在做研究.

目标代码的生成与优化

CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出,也是后端的输入。 Objective C代码也在这一步会进行runtime的桥接:property合成,ARC处理等。

clang -S -fobjc-arc -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.15.0"

%struct.__NSConstantString_tag = type { i32*, i32, i8*, i64 }

@__CFConstantStringClassReference = external global [0 x i32]
@.str = private unnamed_addr constant [8 x i16] [i16 68, i16 69, i16 66, i16 85, i16 71, i16 27169, i16 24335, i16 0], section "__TEXT,__ustring", align 2
@_unnamed_cfstring_ = private global %struct.__NSConstantString_tag { i32* getelementptr inbounds ([0 x i32], [0 x i32]* @__CFConstantStringClassReference, i32 0, i32 0), i32 2000, i8* bitcast ([8 x i16]* @.str to i8*), i64 7 }, section "__DATA,__cfstring", align 8 #0

; Function Attrs: noinline optnone ssp uwtable
define i32 @main() #1 {
  %1 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  notail call void (i8*, ...) @NSLog(i8* bitcast (%struct.__NSConstantString_tag* @_unnamed_cfstring_ to i8*))
  ret i32 0
}

declare void @NSLog(i8*, ...) #2

attributes #0 = { "objc_arc_inert" }
attributes #1 = { noinline optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "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,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "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,+cx8,+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", [3 x i32] [i32 10, i32 15, i32 4]}
!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.3 (clang-1103.0.32.62)"}

中间代码生成后,需要将LLVM代码转化为汇编语言,生成.s文件交给后面的汇编器处理.

clang -S -fobjc-arc main.m -o main.s

使用上面命令行的到汇编文件,部分内容如下

.section    __TEXT,__text,regular,pure_instructions
    .build_version macos, 10, 15    sdk_version 10, 15, 4
    .globl  _main                   ## -- Begin function main
    .p2align    4, 0x90
_main:                                  ## @main
    .cfi_startproc
## %bb.0:
汇编

目标代码需要经过汇编器处理,把汇编语言文件转换为机器码文件,产生 .o 文件(object file)。

clang -fmodules -c main.m -o main.o

使用命令行查看main.o文件

nm -nm main.o

输出

(undefined) external _NSLog
                 (undefined) external ___CFConstantStringClassReference
0000000000000000 (__TEXT,__text) external _main
0000000000000028 (__TEXT,__ustring) non-external l_.str

这里可以看到_NSLog是一个是undefined external的。undefined表示在当前文件暂时找不到符号_NSLog,而external表示这个符号是外部可以访问的,对应表示文件私有的符号是non-external。

链接生成可执行文件

拿到.o机器码文件后,需要对 .o 文件中的对于其他的库的引用的地方进行引用,生成最后的match-o可执行文件.

clang main.o -o main

当然,这个命令行是封装完成的.内部是使用

cc main.o -framework Foundation

来链接其他库的.
最终可以拿到我们的执行文件.运行 ./
得到输出结果 "DEBUG模式".
我们查看可执行文件的符号表

                 U _NSLog
                 U ___CFConstantStringClassReference
0000000100002008 d __dyld_private
0000000100000000 T __mh_execute_header
0000000100000f50 T _main
                 U dyld_stub_binder

关于match-o文件里的符号表的解释,会专门在出一篇文章来做解释.

从上我们可以大致了解了,iOS代码带match-o可执行文件的整个过程.

了解这些知识后,在深入研究可以解决很多问题,譬如:

  • 自动化打包;
  • 在拿到AST后对代码规范进行review;
  • 提高项目编译速度
    ...

你可能感兴趣的:(iOS编译过程及原理)