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

LLVM是什么?

LLVM 是编译器工具链技术的一个集合。而其中的 lld 项目,就是内置链接器。

编译器会对每个文件进行编译,生成 Mach-O(可执行文件);链接器会将项目中的多个Mach-O 文件合并成一个。

Xcode运行的过程就是执行一些命令脚本,下面的截图是Xcode编译main.m的脚本,在bin目录下找到clang命令 在后面加一些参数 比如什么语言 编译到哪些架构上,追加在Xcode设置的配置的参数,最后输出成.o文件。
Xcode_shell_cmd.png

LLVM 编译器架构

Screen Shot 2021-11-25 at 1.57.18 PM.png
编译器分为三部分,编译器前端、通用优化器、编译器后端,中间的优化器是不会变的

增加一种语言只需要处理好编译器前端就行了

增加一种架构,只需要添加一种编译器后端的架构处理就可以了

clang在编译器架构中表示 C、C++、Objective-C的前端,在命令行中也作为一个“黑盒”的Driver,封装了编译管线、前端命令、LLVM命令、Toolchain命令等。

LLVM会执行上述的整个编译流程,大体流程如下:

  • 你写好代码后,LLVM会预处理你的代码,比如把宏嵌入到对应的位置。
  • 预处理完后,LLVM 会对代码进行词法分析和语法分析,生成 AST 。AST 是抽象语法树,结构上比代码更精简,遍历起来更快,所以使用 AST 能够更快速地进行静态检查,同时还能更快地生成 IR(中间表示)
  • 最后 AST 会生成 IR,IR 是一种更接近机器码的语言,区别在于和平台有关,通过 IR 可以生成多份适合不同平台的机器码。对于 iOS 系统,IR 生成的可执行文件就是 Mach-O。

OC源文件的编译过程

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

clang -ccc-print-phases main.m
OC源文件的编译过程.png

0:先找到main.m文件

1:预处理器,就是把include、import、宏定义给替换掉

2:编译成IR中间代码

3:把中间代码给后端,生成汇编代码

4:汇编生成目标代码

5:链接静态库、动态库

6:适合某个架构的代码

预处理

使用以下命令,可以查看预处理阶段所做的工作

clang -E main.m

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

1、删除所有的#define,代码中使用宏定义的地方会进行替换

2、将#include包含的文件插入到文件的位置,这个插入的过程是递归的

3、删除掉注释符号及注释

4、添加行号和文件标识,便于调试

编译

编译的过程就是把预处理后的文件进行 词法分析、语法分析、语义分析及优化后产生相应的汇编代码

1、词法分析

这一步把源文件中的代码转化为特殊的标记流,源码被分割成一个一个的字符和单词,在行尾Loc中都标记出了源码所在的对应源文件和具体行数,方便在报错时定位问题。

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

clang -Xclang -dump-tokens main.m

以下面这段代码为例:

Screen Shot 2021-11-24 at 4.15.00 PM.png

第11行的这段源码

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

通过词法分析,会转化为以下的特殊标记

int 'int'    [StartOfLine]  Loc=<main.m:11:1>
identifier 'main'    [LeadingSpace] Loc=<main.m:11:5>
l_paren '('     Loc=<main.m:11:9>
int 'int'       Loc=<main.m:11:10>
identifier 'argc'    [LeadingSpace] Loc=<main.m:11:14>
comma ','       Loc=<main.m:11:18>
char 'char'  [LeadingSpace] Loc=<main.m:11:20>
star '*'     [LeadingSpace] Loc=<main.m:11:25>
identifier 'argv'    [LeadingSpace] Loc=<main.m:11:27>
l_square '['        Loc=<main.m:11:31>
r_square ']'        Loc=<main.m:11:32>
r_paren ')'     Loc=<main.m:11:33>
l_brace '{'  [LeadingSpace] Loc=<main.m:11:35>

2、语法分析

这一步就是根据词法分析的标记流,解析成一个语法树,在Clang中由Parser和Sema两个模块配合完成

在这里面每一个节点也都标记了自己在源码中的位置

验证语法是否正确,比如少一个;报一个错误提示

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

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

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

会解析成以下的语法树

