iOS底層探索(一) - 從零開始認識Clang與LLVM
寫在前面
編譯器是屬于底層知識,在日常開發中少有涉及,但在我的印象中,越接近底層是越需要編程基本功,也是越復雜的。但要想提升技術卻始終繞不開要對底層原理的探究,很多資料都是直接拋出一堆函數概念和一頓操作,基礎一般的小伙伴看了表示一臉懵逼。在此結合我自己的理解進行優化總結一下。畢竟知識水平有限,有問題或總結不妥的地方歡迎指出,多多學習,非常感謝!2018.2
入門起步
- 經過上一篇對編譯器的基本介紹,相信大家對Clang都有一個基本的認識了,通俗來說是一個編譯器的前端,負責分析源代碼(就是我們使用的C/OC/C++等)。
Clang的編譯過程
1.預處理
-
預處理顧名思義是預先處理,那預處理都做了哪些事情呢?內容如下。
-
(1) import 頭文件替換
-
面向對象編程的思維下,我們寫代碼會經常用到其他類的屬性\方法等,我們只需要導入頭文件就可以用了,如:
#import <Foundation/Foundation.h> // 這里將會在預處理時會把 Foundation.h 文件的內容拷貝過來并替換 復制代碼
-
基于這個原理,這里引出了一個小問題,如果
ClassA.h
文件引用了ClassB.h
,并且ClassB.h
也引用了ClassA.h
,這里是不是就會互相循環引入了?- 解決辦法是在頭文件中使用
@class ClassA; 復制代碼
- 代替
#import "ClassA.h" 復制代碼
- 這么寫意思是聲明
ClassA
是一個類,這樣你就可以使用ClassA
做類名了,如果需要使用ClassA
的方法屬性等可以在 .m 實現文件中再通過import MyClass.h
的方式使用,這種方法不但可以解決互相引入的問題還可以優化編譯速度。
-
-
(2) macro 宏展開
-
無參宏: 如:
#define DATA_TYPE_NUM @"number" 復制代碼
在此宏定義作用域內,輸入了 DATA_TYPE_NUM,在預處理過程中 DATA_TYPE_NUM 都會被替換成 @"number"。
-
帶參宏: 帶參數的宏 如:
#define CYXColor(r, g, b) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:1.0] 復制代碼
-
-
(3) 處理其他的預編譯指令(其實預編譯過程也是出了預編譯指令的過程)
條件編譯語句也是在預處理階段完成,并且條件編譯只允許編譯源程序中滿足條件的程序段,使生成的目標程序較短,從而減少了內存的開銷并提高了程序的效率,如以下代碼就只會保留一個return語句:
#if DEBUG return YES; #elsereturn NO; #endif 復制代碼
-
(4) 總結:
- 簡單來說,“#”這個符號是編譯器預處理的標志, 以下是一些常用的預處理指令參考
預處理指令 用法解析 #undef 取消已定義的宏 #if 如果給定條件為真,則編譯以下代碼 #ifdef 如果宏已經定義,則編譯以下代碼 #ifndef 如果宏沒有定義,則編譯以下代碼 #elif 如果前面的#if給定條件不為真,當前條件為真,則編譯以下代碼 #endif 結束一個#if……#else條件編譯塊
*PS:還需要了解更多關于預編譯的內容,還請自行百度*
[圖片上傳失敗...(image-cf6f6f-1531632712782)][圖片上傳失敗...(image-fd9112-1531632712782)]`$clang -E main.m`
復制代碼
2. Lexical Analysis - 詞法分析(輸出token流)
- 預處理完成了以后,開始詞法分析。詞法分析其實是編譯器開始工作真正意義上的第一個步驟,其所做的工作主要為將輸入的代碼轉換為一系列符合特定語言的詞法單元,這些詞法單元類型包括了關鍵字,操作符,變量等等。舉個例子:
Objective-C語言包含了關鍵字if、else、new等,那么在詞法分析步驟時,遇到i與f或n與e與w組合在一起的時候,需要將這幾個字母組合為關鍵字if或new這個詞法單元。
- 詞法分析,只需要將源代碼以字符文本的形式轉化成Token流的形式,不涉及交驗語義,不需要遞歸,是線性的。
什么是token流呢?可以這么理解:就是有"類型",有"值"的一些小單元。
- 再舉個例子:
比如一個運算表達式:
(28 + 78) * 2
這里面只需要解析出(
是一個開括號,28
是數字整形,+
是一個運算符號即可。
編譯指令: $clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
3.Semantic Analysis - 語法分析(輸出(AST)抽象語法樹)
編譯指令:$clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
-
語法分析的最終產物是輸出抽象語法樹
-
語法分析,在Clang中由Parser和Sema兩個模塊配合完成
-
交驗語法是否正確
-
根據當前語言的語法,生成語意節點,并將所有節點組合成抽象語法樹(AST)
-
這一步跟源碼等價,可以反寫出源碼
-
Static Analysis 靜態分析
- 通過語法樹進行代碼靜態分析,找出非語法性錯誤
- 模擬代碼執行路徑,分析出control-flow graph(CFG) 【MRC時代會分析出引用計數的錯誤】
- 預置了常用Checker(檢查器)
未完待續 ...
這是上篇,為保證博客質量與閱讀體驗(個人感覺一次閱讀過多文字有點影響閱讀體驗),先分享已完成的上半部分,下篇將繼續介紹Clang編譯過程中的剩下環節,歡迎持續關注,感謝理解與支持!2018.2
預告:下篇將繼續介紹Clang與LLVM以下環節的相關知識。
下面是一些關鍵詞,有興趣的朋友先自行谷歌學習吧,下篇等我有閑情的時候再更新了,我也不知道什么時候。2018.7.15
4. CodeGen - (Intermediate Representation,簡稱IR)IR中間代碼生成
- CodeGen 負責將語法樹叢頂至下遍歷,翻譯成LLVM IR
- LLVM IR 是Frontend的輸出,也是LLVM Backend的輸入,前后端的橋接語言 (Swift也是轉成這個)
- 與 Objective-C Runtime 橋接
- Class/Meta Class/Protocol/Category內存結構生成,并存放在指定section中(如Class:_DATA, _objc_classrefs)
- Method/lvar/Property內存結構生成
- 組成method_list/ivar_list/property_list并填入Class
- Non-Fragile ABI:為每個Ivar合成OBJC_IVAR_$_偏移值常量
- 存取Ivar的語句(ivar = 123; int a = ivar;)轉寫成base + OBJC_IVAR$_的形式
- 將語法樹中的ObjcMessageExpr翻譯成相應版本的objc_msgSend,對super關鍵字的調用翻譯成objc_msgSendSuper
- 根據修飾符strong/weak/copy/atomic合成@property 自動實現的 setter/getter
- 處理@synthesize
- 生成block_layout的數據結構
- 變量的capture(__block/__weak)
- 生成_block_invoke函數
- ARC:分析對象引用關系,將objc_storeStrong/objc_storeWeak等ARC代碼插入
- 將ObjCAutoreleasePoolStmt轉譯成objc_autoreleasePoolPush/Pop
- 實現自動調用[super dealloc]
- 為每個擁有ivar的Class合成.cxx_destructor方法來自動釋放類的成員變量,代替MRC時代的“self.xxx = nil”
5. Optimize - 優化IR
- 遞歸優化成偽遞歸
6. LLVM Bitcode - 生成字節碼
7. Assemble - 生成Target相關匯編
- Assemble - 生成Target相關Object(Mach-O)
8. Link生成Executable
參考文檔
https://zh.wikipedia.org/wiki/C%E9%A2%84%E5%A4%84%E7%90%86%E5%99%A8 https://llvm.org/docs/tutorial/LangImpl2.html https://www.objc.io/issues/6-build-tools/compiler/