美文网首页
iOS编译过程及原理

iOS编译过程及原理

作者: 丁勒个东 | 来源:发表于2020-07-17 16:55 被阅读0次

    前言

    一般可以将编程语言分为两种,编译语言直译式语言
    像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 <Foundation/Foundation.h>
    //来个注释
    #define DEBUG 1
    int main(){
        #ifdef DEBUG
        NSLog(@"DEBUG模式");
        #else
        NSLog(@"RELEASE模式");
        #endif
        return 0;
    }
    
    预处理

    预处理器会处理源文件中的宏定义,将代码中的宏用其对应定义的具体内容进行替换,删除注释,展开头文件,产生 .i 文件。
    '#import <Foundation/Foundation.h>'这一行是告诉预处理器将这行用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 <Foundation/Foundation.h>
    //'     Loc=<main.m:2:1>
    int 'int'    [StartOfLine]  Loc=<main.m:5:1>
    identifier 'main'    [LeadingSpace] Loc=<main.m:5:5>
    l_paren '('     Loc=<main.m:5:9>
    r_paren ')'     Loc=<main.m:5:10>
    l_brace '{'     Loc=<main.m:5:11>
    identifier 'NSLog'   [StartOfLine] [LeadingSpace]   Loc=<main.m:7:5>
    l_paren '('     Loc=<main.m:7:10>
    at '@'      Loc=<main.m:7:11>
    string_literal '"DEBUG模式"'      Loc=<main.m:7:12>
    r_paren ')'     Loc=<main.m:7:25>
    semi ';'        Loc=<main.m:7:26>
    return 'return'  [StartOfLine] [LeadingSpace]   Loc=<main.m:11:5>
    numeric_constant '0'     [LeadingSpace] Loc=<main.m:11:12>
    semi ';'        Loc=<main.m:11:13>
    r_brace '}'  [StartOfLine]  Loc=<main.m:12:1>
    eof ''      Loc=<main.m:12:2>
    

    Loc=<main.m:2:1>标示这个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编译过程及原理

          本文链接:https://www.haomeiwen.com/subject/airghktx.html