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

目录

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
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349