-FunctionDecl 0x7ffe251a8ce0 <main.m:11:1, line:20:1> line:11:5 main 'int (int, char **)'
  |-ParmVarDecl 0x7ffe251a8b00 <col:10, col:14> col:14 argc 'int'
  |-ParmVarDecl 0x7ffe251a8bc0 <col:20, col:32> col:27 argv 'char **':'char **'
  `-CompoundStmt 0x7ffe251a9200 <col:35, line:20:1>
    |-ObjCAutoreleasePoolStmt 0x7ffe251a91b8 <line:13:5, line:18:5>
    | `-CompoundStmt 0x7ffe251a9188 <line:13:22, line:18:5>
    |   |-DeclStmt 0x7ffe251a8e30 <line:14:9, col:32>
    |   | `-VarDecl 0x7ffe251a8da8 <col:9, line:9:21> line:14:13 used eight 'int' cinit
    |   |   `-IntegerLiteral 0x7ffe251a8e10 <line:9:21> 'int' 8
    |   |-DeclStmt 0x7ffe251a8ee8 <line:15:9, col:20>
    |   | `-VarDecl 0x7ffe251a8e60 <col:9, col:19> col:13 used six 'int' cinit
    |   |   `-IntegerLiteral 0x7ffe251a8ec8 <col:19> 'int' 6
    |   |-DeclStmt 0x7ffe251a9010 <line:16:9, col:31>
    |   | `-VarDecl 0x7ffe251a8f18 <col:9, col:28> col:13 used rank 'int' cinit
    |   |   `-BinaryOperator 0x7ffe251a8ff0 <col:20, col:28> 'int' '+'
    |   |     |-ImplicitCastExpr 0x7ffe251a8fc0 <col:20> 'int' <LValueToRValue>
    |   |     | `-DeclRefExpr 0x7ffe251a8f80 <col:20> 'int' lvalue Var 0x7ffe251a8da8 'eight' 'int'
    |   |     `-ImplicitCastExpr 0x7ffe251a8fd8 <col:28> 'int' <LValueToRValue>
    |   |       `-DeclRefExpr 0x7ffe251a8fa0 <col:28> 'int' lvalue Var 0x7ffe251a8e60 'six' 'int'
    |   `-CallExpr 0x7ffe251a9128 <line:17:9, col:30> 'void'
    |     |-ImplicitCastExpr 0x7ffe251a9110 <col:9> 'void (*)(id, ...)' <FunctionToPointerDecay>
    |     | `-DeclRefExpr 0x7ffe251a9028 <col:9> 'void (id, ...)' Function 0x7ffe20b20e88 'NSLog' 'void (id, ...)'
    |     |-ImplicitCastExpr 0x7ffe251a9158 <col:15, col:16> 'id':'id' <BitCast>
    |     | `-ObjCStringLiteral 0x7ffe251a9068 <col:15, col:16> 'NSString *'
    |     |   `-StringLiteral 0x7ffe251a9048 <col:16> 'char [8]' lvalue "rank-%d"
    |     `-ImplicitCastExpr 0x7ffe251a9170 <col:26> 'int' <LValueToRValue>
    |       `-DeclRefExpr 0x7ffe251a9088 <col:26> 'int' lvalue Var 0x7ffe251a8f18 'rank' 'int'
    `-ReturnStmt 0x7ffe251a91f0 <line:19:5, col:12>
      `-IntegerLiteral 0x7ffe251a91d0 <col:12> 'int' 0
Screen Shot 2021-11-25 at 3.16.07 PM.png

3、静态分析(通过语法树进行代码静态分析,找出非语法性错误)

1、错误检查

如出现方法被调用但是未定义、定义但是未使用的变量

2、类型检查

一般会把类型分为两类:动态的和静态的。动态的在运行时做检查,静态的在编译时做检查。编写代码时可以向任意对象发送任何消息,在运行时,才会检查对象是否能够响应这些消息。

4、CodeGen - IR代码生成

1、CodeGen 负责将语法树从顶至下遍历,翻译成 LLVM IR
2、LLVM IR是Frontend的输出,也是LLVM Backend的输入,前后端的桥接语言
3、与Objective-C Runtime 桥接
与Objective-C Runtime 桥接的应用

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

2、objct对象发送一个消息最终会编译成什么样子啊,会编译成objc_msgSend调用就发生在这一步,将语法树中的ObjCMessageExpr翻译成相应版本的objc_msgSend,对super关键字的调用翻译成objc_msgSendSuper

3、根据修饰符strong / weak /copy /atomic 合成@property自动实现的getter / setter、处理@synthesize也是这一步做的

4、生成block_layout的数据结构、变量的capture(__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”

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文件就是把UIKit、Foundation这些库用pch文件import一下,这样就不用在每个源文件中去解析这么多东西了,现在iOS这边乱搞把一些全局的变量,自己模块的一些东西都放在里面。

Xcode里面出了一个modules的概念,各个setting里面也是打开的,默认把库打成一个modules的形式,尤其是UIKit、Foundation这些库全部都是modules,好处就是我加这个参数(fmodules)以后它就会自动把#import变成@import,现在的编译就会比最早的那种连pch都没有的快很多,因为它的出现pch就不会默认出现了

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

链接

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

编译时链接器做了什么?

1、Mach-O里面主要是代码和数据,代码是函数的定义,数据是全局变量的定义,不管是代码还是数据都是通过符号关联起来的。

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

为什么要做符号绑定?

1、如果地址和符号不做绑定的话,要让机器知道你在操作什么地址,就需要写代码的时候设置好内存地址。

2、可读性差,修改代码后要重新对地址进行维护

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

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

1、多个文件之间的变量和接口是相互依赖的,就需要链接器把项目中多个Mach-O文件符号和地址绑定起来。

2、不绑定的话单个文件生成的Mach-O就是无法运行的,运行时遇到调用其他文件的函数实现时,就会找不到函数地址。

3、链接多个目标文件就会创建一个符号表,记录所有已定义和未定义的符号,如果出现相同符号的情况,就会出现“ld: dumplicate symbols”的错误信息,如果在目标文件中没有找到符号,就会提示“Undefined symbols”的错误信息。

链接器对代码主要做了哪几件事?

1、去代码文件中查找没有定义的变量

2、将所有符号定义和引用地址收集起来,并放到全局符号表中

3、计算合并后的长度及位置,生成同类型的段进行合并,建立绑定

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

链接器如何去除无用的函数,保证Mach-O的大小?

链接器在整理函数的调用关系时,会以main函数为源头跟随每个引用并将其标记为live,跟随完成后那些未被标记为live的就是无用函数。

总结:一个源文件的编译过程

Screen Shot 2021-11-25 at 4.51.00 PM.png

代码实践

#import <Foundation/Foundation.h>
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 -S main.m -o main.s

装载与链接

一个App从可执行文件到真正启动运行代码,基本需要经过装载和动态库链接两个步骤。

程序运行起来会拥有独立的虚拟地址空间,在操作系统上会同时运行多个进程,彼此之间的虚拟地址空间是隔离的。

装载就是把可执行文件映射到虚拟内存中的过程,由于内存资源稀缺,只将程序最常用的部分驻留在内存里,不太常用的数据放在磁盘里,这也是动态装载的过程。

装载的过程就是进程建立的过程,操作系统主要做了3件事:

1、创建一个独立的虚拟地址

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

3、将CPU的寄存区设置成可执行文件的入口地址,启动运行

静态库

静态库是编译时链接的库,需要链接进你的Mach-O文件里,如果需要更新就重新编译一次,无法动态的加载和更新。

动态库

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

共享缓存

Mach-O是编译后的产物,而动态库在运行时才会被链接,所有Mach-O中并没有动态库的符号定义。

Mach-O中动态库中的符号是未定义的,但他们的名字和对应的库的路径会被记录下来。

运行时dlopen 和 dlsym 导入动态库时,先根据记录的库路径找到对应的库,再通过记录的名字符号找到绑定的地址。

优点:

代码共用、易于维护、减少可执行文件的体积

参考资料:

LLVM框架/LLVM编译流程/Clang前端/LLVM IR/LLVM应用与实践

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

sunnyxx的clang视频分享

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

推荐阅读更多精彩内容