Clang-LLVM下,一個(gè)源文件的編譯過(guò)程

LLVM是什么?

LLVM 是編譯器工具鏈技術(shù)的一個(gè)集合。而其中的 lld 項(xiàng)目,就是內(nèi)置鏈接器。

編譯器會(huì)對(duì)每個(gè)文件進(jìn)行編譯,生成 Mach-O(可執(zhí)行文件);鏈接器會(huì)將項(xiàng)目中的多個(gè)Mach-O 文件合并成一個(gè)。

Xcode運(yùn)行的過(guò)程就是執(zhí)行一些命令腳本,下面的截圖是Xcode編譯main.m的腳本,在bin目錄下找到clang命令 在后面加一些參數(shù) 比如什么語(yǔ)言 編譯到哪些架構(gòu)上,追加在Xcode設(shè)置的配置的參數(shù),最后輸出成.o文件。
Xcode_shell_cmd.png

LLVM 編譯器架構(gòu)

Screen Shot 2021-11-25 at 1.57.18 PM.png
編譯器分為三部分,編譯器前端、通用優(yōu)化器、編譯器后端,中間的優(yōu)化器是不會(huì)變的

增加一種語(yǔ)言只需要處理好編譯器前端就行了

增加一種架構(gòu),只需要添加一種編譯器后端的架構(gòu)處理就可以了

clang在編譯器架構(gòu)中表示 C、C++、Objective-C的前端,在命令行中也作為一個(gè)“黑盒”的Driver,封裝了編譯管線、前端命令、LLVM命令、Toolchain命令等。

LLVM會(huì)執(zhí)行上述的整個(gè)編譯流程,大體流程如下:

  • 你寫(xiě)好代碼后,LLVM會(huì)預(yù)處理你的代碼,比如把宏嵌入到對(duì)應(yīng)的位置。
  • 預(yù)處理完后,LLVM 會(huì)對(duì)代碼進(jìn)行詞法分析和語(yǔ)法分析,生成 AST 。AST 是抽象語(yǔ)法樹(shù),結(jié)構(gòu)上比代碼更精簡(jiǎn),遍歷起來(lái)更快,所以使用 AST 能夠更快速地進(jìn)行靜態(tài)檢查,同時(shí)還能更快地生成 IR(中間表示)
  • 最后 AST 會(huì)生成 IR,IR 是一種更接近機(jī)器碼的語(yǔ)言,區(qū)別在于和平臺(tái)有關(guān),通過(guò) IR 可以生成多份適合不同平臺(tái)的機(jī)器碼。對(duì)于 iOS 系統(tǒng),IR 生成的可執(zhí)行文件就是 Mach-O。

OC源文件的編譯過(guò)程

使用以下命令,查看OC源文件的編譯過(guò)程

clang -ccc-print-phases main.m
OC源文件的編譯過(guò)程.png

0:先找到main.m文件

1:預(yù)處理器,就是把include、import、宏定義給替換掉

2:編譯成IR中間代碼

3:把中間代碼給后端,生成匯編代碼

4:匯編生成目標(biāo)代碼

5:鏈接靜態(tài)庫(kù)、動(dòng)態(tài)庫(kù)

6:適合某個(gè)架構(gòu)的代碼

預(yù)處理

使用以下命令,可以查看預(yù)處理階段所做的工作

clang -E main.m

預(yù)處理主要做了以下幾件事情:

1、刪除所有的#define,代碼中使用宏定義的地方會(huì)進(jìn)行替換

2、將#include包含的文件插入到文件的位置,這個(gè)插入的過(guò)程是遞歸的

3、刪除掉注釋符號(hào)及注釋

4、添加行號(hào)和文件標(biāo)識(shí),便于調(diào)試

編譯

編譯的過(guò)程就是把預(yù)處理后的文件進(jìn)行 詞法分析、語(yǔ)法分析、語(yǔ)義分析及優(yōu)化后產(chǎn)生相應(yīng)的匯編代碼

1、詞法分析

這一步把源文件中的代碼轉(zhuǎn)化為特殊的標(biāo)記流,源碼被分割成一個(gè)一個(gè)的字符和單詞,在行尾Loc中都標(biāo)記出了源碼所在的對(duì)應(yīng)源文件和具體行數(shù),方便在報(bào)錯(cuò)時(shí)定位問(wèn)題。

使用以下命令來(lái)進(jìn)行詞法分析

