第四十一节—iOS用到的LLVM(二)

本文为L_Ares个人写作,以任何形式转载请表明原文出处。

本文接上一节——iOS用到的LLVM(一)。请对LLVMClang不熟悉的同学们移步上一节,了解了基础的信息之后再阅读本节。

一、准备工作

步骤1 : 使用xcode新建一个空的macOS下的commond Line Tool命令行工具,下面称之为工程1
图1.0.0

注意 :

  1. 这里因为用的是命令行(commond Line Tool),所以初创的情况下没有对其他的框架造成依赖。
  2. 因为没有依赖,所以以下的命令都是不引入其他iOS框架的(包括也没有引入Foundation框架)。
  3. 如果想要引入其他的框架,那么就在clang命令上添加框架的地址。下面是举例的一个命令,引入内容按照自己要使用的框架的情况进行修改即可。

clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk(自己的SDK路径) -fmodules -fsyntax-only -Xclang -ast-dump main.m

步骤2 : 打开terminal终端,进入到刚创建的这个项目中main.m所在的文件夹下。
步骤3 : 在terminal终端中输入clang查看详细编译步骤的指令。
 clang -ccc-print-phases main.m

图片未必看的清楚,我把内容拷贝下来了,下面称之为内容1

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

解释

  1. inputpreprocessorcompilerbackendassemblerlinkerbind-arch,这些东西表示的是编译中的操作名称。
  2. main.m{0}.....{5},这些东西表示的是这一步操作中要读取的文件,也就是上一步操作的结果文件。
  3. objective-cobjective-c-cpp-outputirassemblerobjectimage等这些东西就是本步操作完成后,生成的文件,也就是上面2中说的上一步操作的结果文件

对于这个工程,一共有0~6一共7个阶段。这就是main.m这个文件从源码到机器语言的总的流程。下面开始按照流程来说。

二、源码的编译流程

2.1 编译总流程

命令 :

 clang -ccc-print-phases main.m

编译总流程就是上面的内容1

先阐述总流程0~6中都是什么 :

0: 输入文件   : "找到源文件"。
1: 预处理阶段  : 这个阶段处理了"宏的替换"和"头文件的导入"。
2: 编译阶段   : 进行"词法分析"、"语法分析","语义分析"。最重要的是要"
生成中间代码IR"。
3: 后端       : LLVM在这里会"通过一个一个的Pass去优化传入的IR",每个Pass做一些事情,最终生成汇编代码。
4: 生成汇编代码。
5: 链接       : "链接需要的动态库和静态库,生成可执行文件"。
6: 最后一步,"通过不同的架构,生成对应的可执行文件"。

这个步骤与之前经常提及的编译流程,0~6步分别对应着 :

源文件(0)-->预编译(1)-->编译(2)-->汇编(3,4)-->链接(5)-->生成可执行文件(6)

2.2 预处理阶段

在2.1的总流程中说过,预处理阶段要做的事情有两件 :

  • 宏的替换
  • 头文件的导入

举例

  1. 打开工程1,定义一个宏#define JD_NUM 10
  2. 因为Xcode自带的头文件引入#import <Foundation/Foundation.h>是导入Foundation框架Foundation框架太大了而且现在我们不需要用,所以头文件引用就换成#import <stdio.h>
  3. commond + s保存一下。
  4. 打开terminal终端,进入main.m所在的文件夹下。
  5. 键入clang指令查看预处理阶段的详细步骤。命令如下 (详细的Clang命令解释可以看上一节中的Clang常用指令)。
  6. 操作图如下图2.2.0
图2.2.0
Clang命令 :
clang -E main.m >> main2.m

解释 :

现在main.m的文件夹下就会出现main2.m文件,它就是经过预处理阶段操作之后的结果。如下图2.2.1。

图2.2.1
  1. 打开main2.m文件,拉到文件的最后,找到main函数入口。结果如下图2.2.2
图2.2.2
问 : typedef是不是预处理阶段进行的处理?

其实这里通过对#definetypedef本身的概念了解就知道是不一样的,typedef本身是存储类关键字,本质上并不属于宏或头文件。预处理阶段并不会对关键字做解释。

