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閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容