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

目录

25:LLVM 简介和编译流程详解_第1张图片
image.png

传统编译器设计

25:LLVM 简介和编译流程详解_第2张图片
image.png
  • 输入源代码(Obj-C, Swift, ...) → 编译器处理 → 输出机器码(010101)

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

前端 (Frontend)

负责解析源代码,进行:

  • 词法分析

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

优化器 (Optimizer)

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

后端 (Backend)

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

LLVM 的设计

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

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

    25:LLVM 简介和编译流程详解_第3张图片
    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 
    
    #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=
    typedef 'typedef'    [StartOfLine]  Loc=
    int 'int'    [LeadingSpace] Loc=
    identifier 'MD_INT_64'   [LeadingSpace] Loc=
    semi ';'        Loc=
    int 'int'    [StartOfLine]  Loc=
    identifier 'main'    [LeadingSpace] Loc=
    l_paren '('     Loc=
    int 'int'       Loc=
    identifier 'argc'    [LeadingSpace] Loc=
    

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.sdkSDK -fmodules -fsyntax-only -Xclang -ast-dump main.m

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

    |-TypedefDecl 0x7fd405845368  col:13 referenced MD_INT_64 'int'
    | `-BuiltinType 0x7fd405036700 'int'
    `-FunctionDecl 0x7fd405845640  line:15:5 main 'int (int, const char **)'
      |-ParmVarDecl 0x7fd4058453d8  col:14 argc 'int'
      |-ParmVarDecl 0x7fd4058454f0  col:33 argv 'const char **':'const char **'
      `-CompoundStmt 0x7fd4050f1ad8 
        |-ObjCAutoreleasePoolStmt 0x7fd4050f1a90 
        | `-CompoundStmt 0x7fd4050f1a70 
        |   |-DeclStmt 0x7fd4050f1868 
        |   | `-VarDecl 0x7fd4050f1400  col:19 used b 'MD_INT_64':'int' cinit
        |   |   `-IntegerLiteral 0x7fd4050f1468  'int' 20
        |   `-CallExpr 0x7fd4050f1a10  'int'
        |     |-ImplicitCastExpr 0x7fd4050f19f8  'int (*)(const char *, ...)' 
        |     | `-DeclRefExpr 0x7fd4050f1880  '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]

    25:LLVM 简介和编译流程详解_第4张图片
    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 简介和编译流程详解)