美文网首页
25:LLVM 简介和编译流程详解

25:LLVM 简介和编译流程详解

作者: 小瞎_MarkDash | 来源:发表于2020-05-20 18:10 被阅读0次

    目录

    image.png

    传统编译器设计

    image.png
    • 输入源代码(Obj-C, Swift, ...) → 编译器处理 → 输出机器码(010101)

    • 编译器处理分为以下步骤

    前端 (Frontend)

    负责解析源代码,进行:

    • 词法分析

    • 语法分析,语义分析,检查源代码是否有错误,构建 抽象语法树 (Abstract Syntax Tree, AST)

    优化器 (Optimizer)

    负责进行各种优化。例如消除冗余计算 (甚至直接将方法优化成一个固定值,而不去调用方法)等。

    后端 (Backend)

    将代码映射到目标指令集。生成机器语言,此过程会再次优化 (机器语言层面)。

    LLVM 的设计

    • 从图里看出,编译器前端输入源代码,后端输出机器码。因为传统编译器是按照整体程序设计的,所以总共需要做 n×m 个编译器。

    • LLVM使用通用的代码表现形式 (IR,可以理解为中间码),优化器的出入口都是IR,所以LLVM可以为任何编程语言独立编写前端,为任何硬件架构独立编写后端,工作量缩减为 n+m,且能集中力量不断提升优化器性能。

      image.png

    Clang 编译流程

    ClangLLVM的一个子项目。它属于整个LLVM架构的编译器 前端,负责编译 CC++Objective-C

    运行命令,打印源码编译阶段

    运行命令clang -ccc-print-phases main.m

    0: input, "main.m", objective-c
    1: preprocessor, {0}, objective-c-cpp-output
    2: compiler, {1}, ir
    3: backend, {2}, assembler
    4: assembler, {3}, object
    5: linker, {4}, image
    6: bind-arch, "x86_64", {5}, image
    
    • 0:输入文件:找到源文件
    • 1:预处理:替换宏,但不会替换别名typedef;头文件导入并展开,包括头文件的头文件,代码行数激增
    • 2:编译:词法分析 (切割成一个个词,不检查语法错误)、语法分析 (组装词,检查语法错误)、最终生成IR
    • 3:后端:LLVM通过一个个Pass (类似节点) 去优化,每个Pass有自己的优化方式,最终生成汇编代码
    • 4:把汇编文件变成.o文件
    • 5:各个.o文件有联系,需要进行链接,生成Mach-O文件
    • 6:对应不同架构,生成对应的Mach-O文件

    1: 预处理

    • main.m文件

      #import <stdio.h>
      
      #define a 10
      
      typedef int MD_INT_64;
      
      int main(int argc, const char * argv[]) {
          @autoreleasepool {
              // insert code here...
              MD_INT_64 b = 20;
              printf("sum = %d", a + b + 50);
          }
          return 0;
      }
      
    • 运行命令clang -E main.m >> main1.cpp,如果不输入>> main1.cpp,则不会新生成文件,而直接在命令行工具打印。以下省略前面549行代码 ↓

      typedef int MD_INT_64;
      
      int main(int argc, const char * argv[]) {
          @autoreleasepool {
      
              MD_INT_64 b = 20;
              printf("sum = %d", 10 + b + 50);
          }
          return 0;
      }
      

    2.1: 编译-词法分析 (切割词)

    • 运行命令clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

    • 第几行,第几个字符开始,第几个字符结束,一目了然。只截取了一些 ↓

              // insert'      Loc=<main.m:9:1>
      typedef 'typedef'    [StartOfLine]  Loc=<main.m:13:1>
      int 'int'    [LeadingSpace] Loc=<main.m:13:9>
      identifier 'MD_INT_64'   [LeadingSpace] Loc=<main.m:13:13>
      semi ';'        Loc=<main.m:13:22>
      int 'int'    [StartOfLine]  Loc=<main.m:15:1>
      identifier 'main'    [LeadingSpace] Loc=<main.m:15:5>
      l_paren '('     Loc=<main.m:15:9>
      int 'int'       Loc=<main.m:15:10>
      identifier 'argc'    [LeadingSpace] Loc=<main.m:15:14>
      

    2.2: 编译-语法分析 (重新组合,生成抽象语法树)

    • 运行命令clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

    • 如果导入了iOS特有的头文件,需要修改一下指令 (仅供参考,每个人电脑路径和模拟器版本不一样) clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk􏱶􏴣􏴤SDK􏴥􏴦􏱷 -fmodules -fsyntax-only -Xclang -ast-dump main.m

    • 经过重新组合,语法分析出来的代码行数通常会比词法分析短一些,譬如词法分析里的intargc,在语法分析里变成一行这是一个名叫argc的int类型参数。最好带着栈思维去读抽象语法树。只截取了一些 ↓

      |-TypedefDecl 0x7fd405845368 <line:13:1, col:13> col:13 referenced MD_INT_64 'int'
      | `-BuiltinType 0x7fd405036700 'int'
      `-FunctionDecl 0x7fd405845640 <line:15:1, line:22:1> line:15:5 main 'int (int, const char **)'
        |-ParmVarDecl 0x7fd4058453d8 <col:10, col:14> col:14 argc 'int'
        |-ParmVarDecl 0x7fd4058454f0 <col:20, col:38> col:33 argv 'const char **':'const char **'
        `-CompoundStmt 0x7fd4050f1ad8 <col:41, line:22:1>
          |-ObjCAutoreleasePoolStmt 0x7fd4050f1a90 <line:16:5, line:20:5>
          | `-CompoundStmt 0x7fd4050f1a70 <line:16:22, line:20:5>
          |   |-DeclStmt 0x7fd4050f1868 <line:18:9, col:25>
          |   | `-VarDecl 0x7fd4050f1400 <col:9, col:23> col:19 used b 'MD_INT_64':'int' cinit
          |   |   `-IntegerLiteral 0x7fd4050f1468 <col:23> 'int' 20
          |   `-CallExpr 0x7fd4050f1a10 <line:19:9, col:38> 'int'
          |     |-ImplicitCastExpr 0x7fd4050f19f8 <col:9> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
          |     | `-DeclRefExpr 0x7fd4050f1880 <col:9> 'int (const char *, ...)' Function 0x7fd4050f1490 'printf' 'int (const char *, ...)'
      

    2.3 / 3.0: 生成中间码 IR (Intermediate Representation) / Pass 优化

    • 代码生成器 (Code Generation) 会将语法树自顶向下遍历,翻译成LLVM IR

    • 运行命令clang -S -fobjc-arc -emit-llvm main.m,获得main.ll文件。和汇编有点像。只截取了main函数 ↓

    • IR基本语法

      @ 全局标识
      % 局部标识
      alloca 开辟空间
      align 内存对齐
      i32 32个bit,共4个字节
      store 写入内存
      load 读内存的数据
      call 调用函数
      ret 返回

      define i32 @main(i32, i8**) #0 {
        %3 = alloca i32, align 4
        %4 = alloca i32, align 4
        %5 = alloca i8**, align 8
        %6 = alloca i32, align 4
        store i32 0, i32* %3, align 4
        store i32 %0, i32* %4, align 4
        store i8** %1, i8*** %5, align 8
        %7 = call i8* @llvm.objc.autoreleasePoolPush() #1
        store i32 20, i32* %6, align 4
        %8 = load i32, i32* %6, align 4
        %9 = add nsw i32 10, %8
        %10 = add nsw i32 %9, 50
        %11 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([9 x i8], [9 x i8]* @.str, i64 0, i64 0), i32 %10)
        call void @llvm.objc.autoreleasePoolPop(i8* %7)
        ret i32 0
      }
      
    • 刚才是没有优化的,看看优化的,LLVM的优化级别分别为-O0 -O1 -O2 -03 -Os,我们试试-Os,运行命令clang -Os -S -fobjc-arc -emit-llvm main.m,获得main.ll文件。print函数的参数,直接用绝对值80,而不像刚才用局部变量算来算去。只截取了main函数 ↓

      define i32 @main(i32, i8** nocapture readnone) local_unnamed_addr #0 {
        %3 = tail call i8* @llvm.objc.autoreleasePoolPush() #1
        %4 = tail call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([9 x i8], [9 x i8]* @.str, i64 0, i64 0), i32 80) #3, !clang.arc.no_objc_arc_exceptions !9
        tail call void @llvm.objc.autoreleasePoolPop(i8* %3) #1
        ret i32 0
      }
      
    • 这个优化级别在Xcode可以调:Build SettingsCode GenerationDebug模式下为了编译快点一般不优化,选None [-O0]

      image.png

    LLVM的优化使用了叫Pass的东西,可以理解为优化节点,每个节点负责不同的优化事项 (跳转、运算等),一个个Pass搞下来,逻辑处理发生变化,就完成了优化。如果想玩LLVM优化可以试试写Pass

    Pass能使FuncA→FuncB→FuncC变成FuncA→FuncC甚至FuncA(算好的值);也能使FuncA→FuncB变成FuncA→FuncX→FuncY→FuncB,变得复杂,做到混淆效果。不光是逻辑,其中的局部标识也能增加。直接混淆还能看懂些,优化完以后再混淆就真的难看懂。

    2.4: Bitcode

    Xcode7以后,Enable Bitcode苹果会在IR的基础上做进一步的优化,生成.bc代码。

    iOS端:Bitcode可选
    watchOS端:Bitcode必选
    macOS端:Bitcode不可选

    • 运行命令clang -emit-llvm -c main.ll -o main.bc.bc文件暂时不知道怎么打开,没有截图。

    3.1: 生成汇编代码 (属于 后端Backend / 代码生成器CodeGenerator)

    汇编代码可以由.ll.bc代码生成。

    • 运行命令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

    • 只截取部分代码 ↓

      subq    $48, %rsp
      movl    $0, -4(%rbp)
      movl    %edi, -8(%rbp)
      movq    %rsi, -16(%rbp)
      callq   _objc_autoreleasePoolPush
      movl    $20, -20(%rbp)
      

    4: 生成目标文件 .o

    汇编器将汇编代码转换为机器代码,这就是.o文件 (object file)。

    • 运行命令clang -fmodules -c main.s -o main.o

    • 运行命令xcrun nm -nm main.o,查看main.o中的符号

      • undefined,当前文件暂时找不到
      • external,这个符号在外部找 (我们自己内部没有)
                       (undefined) external _objc_autoreleasePoolPop
                       (undefined) external _objc_autoreleasePoolPush
                       (undefined) external _printf
      0000000000000000 (__TEXT,__text) external _main
      

    5. 生成可执行文件 Mach-O

    链接器 (Linker) 把.o文件和.dylib .a文件 生成一个Mach-O文件。

    现在是编译阶段,这个Linker不是dylddyld是运行时的事情。

    • 运行命令clang main.o -o main

      友情提示:如果是上面一路跟下来的,这里会因为找不到@autoreleasepool报错,请去掉源码里的@autoreleasepool再跟一下)

    • 文件变大了,main.s1KB,main13KB

    • 运行命令xcrun nm -nm main,查看main中的符号。

                       (undefined) external _printf (from libSystem)
                       (undefined) external dyld_stub_binder (from libSystem)
      0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
      0000000100000f73 (__TEXT,__text) external _main
      0000000100002008 (__DATA,__data) non-external __dyld_private
      
    • 上面是编译阶段,下面要讲的是运行阶段(dyld相关)的事情。虽然printf仍然是undefined,但这只是一个标示,后面写了(from libSystem),意味着当程序跑起来的时候,自己没有printf,它是个external外部函数,找libSystem,刚好iOS操作系统有libSystem,在那里找到printf的地址以后,进行符号绑定就OK了。

    • 运行命令./main,执行程序

      sum = 80%
      
    • 运行命令,file main,看文件类型和架构

      main: Mach-O 64-bit executable x86_64
      

    相关文章

      网友评论

          本文标题:25:LLVM 简介和编译流程详解

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