iOS 底层探索:LLVM入门

iOS 底层探索: 学习大纲 OC篇

前言

  • 今天来学习一下牛逼的架构编译器LLVM

学习大纲

  1. 简单了解编译器
  2. LLVM概述
  3. LLVM案例体验
  4. LLVM源码 & 编译流程

准备

一 、简单了解编译器

百度百科

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

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

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

  2. 汇编指令

  • 早期科学家,使用0、1编码。 比如 00001111 对应 call, 00000111 对应bl。有了对应关系后。 再手敲0和1就有点难受了。于是写个中间解释器,我们只用输入call、bl这样的标记指令,经过解释器,变成0和1的组合,再交给机器去执行。 这就是汇编的由来。
  • 而基于汇编往上,再映射封装相关对应关系。就跨时代性的c语言,再往上层封装,就出现了高级语言oc、swift等语言。所以汇编执行快,因为它是直接转换为机器语言的。
  • 汇编的指令集,是针对同一操作系统而言,它不支持跨平台。机器指令是cpu的在识别。早期的计算机厂家非常多,虽然都用0和1的组合,但相同组合背后却是相应不同的指令。所以汇编无法跨平台不同操作系统下,汇编指令不同的。
  1. 案例创建体验
  • Python 案例:创建python文件夹,新建helloDemo.py文件,
    内容:
print("hello")
  • 可以看出 Python文件内容 可直接执行

  • 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查看文件:
  • 发现.out文件是:64位的Mach-O可执行文件,当前clang出来的是x86_64架构, mac电脑可读。 所以可以./a.out直接执行:

  • 可以看出 C 文件内容 需要编译

二 、 LLVM概述

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

1. 传统编译器的设计

传统编译器的设计

  • 编译器前端(Frontend):

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

  • 优化器(Optimizer)

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

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

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

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

3. LLVM的设计

  • 传统编译器(如CGG )的前端和后端没有完全分离,耦合在了一起,因而如果要支持一门新的语言或硬件平台,需要做大量的工作。

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

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

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

  • LLVM同时支持 AOT 预先编译和 JIT即时编译

  • 不同的前端后端使用统一的中间代码LLVM Intermediate Representation (LLVM IR)
      1. LLVM IR格式以 .ll结尾、以 .bc 的二进制格式结尾、内存格式
      1. Bitcode(Xcode 7之后)就是以.bc结尾的中间代码,是LLVM-IR在磁盘上的一种二进制表示形式。例如:clang -c -emit-llvm xxxx.m 生成 xxxx. bc
      1. 如果要转换成文本格式查看,例如:llvm-dis xxxx.bc -o xxxx.ll
      1. 苹果单独对 Bitcode 进行了额外的优化.
      • i) 应用上传到 AppStore时,Xcode会将程序对应的 Bitcode一起上传;
      • ii) AppStore会将 Bitcode重新编译为可执行程序,供用户下载;
      • iii) Bitcode被Xcode打包成 xar文档,嵌入的 MachO中。
  • 如果需要支持一种新的编程语言、硬件设备,那么只需要实现一个新的前后端

4. Clang简介

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

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

三 、 LLVM案例体验

  • 新建一个Mac OS命令行工程:

  • 没有改动代码

3.1 编译流程

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

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

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

使用安全拓展:

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

3.3 编译阶段

3.3.1 词法分析

  • 编译main.m文件:
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
  • 词法分析,就是根据空格括号这些将代码拆分成一个个Token。标注了位置第几行第几个字符开始的。
Last login: Mon Nov 16 16:43:13 on ttys000
ios@HJ ~ % cd /Users/ios/Desktop/学习资料/hu/TestDemo/TestDemo 
ios@HJ TestDemo % clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

annot_module_include '#import <Foundation/Foundation.h>
#define C 30

