美文网首页
iOS-深入了解LLVM编译器架构

iOS-深入了解LLVM编译器架构

作者: 似水流年_9ebe | 来源:发表于2021-08-26 20:00 被阅读0次

    前言

    我们会经常听到编译器这个词语,我们就会想什么是编译器,它的功能是什么,跟我们的开发又有什么关系,这篇文章就带大家走入LLVM编译器架构,揭开编译器的神秘面纱。

    1 什么是编译器

    我们用Python(解释型)和C(编译型)来先对比下
    Python代码如下

    print("hello world\n")
    

    我们通过python py1.py命令执行下,看下效果,如图

    1

    python是python的解释器,这个就是解释型语言的效果。
    我们再来看C,代码如下

    #include<stdio.h>
    int main(int argc,char * argv[]){
            printf("hello world\n");
            return 0;
    }
    

    我们通过命令clang hello.c,效果如下

    2
    我们看到并没有执行,而在我们的文件中多了一个a.out文件,在unix下,这是个可执行文件,我们再通过./a.out执行下,效果如图
    3
    我们看到了执行效果。
    从这两个小小的案例可以看出,解释型语言和编译型语言的区别,
    解释型语言读取代码就会执行,而编译型语言要先翻译成cpu可以读的二进制代码。
    我们刚才的用的clang命令就是C,C++和Objective-C的编译器。
    python就是python的解释器。
    我们今天就从clang这个编译器开始说起。

    2 LLVM介绍

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

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

    优化器(Optimizer)
    优化器负责进行各种优化。改善代码的运行时间,例始消除冗余计算ac等。

    后端(Backend)/代码生成器(CodeGenerator)
    将代码映财到目标指令集。生成机器语言,并且进行机器相关的代码优化。

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


    5

    LLVM的设计
    当编译器决定支持多种源语言或多种硬架构时,LLVM的最重要的地方就来了。
    其它的编对器如GCC,它方法非常成功,但由于它是作为整体应用程序设计的,因此它的用途受到了很大的限制。
    LLVM设计的最重要方便是,使用通用的代码表示形式(IR ),它是用来在编译器中表示代码的形式。所以LLVM可以为任何编译语言独立编写前端,并且可以为任意硬件架构独立编写后端。


    6

    Clang是LLVM项目的中的一个子项目。它是基于LLVM架构的轻量编译器,诞生之初是为了替代GCC,提供更快的编译速度。它是负责编译C、C++、Objective-C语言的编译器,它属于整个LLVM架构中的,编译器前端。对于开发者来说,研究Clang可以给我们带来很多好处。

    3 编译流程分析

    我们先看下一段代码,如下

    #import <stdio.h>
    int main(int argc, const char * argv[]) {
        return 0;
    }
    

    我们通过命令clang -ccc-print-phases main.m执行

    7
    我们看编译的流程是什么样的。
    • +- 0: input, "main.m", objective-c 读取代码。
    • +- 1: preprocessor, {0}, objective-c-cpp-output 预处理价段,把宏替换,.h的导入进去。
    • +- 2: compiler, {1}, ir 编译价段,前端编译器的任务。
    • +- 3: backend, {2}, assembler 编译器后端,pass(环节,节点)优化,生成汇编代码。
    • +- 4: assembler, {3}, object 生成目标文件。
    • +- 5: linker, {4}, image 链接外部函数,静态库,动态库,生成镜像文件即可执行文件
    • bind-arch, "x86_64", {5}, image 根据不同的架构生成不同的镜像文件。

    编译流程的分析

    1. 读取代码
    读取我们编写的源代码。

    2. 预处理
    我们改下源码,如

    #import <stdio.h>
    #define C 30
    int main(int argc, const char * argv[]) {
        int a = 10;
        int b = 20;
        printf("%d",a + b +C);
        return 0;
    }
    

    接着执行clang -E main.m >> main1.m,我们看下main1.m文件,

    # 1 "main.m"
    # 1 "<built-in>" 1
    # 1 "<built-in>" 3
    # 379 "<built-in>" 3
    # 1 "<command line>" 1
    # 1 "<built-in>" 2
    # 1 "main.m" 2
    

    这里是宏展开,我们看下main函数

    int main(int argc, const char * argv[]) {
        int a = 10;
        int b = 20;
        printf("%d",a + b +30);
        return 0;
    }
    

    直接把我们的C这个宏展开直接替换成30。
    我们还用过typedef,我们改下代码

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

    执行clang -E main.m >> main1.m,如

    typedef int RO_INT_64
    int main(int argc, const char * argv[]) {
        RO_INT_64 a = 10;
        RO_INT_64 b = 20;
        printf("%d",a + b);
        return 0;
    }
    
    

    没有展开,typedef只是取别名,增强可读性,不是预处理指令
    3.编译价段
    3.1词法分析
    我们再执行命令clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m,词法分析,会把代码切成token,如下所示

    annot_module_include '#import <stdio.h>
    #d'     Loc=<main.m:2:1>
    int 'int'    [StartOfLine]  Loc=<main.m:4:1>
    identifier 'main'    [LeadingSpace] Loc=<main.m:4:5>
    l_paren '('     Loc=<main.m:4:9>
    int 'int'       Loc=<main.m:4:10>
    identifier 'argc'    [LeadingSpace] Loc=<main.m:4:14>
    comma ','       Loc=<main.m:4:18>
    const 'const'    [LeadingSpace] Loc=<main.m:4:20>
    char 'char'  [LeadingSpace] Loc=<main.m:4:26>
    star '*'     [LeadingSpace] Loc=<main.m:4:31>
    identifier 'argv'    [LeadingSpace] Loc=<main.m:4:33>
    l_square '['        Loc=<main.m:4:37>
    r_square ']'        Loc=<main.m:4:38>
    r_paren ')'     Loc=<main.m:4:39>
    l_brace '{'  [LeadingSpace] Loc=<main.m:4:41>
    int 'int'    [StartOfLine] [LeadingSpace]   Loc=<main.m:5:5>
    identifier 'a'   [LeadingSpace] Loc=<main.m:5:9>
    equal '='    [LeadingSpace] Loc=<main.m:5:11>
    numeric_constant '10'    [LeadingSpace] Loc=<main.m:5:13>
    semi ';'        Loc=<main.m:5:15>
    int 'int'    [StartOfLine] [LeadingSpace]   Loc=<main.m:6:5>
    identifier 'b'   [LeadingSpace] Loc=<main.m:6:9>
    equal '='    [LeadingSpace] Loc=<main.m:6:11>
    numeric_constant '20'    [LeadingSpace] Loc=<main.m:6:13>
    semi ';'        Loc=<main.m:6:15>
    identifier 'printf'  [StartOfLine] [LeadingSpace]   Loc=<main.m:7:5>
    l_paren '('     Loc=<main.m:7:11>
    string_literal '"%d"'       Loc=<main.m:7:12>
    comma ','       Loc=<main.m:7:16>
    identifier 'a'      Loc=<main.m:7:17>
    plus '+'     [LeadingSpace] Loc=<main.m:7:19>
    identifier 'b'   [LeadingSpace] Loc=<main.m:7:21>
    plus '+'     [LeadingSpace] Loc=<main.m:7:23>
    numeric_constant '30'       Loc=<main.m:7:24 <Spelling=main.m:3:11>>
    r_paren ')'     Loc=<main.m:7:25>
    semi ';'        Loc=<main.m:7:26>
    return 'return'  [StartOfLine] [LeadingSpace]   Loc=<main.m:8:5>
    numeric_constant '0'     [LeadingSpace] Loc=<main.m:8:12>
    semi ';'        Loc=<main.m:8:13>
    r_brace '}'  [StartOfLine]  Loc=<main.m:9:1>
    eof ''      Loc=<main.m:9:2>
    

    会把代码切成token,比如大小括号,等于号还有字符串等。

    3.2语法分析
    检查语法是否正确,在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等,然后将所有节点组成抽像语法树(Abstract Syntax Tree,AST)。语法分析程序判断源程序在结构上是否正确。
    我们执行clang -fmodules -fsyntax-only -Xclang -ast-dump main.m,

    8
    我们把代码改错,看下效果
    9
    这里有错误提示。
    我分析下语法树
    -FunctionDecl 0x7f9aed0bee00 <line:5:1, line:10:1> line:5:5 main 'int (int, const char **)'
      |-ParmVarDecl 0x7f9aed01e140 <col:10, col:14> col:14 argc 'int'
      |-ParmVarDecl 0x7f9aed01e288 <col:20, col:38> col:33 argv 'const char **':'const char **'
      `-CompoundStmt 0x7f9aed0bf7d0 <col:41, line:10:1>
        |-DeclStmt 0x7f9aed0bf010 <line:6:5, col:21>
        | `-VarDecl 0x7f9aed0bef88 <col:5, col:19> col:15 used a 'RO_INT_64':'int' cinit
        |   `-IntegerLiteral 0x7f9aed0beff0 <col:19> 'int' 10
        |-DeclStmt 0x7f9aed0bf538 <line:7:5, col:21>
        | `-VarDecl 0x7f9aed0bf038 <col:5, col:19> col:15 used b 'RO_INT_64':'int' cinit
        |   `-IntegerLiteral 0x7f9aed0bf0a0 <col:19> 'int' 20
        |-CallExpr 0x7f9aed0bf740 <line:8:5, col:25> 'int'
        | |-ImplicitCastExpr 0x7f9aed0bf728 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
        | | `-DeclRefExpr 0x7f9aed0bf550 <col:5> 'int (const char *, ...)' Function 0x7f9aed0bf0c8 'printf' 'int (const char *, ...)'
        | |-ImplicitCastExpr 0x7f9aed0bf788 <col:12> 'const char *' <NoOp>
        | | `-ImplicitCastExpr 0x7f9aed0bf770 <col:12> 'char *' <ArrayToPointerDecay>
        | |   `-StringLiteral 0x7f9aed0bf5b0 <col:12> 'char [3]' lvalue "%d"
        | `-BinaryOperator 0x7f9aed0bf6b0 <col:17, line:3:11> 'int' '+'
        |   |-BinaryOperator 0x7f9aed0bf670 <line:8:17, col:21> 'int' '+'
        |   | |-ImplicitCastExpr 0x7f9aed0bf640 <col:17> 'RO_INT_64':'int' <LValueToRValue>
        |   | | `-DeclRefExpr 0x7f9aed0bf5d0 <col:17> 'RO_INT_64':'int' lvalue Var 0x7f9aed0bef88 'a' 'RO_INT_64':'int'
        |   | `-ImplicitCastExpr 0x7f9aed0bf658 <col:21> 'RO_INT_64':'int' <LValueToRValue>
        |   |   `-DeclRefExpr 0x7f9aed0bf608 <col:21> 'RO_INT_64':'int' lvalue Var 0x7f9aed0bf038 'b' 'RO_INT_64':'int'
        |   `-IntegerLiteral 0x7f9aed0bf690 <line:3:11> 'int' 30
        `-ReturnStmt 0x7f9aed0bf7c0 <line:9:5, col:12>
          `-IntegerLiteral 0x7f9aed0bf7a0 <col:12> 'int' 0
    
    • FunctionDecl 0x7f9aed0bee00 <line:5:1, line:10:1> line:5:5 main 'int (int, const char )'
      |-ParmVarDecl 0x7f9aed01e140 <col:10, col:14> col:14 argc 'int'
      |-ParmVarDecl 0x7f9aed01e288 <col:20, col:38> col:33 argv 'const char ':'const char ** 这里就是main函数,返回值int,参数int和char,参数名称arc,int类型,参数argv const char
      类型
    • |-CallExpr 0x7f9aed0bf740 <line:8:5, col:25> 'int'
      | |-ImplicitCastExpr 0x7f9aed0bf728 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
      | | `-DeclRefExpr 0x7f9aed0bf550 <col:5> 'int (const char *, ...)' Function 0x7f9aed0bf0c8 'printf' 'int (const char *, ...)'这里有一个函数的调用printf,返回int类型。
    • |-ImplicitCastExpr 0x7f9aed0bf788 <col:12> 'const char *' <NoOp>
      | | -ImplicitCastExpr 0x7f9aed0bf770 <col:12> 'char *' <ArrayToPointerDecay> | |-StringLiteral 0x7f9aed0bf5b0 <col:12> 'char [3]' lvalue "%d" 这是第一个参数
    • |-DeclStmt 0x7f9aed0bf010 <line:6:5, col:21>
      | -VarDecl 0x7f9aed0bef88 <col:5, col:19> col:15 used a 'RO_INT_64':'int' cinit |-IntegerLiteral 0x7f9aed0beff0 <col:19> 'int' 10
      |-DeclStmt 0x7f9aed0bf538 <line:7:5, col:21>
      | `-VarDecl 0x7f9aed0bf038 <col:5, col:19> col:15 used b 'RO_INT_64':'int' 这里是a,b
    • | | -ImplicitCastExpr 0x7f9aed0bf770 <col:12> 'char *' <ArrayToPointerDecay> | |-StringLiteral 0x7f9aed0bf5b0 <col:12> 'char [3]' lvalue "%d"这是第一个参数
    • BinaryOperator 0x7f9aed0bf6b0 <col:17, line:3:11> 'int' '+'是+运算结果,
    • BinaryOperator 0x7f9aed0bf670 <line:8:17, col:21> 'int' '+'
      | | |-ImplicitCastExpr 0x7f9aed0bf640 <col:17> 'RO_INT_64':'int' <LValueToRValue>
      | | | -DeclRefExpr 0x7f9aed0bf5d0 <col:17> 'RO_INT_64':'int' lvalue Var 0x7f9aed0bef88 'a' 'RO_INT_64':'int' | |-ImplicitCastExpr 0x7f9aed0bf658 <col:21> 'RO_INT_64':'int' <LValueToRValue>
      | | -DeclRefExpr 0x7f9aed0bf608 <col:21> 'RO_INT_64':'int' lvalue Var 0x7f9aed0bf038 'b' 'RO_INT_64':'int' |-IntegerLiteral 0x7f9aed0bf690 <line:3:11> 'int' 30 第一个加法运算的结果+30
    • ReturnStmt 0x7f9aed0bf7c0 <line:9:5, col:12> 这里是返回
    • 返回int类型值为0

    3.4 生成中间代码(intermediate representation )
    代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM IR。
    IR基本语法
    @全局标识
    %局部标识
    alloca 开辟空间
    align 内存对齐
    i32 32个bit
    store写入内存
    load读取数据
    call调用函数
    ret返回

    我们改下代码

    #import <stdio.h>
    #define C 30
    typedef int RO_INT_64;
    
    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;
    }
    
    

    我们执行命令clang -S -fobjc-arc -emit-llvm main.m,会生成main.ll文件,我们看下main.ll文件内容

    define i32 @test(i32 %0, i32 %1) #0 { #test(int a, int b )
      %3 = alloca i32, align 4 #开辟空间 4字节对齐 int a3;
      %4 = alloca i32, align 4 #开辟空间 4字节对齐 int a4;
      store i32 %0, i32* %3, align 4 # a3=a;
      store i32 %1, i32* %4, align 4 # a4=b;
      %5 = load i32, i32* %3, align 4 # int a5=a3;
      %6 = load i32, i32* %4, align 4 # int a6=a4;
      %7 = add nsw i32 %5, %6 # int a7 = a5+a6;
      %8 = add nsw i32 %7, 3 # int a8= a7+ 3;
      ret i32 %8 # return a8;
    }
    

    这就是test函数IR代码,这是没有经过优化的。
    IR的优化
    clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
    经过优化会简洁很多,这里不再赘述。
    xcode中的Optimization Level可以设置。
    bitCode
    clang -emit-llvm -c main.ll -o main.bc

    4 生成汇编代码

    我们通过最终的.bc或者.ll代码生成汇编代码
    命令
    clang -S -fobjc-arc main.bc -o main.s
    clang -S -fobjc-arc main.ll -o main.s
    生成汇编代码也可以进行优化
    clang -Os -S -fobjc-arc main.m -o main.s
    执行命令
    clang -S -fobjc-arc main.ll -o main.s

    _test:                                  ## @test
        .cfi_startproc
    ## %bb.0:
        pushq   %rbp 
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register %rbp
        movl    %edi, -4(%rbp)
        movl    %esi, -8(%rbp)
        movl    -4(%rbp), %eax
        addl    -8(%rbp), %eax
        addl    $3, %eax
        popq    %rbp
        retq
        .cfi_endproc
                                            ## -- End function
        .globl  _main                           ## -- Begin function main
        .p2align    4, 0x90
    

    这是x86的汇编指令集。
    我们再执行这个clang -Os -S -fobjc-arc main.m -o main.s优化的命令

    _test:                                  ## @test
        .cfi_startproc
    ## %bb.0:
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset %rbp, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register %rbp
                                            ## kill: def $esi killed $esi def $rsi
                                            ## kill: def $edi killed $edi def $rdi
        leal    3(%rdi,%rsi), %eax
        popq    %rbp
        retq
        .cfi_endproc
                                            ## -- End function
        .globl  _main                           ## -- Begin function main
    _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
        leaq    L_.str(%rip), %rdi
        movl    $6, %esi
    

    这是经过优化过的,main的函数调用的test直接优化成了6。

    5 生成目标文件(汇编器)

    目标文件的生成,是汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输了目标文件(object file)。这里属于后端的作务。
    执行命令clang -fmodules -c main.s -o main.o,生成的main.o就是目标文件。通过xcrun nm -nm main.o查看符号,如下所示

                     (undefined) external _printf
    0000000000000000 (__TEXT,__text) external _test
    000000000000000a (__TEXT,__text) external _main
    

    _printf是一个undefined external的符号。
    undefined表示当前文件暂时找不到符号。
    external表示这个符号是外部可以访问的。

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

    连接器把编译产生的.o文件和(.dylib.a)文件,生成一个macho-o文件。
    我们执行命令clang main.o -o main生成了可执行文件main。
    我们再通过命令xcrun nm -nm main,如下

                  (undefined) external _printf (from libSystem)
                     (undefined) external dyld_stub_binder (from libSystem)
    0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
    0000000100003f6d (__TEXT,__text) external _test
    0000000100003f77 (__TEXT,__text) external _main
    0000000100008008 (__DATA,__data) non-external __dyld_private
    

    这里有两个外部符号_printf(可以找到)和dyld_stub_binder
    当我们的程序进入内存的的时候,外部函数会立即跟dyld_stub_binder绑定,这个dyld是强制执行,链接是打个标记,符号在哪个库中(编译期),绑定是在执行的时候把外部函数地址和符号进行绑定(运行期),一定会有dyld_stub_binder这个符号,先绑定这个符号,其它函数的绑定由dyld_stub_binder执行。

    总结编译器的流程:

    • 前端:读取代码,词法分析,语法分析,语义分析,生成AST(生成IR)
    • 优化器:根据一个个的pass进行优化,
    • 后端:生成汇编,根据不同的架构生成可执行文件

    LLVM最大的好处:前后端分离。
    pass的解释:就是“遍历一遍IR,可以同时对它做一些操作”的意思。翻译成中文应该叫“趟”。 在实现上,LLVM的核心库中会给你一些 Pass类 去继承。你需要实现它的一些方法。 最后使用LLVM的编译器会把它翻译得到的IR传入Pass里,给你遍历和修改

    总结

    这篇文章带大家初步了解了编译器的原理,LLVM的架构。分析了编译的流程,希望这篇文章可以让大家学习到新的知识。

    相关文章

      网友评论

          本文标题:iOS-深入了解LLVM编译器架构

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