简单验证一下 :

  1. 工程1中加入typedef int JD_USE_INT,将int类型创建别名为JD_USE_INT。以后工程1改叫工程2
  2. commons + s保存工程2的代码。
  3. 依然使用clang -E main.m >> main2.m指令,得到main2.m
  4. 打开main2.m直接找到文本最后的main函数入口
  5. 操作图如下图2.2.3
  6. 结果图如下图2.2.4。
图2.2.3
图2.2.4

2.3 编译阶段

编译阶段的主要任务有3个 :

  • 词法分析 : 将预处理阶段传过来的源码的字符序列一个一个的读入源程序,然后根据构词规则转换成单词序列(Token)。
  • 语法分析 : 在词法分析的基础上,将单词序列组合成各类语法短句。例如 : 程序、语句、表达式等。然后将所有的语句节点抽象出来,生成抽象语法树(AST),再检查源程序的结构是否符合语法规则。
  • 生成中间代码IR : 完成上述步骤以后,代码生成器会将抽象语法树(AST)自上而下的遍历,逐步将其转换成LLVM IR

举例

1. 词法分析

  1. terminal终端cd进入新的工程2main.m所在文件夹下。
  2. 输入以下clang指令,查看词法分析。
  3. 源代码图为上图2.2.3,clang结果图为下图2.3.0
  4. 这里注意,空格也算一个位置。
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
图2.3.0.png

2. 语法分析

  1. terminal终端cd进入工程2main.m所在文件夹下。
  2. 输入以下clang指令,查看语法分析。
  3. 结果如下图2.3.1
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
图2.3.1
  1. FunctionDecl : 方法节点

    • <line:7:1, line:13:1> :
      方法节点的代码范围是第7行第1个字符第13行第1个字符
    • line:7:5 main 'int (int, const char **)' :
      第7行第5个字符的位置开始,是main方法的位置,第一个int表示main方法的返回值类型,(int, const char **)表示main方法的参数类型。
  2. ParmVarDecl : 参数节点

    • <col:10, col:14> :
      参数节点因为与main方法在同一行,所以不再说明是第7行
      直接说明第一个参数的位置是第10个字符开始,到第14个字符为止。
    • argc 'int' :
      参数名称是argc,参数类型是int
    • 上述是第一个参数的解释,下面的第二个参数相同,不再赘述。
  3. CompoundStmt : 围栏,也可以说是范围,代表的就是main方法的{ }函数块区域。

  4. ObjCAutoreleasePoolStmt : 自动释放池。

  5. VarDecl : 变量节点。内容比较简单,可以自行理解一下。

  6. CallExpr : 调用函数,这一行后面的int代表这个函数的返回值的类型是int。这里借助一下图片,如下图2.3.2。

图2.3.2
  1. BinaryOperator : 函数的第二个参数,这叫字节运算符。这一行表示这个字节运算符是加法运算。表明函数的第二个参数是一个加法运算的结果。再看下图2.3.3
图2.3.3
  1. ReturnStmt : 返回节点。
  2. IntegerLiteral : 整型。
  3. 语法分析阶段会对代码中的错误进行提示。例如将工程1的代码去掉一个;,重新运行语法分析的clang指令,结果如下图2.3.4,明显提示少了一个;,在第10行的第42个字符处。
图2.3.4

3. 生成中间代码LLVM IR

IR的基本语法
语法 释义
; 注释
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32bit,4字节
store 写入内存
load 载入内存
call 调用函数
ret 返回
操作
  1. 修改工程2的代码,多添加一个函数,方便把所有的IR代码的语法都了解一遍。新的工程命名为工程3工程3代码如下 :
#import <stdio.h>

int sumFunc(int a, int b) {
    return a + b + 3;
}

int main(int argc, const char * argv[]) {
    int c = sumFunc(1, 2);
    printf("%d",c);
    return 0;
}

  1. commond + S保存工程3的代码。
  2. terminal终端cd进入工程3所在文件夹下。
  3. 输入以下clang指令,生成IR文件。
  4. clang指令执行完成后,会在main.m文件所在的文件夹下生成main.ll文件。
  5. 生成main.ll的结果如下图2.3.5。
