源码编译的过程-Clang-LLVM下,一个源文件的编译过程

什么是 LLVM?

LLVM 是编译器工具链技术的集合。 lld项目是外部链接器

编译器编译每个文件以产生Mach-O(可执行文件); 链接器将项目中的多个 Mach-O 文件合并为一个。

Xcode运行的过程就是执行一些命令脚本。 下面的截图是Xcode编译main.m的脚本。

找到bin目录下的clang命令,在前面添加一些参数,比如哪种语言,编译哪个框架,添加Xcode中设置的配置参数,最后输出为.o文件。

LLVM编译器架构

编译器分为三个部分,编译器后端、通用优化器和编译器前端。 中间的优化器不会改变

添加语言只需要处理编译器后端。

添加框架只需添加编译器前端框架处理即可

clang代表了C、C++、Objective-C在编译器架构中的后端,同时也充当了命令行中的“黑匣子”驱动,它封装了编译管道、前端命令、LLVM命令、工具链命令、等等。LLVM将执行上面描述的整个编译过程。 大致流程如下: OC源文件编译流程

使用以下命令查看OC源文件的编译过程

clang -ccc-print-phases main.m

0:先找到main.m文件

1:预处理器是替换include、import、宏定义

2:编译成IR中间代码

3:将中间代码交给前端生成汇编代码

4:汇编生成目标代码

5:链接静态库、动态库

6:适合某个框架的代码

预处理

使用以下命令查看预处理阶段完成的工作

clang -E main.m

预处理主要做了以下几件事:

1、删除所有#defines,代码中使用宏定义的地方将被替换

2. 将#include 中包含的文件插入该文件的位置。 插入过程是递归的

3.删除注释符号和注释

4.添加行号和文件标签,方便调试

编译编译的过程就是对预处理后的文件进行词法分析、句法分析、语义分析和优化,形成相应的汇编代码 1、词法分析

此步骤将源文件中的代码转换为特殊的标记流。 源代码被一一分为字符和短语。 行尾Loc标注了对应的源文件以及源代码所在的具体行数,方便报错。 定位问题。

使用以下命令进行词法分析

clang -Xclang -dump-tokens main.m

以下面的代码为例:

这个源代码在第11行

