iOS编译原理篇“LLVM & Clang”

7,886 阅读7分钟

1.传统编译器设计

image-3.png

1.1编译器前端

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

1.2优化器

优化器负责各种优化,缩小包的体积(剥离符号 改善代码的运行时间( 消除冗余计算、减少指针跳转次数等)。

1.3后端,代码生成器

后端将代码映射到目标指令集,生成机器语言,并且进行机器相关的代码优化。

由于传统的编译器(如GCC) 是为整体的应用程序设计的,不支持多种语言或者多种硬件架构,所以他们的用途收到了很大的限制。

2.LLVM的设计

LLVM 是编译器的架构系统,C++ 编写而成的。用于优化以任意语言编写的程序的编译时间(compile-time)、连接时间(link-time)、运行时间(run-time)、空闲时间 (idle-time)

LLVM设计中最重要的设计是使用了通用的代码表示形式(IR), 在需要支持一种新的语言 只需要再编写一个可以产生IR的 独立 前端; 需要支持一种新的硬件架构 只需要再编写一个可以接收IR的 独立后端

image-4.png

3.Clang

Clang 是一个由 Apple 主动编写,是LLVM项目中的一个子项目。基于 LLVM 的轻量级编译器,之初是为了替代GCC,提供更快的编译速度。他是负责编译C、C++、OC语言的编译器 。 

3.1 编译流程

可以通过下面的命令打印源码的编译过程:


clang -ccc-print-phases main.m

打印结果如下:

image-5.png

整个过程中,没有明确指出优化器,是因为优化已经分布在前后端里面了。

image-6.png

接下来对每个步骤,详细分析:

0: 输入源文件:

找到源文件

1:预处理阶段:

执行预处理指令,包括进行宏替换、头文件的导入、条件编译,产生新的源码给到编译器.可以通过命令clang -E main.m,看到执行预处理指令后的代码。

2:编译阶段 -> IR(.ll) 或者 bitcode(.bc)文件:

进行词法分析、语法分析、语义分析、检测语法是否正确、生成AST、生成IR(bitcode)

2.1 词法分析: 

预处理完成后,进行词法分析,将代码分隔成一个个的Token及标明其所在的行数和列数,包括关键字、类名、方法名、变量名、括号、运算符等

使用下面的命令,可以看到词法分析后的结果:


clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

image-7.png

2.2语法分析

词法分析后,是语法分析,它的任务是验证源程序的语法结构的正确性,在词法分析的基础上,将单词组合成各类语法短语,如语句、表达式等。然后将所有节点组成抽象语法树(AST)。

通过下面的命令,可以查看语法分析后的结果:


clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

// 如果导入头文件找不到,可以指定SDK
clang -isysroot sdk路径 -fmodules -fsyntax-only -Xclang -ast-dump main.m

image-8.png

2.3 生成IR

语法树自顶向下遍历逐步翻译成LLVM IR。OC 代码在这一步会进行运行时的处理,比如 分类属性的合成、ARC处理等

通过下面的命令可以生成  .ll 的文本文件,查看IR代码:


clang -S -fobjc-arc -emit-llvm main.m

image-9.png

上面IR代码的意思是

  1. test 方法,输入两个参数 %0, %1
  2. 创建两个变量 %3,%4
  1. 将%0的数据写入到%3中,将%1的数据写入到%4中
  2. 读取%3的数据并赋值给%5,读取%4的数据并赋值给%6
  1. 将%5 加上 %6的结果赋值给%7
  2. 将%7 加上 3 的结果赋值给%8
  1. 返回%8

IR 优化

在上面的IR代码中,可以看到,通过一点一点翻译语法树,生成的IR代码,看起来有点蠢,其实是可以优化的。

IR优化等级从低到高分别是: -O0 -O1 -O2 -O3 -Os -Ofast -Oz

可以使用命令进行优化:


clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll

也可以在 xcode设置:target -> build Setting -> Optimization Level

image-10.png

我们看一下Os级别优化后的代码:

image-11.png

上面IR代码的意思是

  1. 将%0 加上 3 结果赋值给%3
  2. 将%3加上%1的结果赋值给%4
  1. 返回%4

优化后的IR代码,更加的简洁明了了!

bitcode

xcode7之后,如果开启了bitcode, 苹果会对IR文件做进一步的优化 生成.bc 文件的中间代码。

命令如下:


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

3:后端阶段->汇编文件(.s):

后端将接收到的 IR 结构化成不同的处理对象,并将其处理过程实现为一个个的Pass类型。通过处理 Pass ,来完成对IR的转换、分析和优化。然后生成汇编代码。

命令如下:


// bitcode -> .s  
clang -S -fobjc-arc main.bc -o main.s
// IR -> .s  
clang -S -fobjc-arc main.ll -o main.s
// 也可以对汇编代码进行优化
clang -Os -S -fobjc-arc main.ll -o main.s

image-12.png

4:汇编阶段->生成目标文件(.o):

汇编器将汇编代码转换成机器码,生成一个个的目标文件的Mach-O文件

命令如下:

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

通过 nm 的命令,查看main.o的mach-O文件的符号


xcrun nm -nm main.o

打印结果如下:

image-13.png

可以看到执行命令时,报了一个错:没找到外部的 _printf 的符号。 􏰋􏵘􏵙􏰆因为这个函数是在外部引入的,这里需要把使用到的其他的库也 链接过来,才能不报错􏰱。

5:链接阶段-> 可执行的Mach-O文件:

将一个个的目标文件链接到一起,并且链接需要的动态库(.dylib)和静态库(.a) ,生成可执行文件 -(Mach-O)文件。

命令如下:

clang main.o -o main

image-14.png

可以看到打印结果中依然显示没有找到外部符号 printf , 但是后面多了(from libsystem)。指明_printf 所在的库是 libsystem。 这是因为libsystem动态库, 需要在运行时动态绑定 目前这个文件已经是一个正确的可执行文件了。

使用如下命令执行:

./main

执行结果:

image-15.png

6:绑定硬件架构:

根据x86_64硬件架构生成对应的可执行文件 (Mach-O)

总结编译流程

1. 各阶段使用的命令:



//// ====== 前端 开始=====
// 1. 词法分析
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

// 2. 语法分析
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

// 3. 生成IR文件
clang -S -fobjc-arc -emit-llvm main.m

// 3.1 指定优化级别生成IR文件
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll

// 3.2 (根据编译器设置) 生成bitcode 文件
clang -emit-llvm -c main.ll -o main.bc


//// ====== 后端 开始=====

// 1. 生成汇编文件
// bitcode -> .s  
clang -S -fobjc-arc main.bc -o main.s
// IR -> .s  
clang -S -fobjc-arc main.ll -o main.s
// 指定优化级别生成汇编文件
clang -Os -S -fobjc-arc main.ll -o main.s


// 2. 生成目标Mach-O文件
clang -fmodules -c main.s -o main.o
// 2.1 查看Mach-O文件
xcrun nm -nm main.o

// 3. 生成可执行Mach-O文件
clang main.o -o main


//// ====== 执行 开始=====
// 4. 执行可执行Mach-O文件
./main

2. 各个阶段生成的文件类型:

image-16.png

3.编译流程图示:

image-17.png

3.2 OC 生成C++文件

  • 功能: 可以把 OC  文件编译成 C++文件。 例如 main.m  编译成 main.cpp  文件,用来更好的查看代码的底层结构及实现逻辑,便于了解底层原理。
  • 编译方式: 在终端中进入需要编译的文件所在目录,执行文件编译命令:

//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp

//2、将 ViewController.m 编译成  ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 

举个🌰:


- (instancetype)init {
    self = [super init];
    if (self) {
        NSLog(@"%@-----%@", [self class], [super class]);
    }
    return self;
}

编译后:

static instancetype _I_LGPerson_init(LGPerson * self, SEL _cmd) {
    self = ((LGPerson *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGPerson"))}, sel_registerName("init"));
    if (self) {
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_86_0y_j3bzj65z6vw6hy1chw_4m0000gp_T_LGPerson_1615a9_mi_0, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")), ((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGPerson"))}, sel_registerName("class")));
    }
    return self;
}

以上是小编给大家整理的编译资料,希望在以后的程序生涯中对你有所帮助。 青山不改,绿水长流,后会有期,感谢每一位佳人的支持!