clang -S -fobjc-arc -emit-llvm main.m
图2.3.5.png
  1. 可以利用Sublime Text打开main.ll文件,并将Sunlime Text软件右下角的Plain Text改成Objective-C的格式。结果如下图2.3.6。
图2.3.6.png

2.4 优化器

我们知道了ClangLLVM的前端,Clang做了2.2预处理阶段2.3编译阶段的事情,那么从哪里开始算是LLVM的后端?

优化器(Optimizer)和代码生成器(CodeGenerator)都可以算作LLVM后端

  • 后端的作用 :
    (1). 优化。
    2.3编译阶段最后生成的LLVM IR代码传入一个一个的Pass进行IR优化,每个Pass都会对传入的IR进行本Pass要做的优化。
    (2). 生成汇编代码。
    完成所有所需Pass优化的IR将会变成汇编代码
  • 什么是Pass?
    (1). 首先,Pass是节点。是LLVM优化过程中的优化逻辑所在之处。
    (2). 其次,Pass是属于LLVM的后端(Backend)的。
    (3). 最后,LLVM的优化是以节点(Pass)来完成的,是一个节点一个节点去完成的,所有节点一起合作之后,才完成了LLVM的优化的转化。
    例如 : 有的节点是负责运算之后将冗余的代码减去的,有的节点则是负责跳转之后再减去冗余代码的。
  • 什么是bitCode?
    (1). 苹果在xcode7之后可以开启bitCode,在iOS中,我们说bitCode是苹果对LLVM在编译阶段生成的IR的一种特殊形式,本质上bitCode也是IR,也是中间代码,它以二进制形式存在,苹果推出bitCode就是一种官方的优化方式。
    (2). 在经过bitCode的优化之后,IR代码文件会转化成.bc文件格式的中间代码。

举例

很明显,通过2.3编译阶段生成的IR在阅读理解上是很冗余的,短短的几行简单的代码都变得很长,所以LLVM中存在对IR代码进行一些适当的优化,当然这个优化在xcode上面是可选择的。还是选择以工程3为基本,如图2.4.0。

图2.4.0.png

xcode是带有对IR代码是否进行优化的可视化界面的,一般情况下,Debug模式下默认都是没有开启代码优化,而Release模式下,则开启了优化。

4.1 LLVM的优化级别
级别 释义
O0 None,不进行IR优化
O1 Fast
O2 Faster
O3 Fastest
Os Fastest , Smallest
Ofast 比Os还要更近一步的优化
Oz 让IR代码体积最小的优化

注释 : 级别的中的O是英文字母,不是数字0。

4.2 利用命令行对IR进行优化的举例

还是利用工程3,我们就不直接利用xcode的优化了,为了看到优化的IR代码,利用终端的命令行对IR代码进行优化。

  1. 利用终端,进入到工程3main.m所在文件夹下。
  2. 终端中输入以下clang命令
clang -Os -S -fobjc-arc -emit-llvm main.m
  1. 依然利用Sublime Text打开main.ll文件,调整成OC的语法格式。
  1. 结果如下图2.4.1所示。
图2.4.1.png
4.3 bitCode的生成

还是利用工程3

  1. 利用终端进入工程3main.m所在的文件夹下。
  2. 终端中输入以下clang命令,先生成IRmain.ll文件。
clang -S -fobjc-arc -emit-llvm main.m
  1. 再在终端中输入以下clang命令,利用main.ll文件生成main.bc文件。
clang -emit-llvm -c main.ll -o main.bc
  1. 生成的结果如下图2.4.2所示。
图2.4.2.png

2.5 汇编

2.5.1 直接生成汇编

直接利用上面图2.4.2中的3个文件。

  1. .m格式的源文件转化为汇编代码,利用下述命令。
clang -S -fobjc-arc main.m -o main.s
  1. .ll格式的IR代码文件转化为汇编代码,利用下述命令。
clang -S -fobjc-arc main.ll -o main1.s
  1. .bc格式的bitCode优化后的文件转化为汇编代码,利用下述命令。
clang -S -fobjc-arc main.bc -o main2.s