clang -Xclang -dump-tokens main.m

以下面這段代碼為例:

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

第11行的這段源碼

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

通過(guò)詞法分析,會(huì)轉(zhuǎn)化為以下的特殊標(biāo)記

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、語(yǔ)法分析

這一步就是根據(jù)詞法分析的標(biāo)記流,解析成一個(gè)語(yǔ)法樹(shù),在Clang中由Parser和Sema兩個(gè)模塊配合完成

在這里面每一個(gè)節(jié)點(diǎn)也都標(biāo)記了自己在源碼中的位置

驗(yàn)證語(yǔ)法是否正確,比如少一個(gè);報(bào)一個(gè)錯(cuò)誤提示

根據(jù)當(dāng)前語(yǔ)言的語(yǔ)法,生成語(yǔ)義節(jié)點(diǎn),并將所有的節(jié)點(diǎn)組合成抽象語(yǔ)法樹(shù)

使用以下命令來(lái)進(jìn)行語(yǔ)法分析

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

會(huì)解析成以下的語(yǔ)法樹(shù)

-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、靜態(tài)分析(通過(guò)語(yǔ)法樹(shù)進(jìn)行代碼靜態(tài)分析,找出非語(yǔ)法性錯(cuò)誤)

1、錯(cuò)誤檢查

如出現(xiàn)方法被調(diào)用但是未定義、定義但是未使用的變量

2、類型檢查

一般會(huì)把類型分為兩類:動(dòng)態(tài)的和靜態(tài)的。動(dòng)態(tài)的在運(yùn)行時(shí)做檢查,靜態(tài)的在編譯時(shí)做檢查。編寫(xiě)代碼時(shí)可以向任意對(duì)象發(fā)送任何消息,在運(yùn)行時(shí),才會(huì)檢查對(duì)象是否能夠響應(yīng)這些消息。

4、CodeGen - IR代碼生成

1、CodeGen 負(fù)責(zé)將語(yǔ)法樹(shù)從頂至下遍歷,翻譯成 LLVM IR
2、LLVM IR是Frontend的輸出,也是LLVM Backend的輸入,前后端的橋接語(yǔ)言
3、與Objective-C Runtime 橋接
與Objective-C Runtime 橋接的應(yīng)用

1、在Objective-C中的 Class / Meta Class / Protocol /Category 這些結(jié)構(gòu)體的內(nèi)存結(jié)構(gòu)就是在這一步生成的,并放在了Mach-O指定的Section中(如 Class: _DATA, _objc _classrefs),這個(gè) DATA段也會(huì)存放一些static變量

2、objct對(duì)象發(fā)送一個(gè)消息最終會(huì)編譯成什么樣子啊,會(huì)編譯成objc_msgSend調(diào)用就發(fā)生在這一步,將語(yǔ)法樹(shù)中的ObjCMessageExpr翻譯成相應(yīng)版本的objc_msgSend,對(duì)super關(guān)鍵字的調(diào)用翻譯成objc_msgSendSuper

3、根據(jù)修飾符strong / weak /copy /atomic 合成@property自動(dòng)實(shí)現(xiàn)的getter / setter、處理@synthesize也是這一步做的

4、生成block_layout的數(shù)據(jù)結(jié)構(gòu)、變量的capture(__block / 和 __weak),生成_block_invoke函數(shù)都發(fā)生在這一步

5、之前總說(shuō)ARC是編譯器幫我們插入一些內(nèi)存管理的代碼,具體也是在這一步完成的

ARC: 分析對(duì)象的引用關(guān)系,將objc_StoreStrong / Objc_StoreWeak等ARC代碼的插入

將ObjCAutotreleasePoolStmt轉(zhuǎn)譯成objc_autoreleasePoolPush/Pop

實(shí)現(xiàn)自動(dòng)調(diào)用[super dealloc]

為每個(gè)擁有ivar的Class 合成.cxx_destructor 方法來(lái)自動(dòng)釋放類的成員變量,代替MRC時(shí)代的 “self.xxx = nil”

LLVM的中間產(chǎn)物及優(yōu)化

使用以下命令,生成LLVM中間產(chǎn)物IR(Intermediate Representation),把這個(gè)過(guò)程打印出來(lái)

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

使用以下命令,會(huì)使用LLVM對(duì)代碼進(jìn)行優(yōu)化。

