美文网首页
第四十一节—iOS用到的LLVM(二)

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

作者: L_Ares | 来源:发表于2021-10-27 19:21 被阅读0次

    本文为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是一个单一架构的文件不是胖二进制文件。

    相关文章

      网友评论

          本文标题:第四十一节—iOS用到的LLVM(二)

          本文链接:https://www.haomeiwen.com/subject/lrmnzktx.html