结果如下图2.5.0和2.5.1所示

图2.5.0.png
图2.5.1.png

2.5.2 生成汇编可进行优化

生成汇编进行的优化是对机器语言的优化。

我们已经知道,源码变成汇编的过程要经过 : 源码 --> IR --> bitcode --> 汇编,其实除了在源码 --> IR的时候可以进行优化,在生成汇编的时候,系统还是会进行一步优化,我们在上一节的传统优化器的设计中说过后端/代码生成器也有优化能力。

还是利用工程3的源码。并且优化的级别统一选定为最高级别Os,其他的级别自行更换尝试。

  1. 源码直接生成汇编的优化
clang -Os -S -fobjc-arc main.m -o main3.s

对比main.m未经过优化和经过优化分别生成的汇编main.smain3.s :

图2.5.2.png
  1. IR生成汇编的优化
clang -Os -S -fobjc-arc main.ll -o main4.s
图2.5.3.png
  1. bc生成汇编的优化
clang -Os -S -fobjc-arc main.bc -o main5.s
图2.5.4.png

因为我们的源码只有最简单的11行,所以优化的效果不会有那么的大,但也可以看得出来优化的效果还是很好的。

但是!!!这里我们正常的情况下是不可以手动的进行调节的。

对比IR的优化来看,IR的优化我们可以在xcode中就可以进行配置,就是上面的图2.4.0,而生成汇编的时候进行的优化,我们没有办法人工的干预。

2.6 生成目标文件和生成可执行文件(链接)

以下所有的操作都是以工程3为基础的。

2.6.1 生成目标文件

目标文件的生成是汇编器以汇编代码作为输入,将汇编代码转换成机器代码,最后输出目标文件(object file)

常用命令是 :

clang -fmodules -c main.s -o main.o

命令结果 :

图2.6.0.png

查看目标文件main.o的符号的命令 :

xcrun nm -nm main.o

命令结果 :

图2.6.1.png
  1. undefined : 表示在当前文件,暂时找不到某个符号,比如在上图2.6.1中就是说找不到_printf这个符号,也就是找不到printf这个方法。

  2. external : 表示这个符号是外部可以访问的。比如上图的2.6.1中找不到的_printf这个符号是可以在外部访问的到的,也就是说printf这个方法不是本文件的方法,但是是可以经过外部的文件找得到的方法。

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

我们知道,可执行文件的生成就是由很多的.o文件来完成的。这些.o文件要集合在一起需要要存在一些的联系,而这个联系就是由链接(linker)来做到的。

连接器把编译产生的.o文件和.dylib.a文件生成一个mach-o文件。

用下述命令生成可执行文件 :

clang main.o -o main

生成可执行文件的结果 :

图2.6.2.png

链接之后,我们再查看可执行文件的符号,对比目标文件来看。

查看可执行文件的符号的命令 :

xcrun nm -nm main

结果图 :

图2.6.3.png

从图3.6.3中可以看到,虽然undefined标识是依然存在的,但是后面的括号中已经告诉我们_printf符号是来自于libSystem的。

那为什么要有这个from libSystem呢?

因为当这个可执行文件main要被执行的时候,main内部有一个符号_printf是来自于外部,当要调用这个_printf的时候,dyld会在加载的时候进行绑定,而如何绑定呢?就会根据符号提供的位置,也就是(from libSystem)来确定_printf符号是来自于libSystem的,这时iOS的操作系统中的libSystem动态库就会把_printf的地址告诉dyld,然后进行符号的绑定。

所以说,这个符号是在运行的时候动态绑定的。这也是为什么fishhook可以去hook一些外部函数的原因。

main这个可执行文件生成之后,我们就可以直接执行这个main,命令行如下 :

./main

结果如下图 :

图2.6.4.png

也可以查看一下main的基本信息,比如它的格式、版本信息、运行所需的系统要求等,命令行 :

file main

结果如下图 :

图2.6.5.png

可以看到main的文件格式是Mach-O,是64位的x86架构下可运行的,也就是说main是一个单一架构的文件不是胖二进制文件。

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

推荐阅读更多精彩内容