//針對(duì)全局變量?jī)?yōu)化、循環(huán)優(yōu)化、尾遞歸優(yōu)化等。
//在 Xcode 的編譯設(shè)置里也可以設(shè)置優(yōu)化級(jí)別-01,-03,-0s,還可以寫(xiě)些自己的 Pass。
clang -emit-llvm -c main.m -o main.bc

生成匯編代碼

使用以下命令,生成相對(duì)應(yīng)的匯編代碼。

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

至此,編譯階段完成,將書(shū)寫(xiě)代碼轉(zhuǎn)換成了機(jī)器可以識(shí)別的匯編代碼,匯編器是將匯編代碼轉(zhuǎn)變成機(jī)器可以執(zhí)行的指令,每一個(gè)匯編語(yǔ)句幾乎都對(duì)應(yīng)一條機(jī)器指令。根據(jù)匯編指令和機(jī)器指令的對(duì)照表一一翻譯就可以了。

使用以下命令,生成對(duì)應(yīng)的目標(biāo)文件。
clang -fmodules -c main.m -o main.o
后來(lái)的Xcode新建的工程里并沒(méi)有pch文件,為什么呢?

pch文件就是把UIKit、Foundation這些庫(kù)用pch文件import一下,這樣就不用在每個(gè)源文件中去解析這么多東西了,現(xiàn)在iOS這邊亂搞把一些全局的變量,自己模塊的一些東西都放在里面。

Xcode里面出了一個(gè)modules的概念,各個(gè)setting里面也是打開(kāi)的,默認(rèn)把庫(kù)打成一個(gè)modules的形式,尤其是UIKit、Foundation這些庫(kù)全部都是modules,好處就是我加這個(gè)參數(shù)(fmodules)以后它就會(huì)自動(dòng)把#import變成@import,現(xiàn)在的編譯就會(huì)比最早的那種連pch都沒(méi)有的快很多,因?yàn)樗某霈F(xiàn)pch就不會(huì)默認(rèn)出現(xiàn)了

$clang -E -fmodules main.m //加入fmodules參數(shù)生成可執(zhí)行文件

鏈接

這一階段是將上個(gè)階段生成的目標(biāo)文件和引用的靜態(tài)庫(kù)鏈接起來(lái),最終生成可執(zhí)行文件,鏈接器解決了目標(biāo)文件和庫(kù)之間的鏈接。

編譯時(shí)鏈接器做了什么?

1、Mach-O里面主要是代碼和數(shù)據(jù),代碼是函數(shù)的定義,數(shù)據(jù)是全局變量的定義,不管是代碼還是數(shù)據(jù)都是通過(guò)符號(hào)關(guān)聯(lián)起來(lái)的。

2、Mach-O里面的代碼,要操作的變量和函數(shù)要綁定到各自的地址上,鏈接器的作用就是完成變量和函數(shù)的符號(hào)和其地址的綁定。

為什么要做符號(hào)綁定?

1、如果地址和符號(hào)不做綁定的話,要讓機(jī)器知道你在操作什么地址,就需要寫(xiě)代碼的時(shí)候設(shè)置好內(nèi)存地址。

2、可讀性差,修改代碼后要重新對(duì)地址進(jìn)行維護(hù)

3、需要針對(duì)不同平臺(tái)寫(xiě)多份代碼,相當(dāng)于直接寫(xiě)匯編

為什么還要把項(xiàng)目中的多個(gè)Mach-O合并成一個(gè)?

1、多個(gè)文件之間的變量和接口是相互依賴的,就需要鏈接器把項(xiàng)目中多個(gè)Mach-O文件符號(hào)和地址綁定起來(lái)。

2、不綁定的話單個(gè)文件生成的Mach-O就是無(wú)法運(yùn)行的,運(yùn)行時(shí)遇到調(diào)用其他文件的函數(shù)實(shí)現(xiàn)時(shí),就會(huì)找不到函數(shù)地址。

3、鏈接多個(gè)目標(biāo)文件就會(huì)創(chuàng)建一個(gè)符號(hào)表,記錄所有已定義和未定義的符號(hào),如果出現(xiàn)相同符號(hào)的情況,就會(huì)出現(xiàn)“l(fā)d: dumplicate symbols”的錯(cuò)誤信息,如果在目標(biāo)文件中沒(méi)有找到符號(hào),就會(huì)提示“Undefined symbols”的錯(cuò)誤信息。