int main(int argc, char * argv[]) {

通过词法分析,会转化为以下特殊token

int 'int'	 [StartOfLine]	Loc=
identifier 'main' [LeadingSpace] Loc=
l_paren '(' Loc=
int 'int' Loc=
identifier 'argc' [LeadingSpace] Loc=
comma ',' Loc=
char 'char' [LeadingSpace] Loc=
star '*' [LeadingSpace] Loc=
identifier 'argv' [LeadingSpace] Loc=
l_square '[' Loc=
r_square ']' Loc=
r_paren ')' Loc=
l_brace '{' [LeadingSpace] Loc=

2. 语法分析

这一步是根据词法分析将token流解析成语法树,由Clang中的Parser和Sema模块完成。

其上的每个节点也标记了其在源代码中的位置

验证句型是否正确,如漏一项; 报告错误信息

根据当前语言的句型,生词语义节点,并将所有节点组合成抽象语法树

使用以下命令进行语法分析

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

会被解析成如下语法树

-FunctionDecl 0x7ffe251a8ce0 
line:11:5 main 'int (int, char **)' |-ParmVarDecl 0x7ffe251a8b00 col:14 argc 'int' |-ParmVarDecl 0x7ffe251a8bc0 col:27 argv 'char **':'char **' `-CompoundStmt 0x7ffe251a9200 |-ObjCAutoreleasePoolStmt 0x7ffe251a91b8 | `-CompoundStmt 0x7ffe251a9188 | |-DeclStmt 0x7ffe251a8e30 | | `-VarDecl 0x7ffe251a8da8 line:14:13 used eight 'int' cinit | | `-IntegerLiteral 0x7ffe251a8e10 'int' 8 | |-DeclStmt 0x7ffe251a8ee8 | | `-VarDecl 0x7ffe251a8e60 col:13 used six 'int' cinit | | `-IntegerLiteral 0x7ffe251a8ec8 'int' 6 | |-DeclStmt 0x7ffe251a9010 | | `-VarDecl 0x7ffe251a8f18 col:13 used rank 'int' cinit | | `-BinaryOperator 0x7ffe251a8ff0 'int' '+' | | |-ImplicitCastExpr 0x7ffe251a8fc0 'int' | | | `-DeclRefExpr 0x7ffe251a8f80 'int' lvalue Var 0x7ffe251a8da8 'eight' 'int' | | `-ImplicitCastExpr 0x7ffe251a8fd8 'int' | | `-DeclRefExpr 0x7ffe251a8fa0 'int' lvalue Var 0x7ffe251a8e60 'six' 'int' | `-CallExpr 0x7ffe251a9128 'void' | |-ImplicitCastExpr 0x7ffe251a9110 'void (*)(id, ...)' | | `-DeclRefExpr 0x7ffe251a9028 'void (id, ...)' Function 0x7ffe20b20e88 'NSLog' 'void (id, ...)' | |-ImplicitCastExpr 0x7ffe251a9158 'id':'id' | | `-ObjCStringLiteral 0x7ffe251a9068 'NSString *' | | `-StringLiteral 0x7ffe251a9048 'char [8]' lvalue "rank-%d" | `-ImplicitCastExpr 0x7ffe251a9170 'int' | `-DeclRefExpr 0x7ffe251a9088 'int' lvalue Var 0x7ffe251a8f18 'rank' 'int' `-ReturnStmt 0x7ffe251a91f0 `-IntegerLiteral 0x7ffe251a91d0 'int' 0

3.静态分析(代码静态分析通过语法树查找非语法错误) 1.错误检测

称为出现和未定义、已定义和未使用的变量

2. 类型检测

类型一般分为两类:动态和静态。 动态检测在运行时进行,静态检测在编译时进行。 在编写代码时,您可以向任何对象发送任何消息,并且在运行时,您将检查该对象是否仍然可以响应此类消息。

4. CodeGen - IR 代码生成 1. CodeGen 负责从上到下遍历语法树并将其翻译为 LLVM IR2。 LLVM IR 是 Frontend 的输出和 LLVM Backend 的输入。 前端和Objective-C Runtime的桥接语言 桥接和Objective-C Runtime桥接的应用

1、Objective-C中的Class/Meta Class/Protocol/Category结构体的内存结构是在这一步生成的,并放置在Mach-O指定的Section中(如Class:_DATA,_objc _classrefs),这个DATA段将还存储一些静态变量

2. objct对象发送的消息会被编译成什么? 它将被编译成 objc_msgSend 调用。 发生这一步,语法树中的ObjCMessageExpr被翻译成对应版本的objc_msgSend,对super关键字的调用被翻译成objc_msgSendSuper

3、根据修饰符strong/weak/copy/atomic,合成@property手动实现的getter/setter,@synthesize的过程也是在这一步完成的

4、生成block_layout的数据结构、变量的捕获(__block/和__weak)、生成_block_invoke函数都发生在这一步

5、总说ARC是编译器帮我们插入一些内存管理代码,也是在这一步完成的。

ARC:分析对象的引用关系,插入objc_StoreStrong / Objc_StoreWeak等ARC代码

将 ObjCAutotreleasePoolStmt 转换为 objc_autoreleasePoolPush/Pop

实现手动调用[super dealloc]

用ivar综合每个Class的.cxx_destructor方法来手动释放该类的成员变量,而不是MRC时代的“self.xxx = nil”

源码编译的过程-Clang-LLVM下,一个源文件的编译过程

LLVM中间产品和优化

使用以下命令生成LLVM中间产物IR(Intermediate Representation),并复制此过程

clang -O3 -S -emit-llvm main.m -o main.ll

使用以下命令,代码将使用 LLVM 进行优化。

//针对全局变量优化、循环优化、尾递归优化等。
//在 Xcode 的编译设置里也可以设置优化级别-01,-03,-0s,还可以写些自己的 Pass。
clang -emit-llvm -c main.m -o main.bc

生成汇编代码

使用以下命令生成相应的汇编代码。

clang -S -fobjc-arc main.m -o main.s

至此,编译阶段完成,将编写的代码转换为机器可以识别的汇编代码。 汇编器将汇编代码转换成机器可以执行的指令。 每个汇编语句几乎对应一条机器指令。 根据汇编指令和机器指令的对照表一一翻译就够了。

使用以下命令生成对应的目标文件。

clang -fmodules -c main.m -o main.o

后来Xcode新建的工程中没有pch文件,为什么?

pch文件就是用pch文件导入UIKit、Foundation等库,这样就不需要在每个源文件中解析那么多东西了。 现在iOS正在搞乱一些全局变量和它自己的模块中的一些东西。 把它放在上面。

Xcode上有模块的概念,并且各个设置也是开放的。 默认情况下,库被打包为模块,尤其是 UIKit 和 Foundation 等库都是模块。 好处是,我加上这个参数(fmodules)后,我也会手动把#import改成@import,现在的编译会比最早不带pch的快很多,因为pch的时候默认不会出现出现

$clang -E -fmodules main.m //加入fmodules参数生成可执行文件

关联

该阶段是将上一阶段生成的目标文件与引用的静态库进行链接,最终生成可执行文件。 链接器解决了目标文件和库之间的链接。

链接器在编译时做了什么?

1. Mach-O主要包含代码和数据。 代码是函数的定义,数据是全局变量的定义。 代码和数据都通过符号关联起来。

2、对于Mach-O上面的代码,要操作的变量和函数必须绑定到各自的地址。 链接器的作用是完成变量和函数的符号及其地址的绑定。

为什么要进行符号绑定?

源码编译的过程-Clang-LLVM下,一个源文件的编译过程

1、如果地址和符号没有绑定,为了让机器知道你操作的是哪个地址,需要在写代码的时候设置内存地址。

2.可读性差,修改代码后需要重新维护地址

3、需要针对不同平台编写多段代码,相当于直接写汇编

为什么要将项目中的多个 Mach-O 合并为一个?

1.多个文件之间的变量和套接字是相互依赖的源码编译的过程,因此链接器需要绑定项目中多个Mach-O文件的符号和地址。

2、如果不绑定,单个文件生成的Mach-O将难以运行。 当运行时遇到调用其他文件的函数实现时,将找不到函数地址。

3.链接多个目标文件,创建符号表,记录所有已定义和未定义的符号。 如果出现相同的符号,则会出现错误消息“ld:重复符号”。 如果目标文件中没有符号,如果找到符号,则会提示“未定义符号”的错误信息。

链接器对代码执行的主要操作是什么?

1.进入代码文件查找未定义的变量

2、收集所有符号定义和引用地址,放入全局符号表中

3.计算合并后的宽度和位置,生成相同类型的线段进行合并,并建立绑定

4、项目中不同文件中变量的地址重定位

链接器如何消除无用函数并保证Mach-O的大小? 当链接器安排函数的调用关系时,它会跟踪来自主函数的每个引用,并将其标记为活动状态。 后续完成后,这些没有标记为live的函数都是无用函数。总结:一个源文件的编译过程

代码练习

#import 
int main() {
    NSLog(@"hello world!");
    return 0;
}

1.生成Mach-O可执行文件

clang -fmodules main.m -o main

2.生成抽象语法树

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

3.生成汇编代码

源码编译的过程-Clang-LLVM下,一个源文件的编译过程

clang -S main.m -o main.s

加载和链接

一个App从可执行文件到实际的启动运行代码,基本上需要经历加载和动态库链接两个步骤。

程序运行时,会有独立的虚拟地址空间,操作系统上会同时运行多个进程,它们之间的虚拟地址空间是隔离的。

加载是将可执行文件映射到虚拟内存的过程。 由于显存资源稀缺,只将程序中最常用的部分保存在显存中,不太常用的数据则放在C盘中。 这也是一个动态加载的过程。

加载过程就是流程构建的过程。 操作系统主要做了三件事:

1.创建独立的虚拟地址

2、读取可执行文件头,建立虚拟空间与可执行文件的映射关系

3、设置CPU的寄存器区域作为可执行文件的入口地址,开始运行

静态库

静态库是在编译时链接的库,需要链接到您的 Mach-O 文件中。 如果需要更新,必须重新编译。 它不能动态加载和更新。

动态库

动态库是在运行时链接的库。 动态加载可以使用dyld来实现。 iOS中的系统库都是动态链接的。

共享缓存

Mach-O是编译后的产品,动态库只能在运行时链接。 Mach-O 中没有动态库的符号定义。

Mach-O中动态库中的符号是未定义的,但记录了它们的名称和对应的库路径。

运行时用dlopen和dlsym导入动态库时源码编译的过程,首先根据记录的库路径找到对应的库,然后通过记录的名称和子符号找到绑定地址。

优势:

代码共享、易于维护、减少可执行文件大小

参考:

iOS底层学习——从编译到启动的奇幻旅程

sunnyxx 的 clang 视频分享

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 源码编译 源码编译的过程-Clang-LLVM下,一个源文件的编译过程 https://www.wkzy.net/game/163919.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务