int main(int argc, const char * argv[]) {
    @autoreleasepoo'        Loc=<main.m:8:1>
int 'int'    [StartOfLine]  Loc=<main.m:11:1>
identifier 'main'    [LeadingSpace] Loc=<main.m:11:5>
l_paren '('     Loc=<main.m:11:9>
int 'int'       Loc=<main.m:11:10>
identifier 'argc'    [LeadingSpace] Loc=<main.m:11:14>
comma ','       Loc=<main.m:11:18>
const 'const'    [LeadingSpace] Loc=<main.m:11:20>
char 'char'  [LeadingSpace] Loc=<main.m:11:26>
star '*'     [LeadingSpace] Loc=<main.m:11:31>
identifier 'argv'    [LeadingSpace] Loc=<main.m:11:33>
l_square '['        Loc=<main.m:11:37>
r_square ']'        Loc=<main.m:11:38>
r_paren ')'     Loc=<main.m:11:39>
l_brace '{'  [LeadingSpace] Loc=<main.m:11:41>
at '@'   [StartOfLine] [LeadingSpace]   Loc=<main.m:12:5>
identifier 'autoreleasepool'        Loc=<main.m:12:6>
l_brace '{'  [LeadingSpace] Loc=<main.m:12:22>
int 'int'    [StartOfLine] [LeadingSpace]   Loc=<main.m:13:9>
identifier 'a'   [LeadingSpace] Loc=<main.m:13:13>
equal '='    [LeadingSpace] Loc=<main.m:13:15>
numeric_constant '10'    [LeadingSpace] Loc=<main.m:13:17>
semi ';'        Loc=<main.m:13:19>
int 'int'    [StartOfLine] [LeadingSpace]   Loc=<main.m:14:9>
identifier 'b'   [LeadingSpace] Loc=<main.m:14:13>
equal '='    [LeadingSpace] Loc=<main.m:14:15>
numeric_constant '20'    [LeadingSpace] Loc=<main.m:14:17>
semi ';'        Loc=<main.m:14:19>
identifier 'printf'  [StartOfLine] [LeadingSpace]   Loc=<main.m:15:9>
l_paren '('     Loc=<main.m:15:15>
string_literal '"%d"'       Loc=<main.m:15:16>
comma ','       Loc=<main.m:15:20>
identifier 'a'   [LeadingSpace] Loc=<main.m:15:22>
plus '+'     [LeadingSpace] Loc=<main.m:15:24>
identifier 'b'   [LeadingSpace] Loc=<main.m:15:26>
plus '+'     [LeadingSpace] Loc=<main.m:15:28>
numeric_constant '30'    [LeadingSpace] Loc=<main.m:15:30 <Spelling=main.m:9:11>>
r_paren ')'     Loc=<main.m:15:31>
semi ';'        Loc=<main.m:15:32>
r_brace '}'  [StartOfLine] [LeadingSpace]   Loc=<main.m:16:5>
return 'return'  [StartOfLine] [LeadingSpace]   Loc=<main.m:17:5>
numeric_constant '0'     [LeadingSpace] Loc=<main.m:17:12>
semi ';'        Loc=<main.m:17:13>
r_brace '}'  [StartOfLine]  Loc=<main.m:18:1>
eof ''      Loc=<main.m:18:2>
ios@HJ TestDemo % 

3.3.2 语法分析
  • 语法分析是验证语法是否正确
    在词法分析的基础上,将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等,然后将所有节点组成抽象语法树(Abstract Syntax Tree,AST)。 语法分析程序判断源程序结构上是否正确
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
  • 作用域类型运算方式十分清晰。( 语法树一次只能处理一次计算。两次运算,就得多分一层级。)
  • 语法分析,就是在生成语法树完成检测的。
  • 头文件找不到时,可以指定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
  • Q:图中为何多创建那么多局部变量?(如test函数内的a5、a6)
  • 因为在上一阶段(编译阶段),我们将代码编译成了语法树结构。而此时,我们只是沿语法树进行读取。 语法树每一个层级,都需要一个临时变量承接。再返回上一层级处理
  • 所以会产生那么多局部变量
3.4.2 IR优化
  • 我们可以在XcodeBuild Settings中搜索Optimization,可以看到优化级别。
    (Debug模式默认None [O0]无优化,Release模式默认Fastest,Smallest [Os]最快最小)
image
  • 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 最快最小
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
  • 分别对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

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

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

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

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

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

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

  • 通过链接器把编译产生的.o文件和.dylib.a文件链接关联起来,生成真正的mach-o可执行文件
clang main.o -o main // 将目标文件转成可执行文件
file main            // 查看文件
xcrun nm -nm main    // 查看main的符号

image
  • 对比main.o目标文件,此时生成的main文件:
  1. object文件变成了executable可执行文件
  2. 虽然都有undefined,但是可执行文件中指定了该符号来源库。机器在运行时,会从相应的库中取读取符号(printf)

总结:

源代码可执行文件整个流程

image

四. LLVM源码 & 编译流程

【注意】

  1. LLVM源码2.29G编译后文件30G,请确保电脑硬盘空间足够
  2. 编译时,电脑温度会飙升90多度,请用空调伺候着,可能黑屏
  3. 编译时间长达1个多小时,请合理安排时间。

如果以上3点,你确定能接受,那我们就开始吧。

4.1 LLVM下载

4.2 LLVM编译

  • 最新的LLVM只支持cmake编译,需要使用Homebrew安装cmake:
4.2.1 安装cmake
  • 查看brew列表,检查是否安装过cmake,如果有,就跳过此步骤
brew list
  • 如果没有,就使用brew安装:
brew install cmake

如果报权限错误,可sudo chown -Rwhoami:admin /usr/local/share放开权限

image
4.2.2 编译llvm
  • cmake编译成Xcode项目
cd llvm-project
mkdir build
cd build
cmake -G Xcode ../llvm   
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="Release" ../llvm
// 或者: cmake -G Xcode CMAKE_BUILD_TYPE="debug" ../llvm 
  • 成功之后,可以看到生成的Xcode文件:

    image
  • 打开LLVM.xcodeproj,选择自动创建Schemes

    image
  • 自动创建完成后,选择ALL_BUILD进行编译(耗时0.5-1小时,CPU满负荷运转)

    image
  • 编译完成。接下来我们开始创建插件。

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

推荐阅读更多精彩内容