鏈接器對(duì)代碼主要做了哪幾件事?

1、去代碼文件中查找沒(méi)有定義的變量

2、將所有符號(hào)定義和引用地址收集起來(lái),并放到全局符號(hào)表中

3、計(jì)算合并后的長(zhǎng)度及位置,生成同類型的段進(jìn)行合并,建立綁定

4、對(duì)項(xiàng)目中不同文件里的變量進(jìn)行地址重定位

鏈接器如何去除無(wú)用的函數(shù),保證Mach-O的大???

鏈接器在整理函數(shù)的調(diào)用關(guān)系時(shí),會(huì)以main函數(shù)為源頭跟隨每個(gè)引用并將其標(biāo)記為live,跟隨完成后那些未被標(biāo)記為live的就是無(wú)用函數(shù)。

總結(jié):一個(gè)源文件的編譯過(guò)程

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

代碼實(shí)踐

#import <Foundation/Foundation.h>
int main() {
    NSLog(@"hello world!");
    return 0;
}
1、生成Mach-O可執(zhí)行文件
clang -fmodules main.m -o main
2、生成抽象語(yǔ)法樹(shù)
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

3、生成匯編代碼

clang -S main.m -o main.s

裝載與鏈接

一個(gè)App從可執(zhí)行文件到真正啟動(dòng)運(yùn)行代碼,基本需要經(jīng)過(guò)裝載和動(dòng)態(tài)庫(kù)鏈接兩個(gè)步驟。

程序運(yùn)行起來(lái)會(huì)擁有獨(dú)立的虛擬地址空間,在操作系統(tǒng)上會(huì)同時(shí)運(yùn)行多個(gè)進(jìn)程,彼此之間的虛擬地址空間是隔離的。

裝載就是把可執(zhí)行文件映射到虛擬內(nèi)存中的過(guò)程,由于內(nèi)存資源稀缺,只將程序最常用的部分駐留在內(nèi)存里,不太常用的數(shù)據(jù)放在磁盤(pán)里,這也是動(dòng)態(tài)裝載的過(guò)程。

裝載的過(guò)程就是進(jìn)程建立的過(guò)程,操作系統(tǒng)主要做了3件事:

1、創(chuàng)建一個(gè)獨(dú)立的虛擬地址

2、讀取可執(zhí)行文件頭,并且建立虛擬空間與可執(zhí)行文件的映射關(guān)系

3、將CPU的寄存區(qū)設(shè)置成可執(zhí)行文件的入口地址,啟動(dòng)運(yùn)行

靜態(tài)庫(kù)

靜態(tài)庫(kù)是編譯時(shí)鏈接的庫(kù),需要鏈接進(jìn)你的Mach-O文件里,如果需要更新就重新編譯一次,無(wú)法動(dòng)態(tài)的加載和更新。

動(dòng)態(tài)庫(kù)

動(dòng)態(tài)庫(kù)是運(yùn)行時(shí)鏈接的庫(kù),使用dyld就可以實(shí)現(xiàn)動(dòng)態(tài)加載,iOS中的系統(tǒng)庫(kù)都是動(dòng)態(tài)鏈接的。

共享緩存

Mach-O是編譯后的產(chǎn)物,而動(dòng)態(tài)庫(kù)在運(yùn)行時(shí)才會(huì)被鏈接,所有Mach-O中并沒(méi)有動(dòng)態(tài)庫(kù)的符號(hào)定義。

Mach-O中動(dòng)態(tài)庫(kù)中的符號(hào)是未定義的,但他們的名字和對(duì)應(yīng)的庫(kù)的路徑會(huì)被記錄下來(lái)。

運(yùn)行時(shí)dlopen 和 dlsym 導(dǎo)入動(dòng)態(tài)庫(kù)時(shí),先根據(jù)記錄的庫(kù)路徑找到對(duì)應(yīng)的庫(kù),再通過(guò)記錄的名字符號(hào)找到綁定的地址。

優(yōu)點(diǎn):

代碼共用、易于維護(hù)、減少可執(zhí)行文件的體積

參考資料:

LLVM框架/LLVM編譯流程/Clang前端/LLVM IR/LLVM應(yīng)用與實(shí)踐

iOS底層學(xué)習(xí) - 從編譯到啟動(dòng)的奇幻旅程

sunnyxx的clang視頻分享

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容