iOS 底层原理 iOS 进阶之路

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

2020-11-15  本文已影响0人  markhetao

OC底层原理 学习大纲

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

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

1 什么是编译器?

1.1 Python案例

print("hello")

1.2 C 案例

#include <stdio.h>
int main(int a, char * argv[]) {
        printf("hello \n");
        return 0;
}

发现.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概述

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的设计

LLVM的设计

2.3 Clang

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

3. LLVM案例体验

3.1 编译流程

clang -ccc-print-phases main.m
image.png
编译流程分为以下7步

optimizer优化没有作为一个独立阶段,在编译阶段内部完成

3.2 预处理阶段

#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 -E main.m >> main2.m

预编译阶段: 1. 导入头文件 2.替换宏

#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;
}

安全拓展:

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

3.3 编译阶段

3.3.1 词法分析
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

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

image.png
3.3.2 语法分析
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 生成中间代码
#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 返回
image.png
  • Q:图中为何多创建那么多局部变量?(如test函数内的a5、a6)
  • 因为在上一阶段(编译阶段),我们将代码编译成了语法树结构。而此时,我们只是沿语法树进行读取。 语法树每一个层级,都需要一个临时变量承接。再返回上一层级处理
  • 所以会产生那么多局部变量
3.4.2 IR优化
image.png
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再优化

优化体现:上传APPstore的包,针对不同型号手机做了区分,不同型号手机下载时,大小不同

clang -emit-llvm -c main.ll -o main.bc

3.5 生成汇编代码

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 生成目标文件(机器代码)

clang -fmodules -c main.s -o main.o
  • file对比一下main.s汇编代码和main.o机器代码:
file main3.m
file main.o
image.png
xcrun nm -nm main.o
image.png

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

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

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

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
上一篇 下一篇

猜你喜欢

热点阅读