美文网首页
iOS 底层探索:LLVM入门

iOS 底层探索:LLVM入门

作者: 欧德尔丶胡 | 来源:发表于2020-11-17 11:37 被阅读0次

    iOS 底层探索: 学习大纲 OC篇

    前言

    • 今天来学习一下牛逼的架构编译器LLVM

    学习大纲

    1. 简单了解编译器
    2. LLVM概述
    3. LLVM案例体验
    4. LLVM源码 & 编译流程

    准备

    一 、简单了解编译器

    百度百科

    1. 解释型语言编译型语言
      • 编译型语言:编译后输出的是指令(0、1组合),cpu可直接执行指令
      • 解释性语言:生成的是数据,不是0、1组合,机器也能直接识别

    python解释型语言,一边翻译一边执行。和js一样,机器可直接执行
    C语言编译型语言,不能直接执行,需要编译器将其转换成机器识别语言

    1. 编译器的作用,就是将高级语言转化为机器能够识别的语言(可执行文件)。

    2. 汇编指令

    • 早期科学家,使用0、1编码。 比如 00001111 对应 call, 00000111 对应bl。有了对应关系后。 再手敲0和1就有点难受了。于是写个中间解释器,我们只用输入call、bl这样的标记指令,经过解释器,变成0和1的组合,再交给机器去执行。 这就是汇编的由来。
    • 而基于汇编往上,再映射封装相关对应关系。就跨时代性的c语言,再往上层封装,就出现了高级语言oc、swift等语言。所以汇编执行快,因为它是直接转换为机器语言的。
    • 汇编的指令集,是针对同一操作系统而言,它不支持跨平台。机器指令是cpu的在识别。早期的计算机厂家非常多,虽然都用0和1的组合,但相同组合背后却是相应不同的指令。所以汇编无法跨平台不同操作系统下,汇编指令不同的。
    1. 案例创建体验
    • Python 案例:创建python文件夹,新建helloDemo.py文件,
      内容:
    print("hello")
    
    • 可以看出 Python文件内容 可直接执行

    • C 案例:vim创建helloDemo.c文件:
      内容:

     #include <stdio.h>
     int main(int a, char * argv[]) {
             printf("hello \n");
             return 0;
     }
    
    • clang helloDemo.c编译,生成a.out文件。file a.out查看文件:
    • 发现.out文件是:64位的Mach-O可执行文件,当前clang出来的是x86_64架构, mac电脑可读。 所以可以./a.out直接执行:

    • 可以看出 C 文件内容 需要编译

    二 、 LLVM概述

    • LLVM是架构编译器(compiler)的框架系统,以c++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼任已有脚本。
    • 2006年Chris Lattner加盟Apple Inc.并致力于LLVM在Apple开发体系中的应用。Apple也是LLVM计划的主要资助者。目前LLVM已经被苹果iOS开发工具、Xilinx Vivado、Facebook、Google等各大公司采用。

    1. 传统编译器的设计

    传统编译器的设计
    • 编译器前端(Frontend):

    编译器的前端任务是解析源代码。 会进行词法分析、语法分析、语义分析。检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree AST),LLVM前端还会生成中间代码(intermediate representation, IR)

    • 优化器(Optimizer)

    优化器负责各种优化。改善代码的运行时间,如消除冗余计算等

    • 后端(Backkend)/ 代码生成器(CodeGenerator)

    代码映射到目标指令集生成机器语言,并进行机器相关的代码优化(目标指不同操作系统)

    2. iOS的编译器架构
    Objective C / C / C++ 使用的编译器前端是Clang,Swift是swift,后端都是LLVM。

    3. LLVM的设计

    • 传统编译器(如CGG )的前端和后端没有完全分离,耦合在了一起,因而如果要支持一门新的语言或硬件平台,需要做大量的工作。

    • LLVM最重要的地方:支持多种语言多种硬件架构。使用通用代码表示形式:IR(用来在编译器中表示代码的形式)

    • LLVM可以为任何编程语言独立编写前端,也可以为任何硬件架构独立编写后端.

    • 所以LLVM不是一个简单的编译器,而是架构编译器,可以兼容所有前端和后端。

    • LLVM同时支持 AOT 预先编译和 JIT即时编译

    • 不同的前端后端使用统一的中间代码LLVM Intermediate Representation (LLVM IR)
        1. LLVM IR格式以 .ll结尾、以 .bc 的二进制格式结尾、内存格式
        1. Bitcode(Xcode 7之后)就是以.bc结尾的中间代码,是LLVM-IR在磁盘上的一种二进制表示形式。例如:clang -c -emit-llvm xxxx.m 生成 xxxx. bc
        1. 如果要转换成文本格式查看,例如:llvm-dis xxxx.bc -o xxxx.ll
        1. 苹果单独对 Bitcode 进行了额外的优化.
        • i) 应用上传到 AppStore时,Xcode会将程序对应的 Bitcode一起上传;
        • ii) AppStore会将 Bitcode重新编译为可执行程序,供用户下载;
        • iii) Bitcode被Xcode打包成 xar文档,嵌入的 MachO中。
    • 如果需要支持一种新的编程语言、硬件设备,那么只需要实现一个新的前后端

    4. Clang简介

    ClangLLVM项目的一个子项目。基于LLVM架构的轻量级编辑器,诞生之初就是为了替代GCC,提供更快的编译速度。 他是负责编译C、C++、Objecte-C语言的编译器,它属于整个LLVM架构中的编译器前端

    对于开发者而言,研究Clang可以给我们带来很多好处。

    三 、 LLVM案例体验

    • 新建一个Mac OS命令行工程:

    • 没有改动代码

    3.1 编译流程

    • cd到main.m的文件夹。使用下面命令查看main.m的编译步骤:
    clang -ccc-print-phases main.m
    

    编译流程分为以下7步

    • 0: input, "main.m", objective-c
      输入文件:找到源文件
    • 1: preprocessor, {0}, objective-c-cpp-output
      预处理:宏的展开,头文件的导入
    • 2: compiler, {1}, ir
      编译:词法、语法、语义分析,最终生成IR
    • 3: backend, {2}, assembler ()
      汇编: LLVM通过一个个的Pass去优化,每个Pass做一些事,最后生成汇编代码
    • 4: assembler, {3}, object
      目标文件
    • 5: linker, {4}, image
      链接: 链接需要的动态库和静态库,生成可执行文件
    • 6: bind-arch, "x86_64", {5}, image
      架构可执行文件:通过不同架构,生成对应的可执行文件

    optimizer优化没有作为一个独立阶段,在编译阶段内部完成

    3.2 预处理阶段

    • main.m中准备测试代码:
    #import <stdio.h>
    #define C 30
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            int a = 10;
            int b = 20;
            printf("%d", a + b + C);
        }
        return 0;
    }
    
    
    • clang预编译输出main2.m文件:
    clang -E main.m >> main2.m
    
    
    • 打开main2.m,有575行。其中大部分是stdio库的代码:

    • 我们发现测试代码中的宏C,在预编译阶段完成了替换,变成了30

    预编译阶段: 1. 导入头文件 2.替换宏

    • 修改测试代码,给int类型取个别名HT_INT_64,再次预编译处理
    #define C 30
    
    typedef int HT_INT_64;
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            HT_INT_64 a = 10;
            HT_INT_64 b = 20;
            printf("%d", a + b + C);
        }
        return 0;
    }
    
    
    • 发现typedef不会被替换

    使用安全拓展:

    1. 使用define重要方法名称进行替换。比如#define Pay XXXTest这样开发者使用宏Pay开发舒服,但是被hank时,实际代码是XXXTest,不容易被察觉。
      #define真实内容不应该写成乱码,最好弄成系统类似名称或其他不经意名称。这样才容易忽视安全级别更高 😃)
    2. typedef有一种掩人耳目的效果。define只影响预处理期。

    3.3 编译阶段

    3.3.1 词法分析

    • 编译main.m文件:
    clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
    
    • 词法分析,就是根据空格括号这些将代码拆分成一个个Token。标注了位置第几行第几个字符开始的。
    Last login: Mon Nov 16 16:43:13 on ttys000
    ios@HJ ~ % cd /Users/ios/Desktop/学习资料/hu/TestDemo/TestDemo 
    ios@HJ TestDemo % clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
    
    annot_module_include '#import <Foundation/Foundation.h>
    #define C 30
    
    int main(int argc, const char * argv[]) {
        @autoreleasepoo'        Loc=<main.m:8:1>
    int 'int'    [StartOfLine]  Loc=<main.m:11:1>
    identifier 'main'    [LeadingSpace] Loc=<main.m:11:5>
    l_paren '('     Loc=<main.m:11:9>
    int 'int'       Loc=<main.m:11:10>
    identifier 'argc'    [LeadingSpace] Loc=<main.m:11:14>
    comma ','       Loc=<main.m:11:18>
    const 'const'    [LeadingSpace] Loc=<main.m:11:20>
    char 'char'  [LeadingSpace] Loc=<main.m:11:26>
    star '*'     [LeadingSpace] Loc=<main.m:11:31>
    identifier 'argv'    [LeadingSpace] Loc=<main.m:11:33>
    l_square '['        Loc=<main.m:11:37>
    r_square ']'        Loc=<main.m:11:38>
    r_paren ')'     Loc=<main.m:11:39>
    l_brace '{'  [LeadingSpace] Loc=<main.m:11:41>
    at '@'   [StartOfLine] [LeadingSpace]   Loc=<main.m:12:5>
    identifier 'autoreleasepool'        Loc=<main.m:12:6>
    l_brace '{'  [LeadingSpace] Loc=<main.m:12:22>
    int 'int'    [StartOfLine] [LeadingSpace]   Loc=<main.m:13:9>
    identifier 'a'   [LeadingSpace] Loc=<main.m:13:13>
    equal '='    [LeadingSpace] Loc=<main.m:13:15>
    numeric_constant '10'    [LeadingSpace] Loc=<main.m:13:17>
    semi ';'        Loc=<main.m:13:19>
    int 'int'    [StartOfLine] [LeadingSpace]   Loc=<main.m:14:9>
    identifier 'b'   [LeadingSpace] Loc=<main.m:14:13>
    equal '='    [LeadingSpace] Loc=<main.m:14:15>
    numeric_constant '20'    [LeadingSpace] Loc=<main.m:14:17>
    semi ';'        Loc=<main.m:14:19>
    identifier 'printf'  [StartOfLine] [LeadingSpace]   Loc=<main.m:15:9>
    l_paren '('     Loc=<main.m:15:15>
    string_literal '"%d"'       Loc=<main.m:15:16>
    comma ','       Loc=<main.m:15:20>
    identifier 'a'   [LeadingSpace] Loc=<main.m:15:22>
    plus '+'     [LeadingSpace] Loc=<main.m:15:24>
    identifier 'b'   [LeadingSpace] Loc=<main.m:15:26>
    plus '+'     [LeadingSpace] Loc=<main.m:15:28>
    numeric_constant '30'    [LeadingSpace] Loc=<main.m:15:30 <Spelling=main.m:9:11>>
    r_paren ')'     Loc=<main.m:15:31>
    semi ';'        Loc=<main.m:15:32>
    r_brace '}'  [StartOfLine] [LeadingSpace]   Loc=<main.m:16:5>
    return 'return'  [StartOfLine] [LeadingSpace]   Loc=<main.m:17:5>
    numeric_constant '0'     [LeadingSpace] Loc=<main.m:17:12>
    semi ';'        Loc=<main.m:17:13>
    r_brace '}'  [StartOfLine]  Loc=<main.m:18:1>
    eof ''      Loc=<main.m:18:2>
    ios@HJ TestDemo % 
    
    
    3.3.2 语法分析
    • 语法分析是验证语法是否正确
      在词法分析的基础上,将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等,然后将所有节点组成抽象语法树(Abstract Syntax Tree,AST)。 语法分析程序判断源程序结构上是否正确
    clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
    
    • 作用域类型运算方式十分清晰。( 语法树一次只能处理一次计算。两次运算,就得多分一层级。)
    • 语法分析,就是在生成语法树完成检测的。
    • 头文件找不到时,可以指定SDK:
    clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk(自己SDK路径) -fmodules -fsyntax-only -Xclang -ast-dump main.m
    

    3.4 生成中间代码IR(Intermediate representation)

    3.4.1 生成中间代码
    • 完成以上步骤后,就开始生成中间代码IR,代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVMIR

    • 便于理解,我们简化代码:

    #import <stdio.h>
    
    int test(int a, int b) {
        return a + b + 3;
    }
    
    int main(int argc, const char * argv[]) {
        int a = test(1,2);
        printf("%d",a);
        return 0;
    }
    

    通过下面命令生成.ll文本文件,查看IR代码:

    clang -S -fobjc-arc -emit-llvm main.m
    
    • IR基本语法
      @ 全局标识
      % 局部标识
      alloca 开辟空间
      align 内存对齐
      i32 32个bit,4个字节
      store 写入内存
      load 读取数据
      call 调用数据
      ret 返回
    • 使用VSCodeSublime Text可以打开代码:(可以指定文件语言,让代码高亮色
    image
    • Q:图中为何多创建那么多局部变量?(如test函数内的a5、a6)
    • 因为在上一阶段(编译阶段),我们将代码编译成了语法树结构。而此时,我们只是沿语法树进行读取。 语法树每一个层级,都需要一个临时变量承接。再返回上一层级处理
    • 所以会产生那么多局部变量
    3.4.2 IR优化
    • 我们可以在XcodeBuild Settings中搜索Optimization,可以看到优化级别。
      (Debug模式默认None [O0]无优化,Release模式默认Fastest,Smallest [Os]最快最小)
    image
    • LLVM的优化级别分为 -O0-O1-O2-O3-Os(第一个字母是Optimization的O)。

    • 分别选择O0Os两个优化等级进行中间代码的生成比较:

    clang -S -fobjc-arc -emit-llvm main.m -o mainO0.ll      //  O0  无优化
    clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll  //  Os 最快最小
    
    3.4.3 bitCode再优化

    Xcode7之后开启bitCode苹果会再进一步优化,生成.bc中间代码

    优化体现:上传APPstore的包,针对不同型号手机做了区分,不同型号手机下载时,大小不同

    clang -emit-llvm -c main.ll -o main.bc
    

    3.5 生成汇编代码

    • 完成中间代码的生成后,可以将代码转变汇编代码了。

    • 此刻我们有4种不同程度的代码(源代码->无优化IR代码->Os优化IR代码 -> bitcode优化代码):

      image
    • 分别对4种程度的代码输出汇编文件:

    clang -S -fobjc-arc main.m -o main.s
    clang -S -fobjc-arc main.ll -o mainO0.s
    clang -S -fobjc-arc mainOs.ll -o mainOs.s
    clang -S -fobjc-arc main.bc -o mainbc.s
    

    生成汇编代码时,只有选择优化等级,才能减少汇编代码量

    3.6 生成目标文件(机器代码)

    • 生成汇编文件后,汇编器汇编代码作为输入,将汇编代码转换机器代码输出目标文件(object file)
    clang -fmodules -c main.s -o main.o
    
    • file对比一下main.s汇编代码和main.o机器代码:
    file main3.m
    file main.o 
    
    image
    • xcrun执行nm命令查看main.o文件中的符号:
    xcrun nm -nm main.o
    
    image
    • 此时只是把当前文件编译为了机器码外部符号(如printf)无法识别。

    undefined: 表示当前文件暂时找不到符号
    external:表示这个符号外部可以访问的。(实现不在我这,在外部某个地方

    所以当前虽转换成了机器代码。但是只是目标文件,并不能直接执行,需要所有资源链接起来,才可以执行

    3.7 生成可执行文件(链接)

    • 通过链接器把编译产生的.o文件和.dylib.a文件链接关联起来,生成真正的mach-o可执行文件
    clang main.o -o main // 将目标文件转成可执行文件
    file main            // 查看文件
    xcrun nm -nm main    // 查看main的符号
    
    
    image
    • 对比main.o目标文件,此时生成的main文件:
    1. object文件变成了executable可执行文件
    2. 虽然都有undefined,但是可执行文件中指定了该符号来源库。机器在运行时,会从相应的库中取读取符号(printf)

    总结:

    源代码可执行文件整个流程

    image

    四. LLVM源码 & 编译流程

    【注意】

    1. LLVM源码2.29G编译后文件30G,请确保电脑硬盘空间足够
    2. 编译时,电脑温度会飙升90多度,请用空调伺候着,可能黑屏
    3. 编译时间长达1个多小时,请合理安排时间。

    如果以上3点,你确定能接受,那我们就开始吧。

    4.1 LLVM下载

    4.2 LLVM编译

    • 最新的LLVM只支持cmake编译,需要使用Homebrew安装cmake:
    4.2.1 安装cmake
    • 查看brew列表,检查是否安装过cmake,如果有,就跳过此步骤
    brew list
    
    • 如果没有,就使用brew安装:
    brew install cmake
    

    如果报权限错误,可sudo chown -Rwhoami:admin /usr/local/share放开权限

    image
    4.2.2 编译llvm
    • cmake编译成Xcode项目
    cd llvm-project
    mkdir build
    cd build
    cmake -G Xcode ../llvm   
    // 或者: cmake -G Xcode CMAKE_BUILD_TYPE="Release" ../llvm
    // 或者: cmake -G Xcode CMAKE_BUILD_TYPE="debug" ../llvm 
    
    • 成功之后,可以看到生成的Xcode文件:

      image
    • 打开LLVM.xcodeproj,选择自动创建Schemes

      image
    • 自动创建完成后,选择ALL_BUILD进行编译(耗时0.5-1小时,CPU满负荷运转)

      image
    • 编译完成。接下来我们开始创建插件。

    相关文章

      网友评论

          本文标题:iOS 底层探索:LLVM入门

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