OC底层原理三十一:LLVM入门

OC底层原理 学习大纲

本节,我们给大家介绍一个伟大的架构编译器LLVM

  1. 什么是编译器
  2. LLVM概述
  3. LLVM案例体验

1 什么是编译器?

1.1 Python案例

  • 创建python文件夹,新建helloDemo.py文件,内容如下:
print("hello")
  • 调用python helloDemo.py执行文件,打印出python
    image.png

1.2 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查看文件:
    image.png

发现.out文件是:64位的Mach-O可执行文件,当前clang出来的是x86_64架构, mac电脑可读。 所以可以./a.out直接执行:

image.png

Q:解释型语言与编译型语言

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

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

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

Q:汇编有指令吗?

  • 早期科学家,使用0、1编码。 比如 00001111 对应 call00000111 对应bl。有了对应关系后。 再手敲0和1就有点难受了。于是写个中间解释器,我们只用输入callbl这样的标记指令,经过解释器,变成0和1的组合,再交给机器去执行。 这就是汇编的由来

  • 而基于汇编往上,再映射封装相关对应关系。就跨时代性c语言,再往上层封装,就出现了高级语言ocswift等语言。所以汇编执行快,因为它是直接转换机器语言的。

  • 汇编指令集,是针对同一操作系统而言,它支持跨平台机器指令cpu的在识别。早期的计算机厂家非常,虽然都用01组合,但相同组合背后却是相应不同指令。所以汇编无法跨平台不同操作系统下,汇编指令不同的。

2. LLVM概述

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

2.1 传统编译器的设计

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

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

- 优化器(Optimizer)

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

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

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

iOS的编译器架构:

Objective C / C / C++ 使用的编译器前端ClangSwiftswift,后端都是LLVM

image.png

2.2 LLVM的设计

  • GCC是一个非常成功编译器,但由于它作为整体应用程序设计的,用途受到了限制

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

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

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

LLVM的设计

2.3 Clang

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

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

3. LLVM案例体验

  • 新建一个Mac OS命令行工程:
    image.png
  • 没有改动代码


    image.png

3.1 编译流程

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

image.png

编译流程分为以下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库的代码:

    image.png

  • 我们发现测试代码中的宏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不会被替换
    image.png

安全拓展:

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

3.3 编译阶段

3.3.1 词法分析
  • 编译main.m文件:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

-词法分析,就是根据空格括号这些将代码拆分成一个个Token。标注了位置第几行第几个字符开始的。

image.png

3.3.2 语法分析
  • 语法分析是验证语法是否正确
    在词法分析的基础上,将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等,然后将所有节点组成抽象语法树(Abstract Syntax Tree,AST)。 语法分析程序判断源程序结构上是否正确
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
  • 作用域类型运算方式十分清晰。( 语法树一次只能处理一次计算。两次运算,就得多分一层级。)

    image.png

  • 语法分析,就是在生成语法树完成检测的。

  • 头文件找不到时,可以指定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.png
  • Q:图中为何多创建那么多局部变量?(如test函数内的a5、a6)
  • 因为在上一阶段(编译阶段),我们将代码编译成了语法树结构。而此时,我们只是沿语法树进行读取。 语法树每一个层级,都需要一个临时变量承接。再返回上一层级处理
  • 所以会产生那么多局部变量
3.4.2 IR优化
  • 我们可以在XcodeBuild Settings中搜索Optimization,可以看到优化级别。
    (Debug模式默认None [O0]无优化,Release模式默认Fastest,Smallest [Os]最快最小)
image.png
  • 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 最快最小
image.png

image.png
  • 优化后的代码,舒服多了。之前那些冗余临时局部变量,也都被优化代码量减少很多。
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.png

  • 分别对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
image.png

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

【拓展】在生成中间代码前后都可以进行优化

  • [尝试一] 将main.m直接选择Os级别优化生成.s汇编文件
clang -Os -S -fobjc-arc main.m -o mainOs.s
  • [尝试二] 将main.m生成无优化main.s,再main.s选择Os级别优化生成.s汇编文件
clang -S -fobjc-arc -emit-llvm main.m -o mainO0.ll
clang -Os -S -fobjc-arc mainO0.ll -o mainOoOs.s
  • [尝试三] 将main.m选择Os级别优化生成main.s,再main.s选择无优化级别生成.s汇编文件
clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll
clang -S -fobjc-arc mainOs.ll -o mainOsOo.s
  • [尝试四] 将main.m选择Os级别优化生成main.s,再main.s选择Os级别优化生成.s汇编文件
clang -Os -S -fobjc-arc -emit-llvm main.m -o mainOs.ll
clang -Os -S -fobjc-arc mainOs.ll -o mainOsOs.s
  • 内容比较:
image.png

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

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

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

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

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

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

至此,我们已完整分析:源代码可执行文件整个流程

image.png

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

推荐阅读更多精彩内容

  • 什么是LLVM LLVM项目是模块化、可重用的编译器以及工具链技术的集合 The LLVM Project is ...
    那位小姐阅读 2,644评论 0 10
  • OC底层原理 学习大纲[https://www.jianshu.com/p/9e19354c0266] 对象的本质...
    markhetao阅读 1,180评论 11 1
  • 解释性语言和编译性语言的区别?解释性语言可以通过解释器直接执行相应的代码,比如python语言;而编译性语言要经过...
    半边枫叶阅读 851评论 0 1
  • 目录 传统编译器设计 输入源代码(Obj-C, Swift, ...) → 编译器处理 → 输出机器码(01010...
    小瞎_MarkDash阅读 1,226评论 0 2
  • 什么是LLVM LLVM项目是模块化、可重用的编译器以及工具链技术的集合。 美国计算机协会 (ACM) 将其201...
    Coder_LRT阅读 2,362评论 0 1