iOS底层编译过程

前言

我们知道,编程语言分为编译语言和解释语言。两者的执行过程不同。

编译语言是通过编译器将代码直接编写成机器码,然后直接在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文件的过程,在这个过程中,进行了一系列的语法检查,代码优化,符号绑定等工作

文章原作者 点击这里

你可能感兴趣的:(iOS底层编译过程)