LLVM
,
Clang
,
Swiftc
,
IR
,
preprocessor
,
Mach-O
,
dyld
把一種編程語言(原始語言)轉換爲另外一種編程語言(目標語言)的程序叫作編譯器.
前端
大多數編譯器由兩部分組成: 前端和後端.
編程
前端負責詞法分析,語法分析,生成中間代碼;
後端以中間代碼做爲輸入,進行行架構無關的代碼優化,接着針對不一樣架構生成不一樣的機器碼。swift
先後端依賴統一格式的中間代碼(IR), 使得先後端能夠獨立的變化. 新增一門語言只須要修改前端, 而新增一個CPU架構只須要修改後端便可. Objective C/C/C++使用的編譯器前端是clang, swift是swift, 後端都是LLVM.後端
LLVM的核心庫提供了現代化的source-target-independent優化器和支持諸多流行CPU架構的代碼生成器. Clang 和 LLDB都是基於LLVM衍生的子項目.bash
Clang是C語言家族的編譯器前端,誕生之初是爲了替代GCC,提供更快的編譯速度。一張圖瞭解clang編譯的大體流程:數據結構
大體看來, Clang能夠分爲一下幾個步驟:架構
預處理 -> 詞法分析 -> 語法分析 -> 靜態分析 -> 生成中間代碼和優化 -> 彙編 -> 連接app
預處理會進行以下操做:
1)頭文件引入, 遞歸將頭文件引用替換爲頭文件中的實際內容, 因此儘可能減小頭文件中的#import, 使用@class替代, 把#import放到.m文件中.
2)宏替換, 在源碼中使用的宏定義會被替換爲對應#define的內容, 不要在須要預處理的代碼中加入太多的內聯代碼邏輯.
3)註釋處理, 在預處理的時候, 註釋被刪除
4)條件編譯, (#if, #else, #endif)
編程語言
這一步把源文件中的代碼轉化爲特殊的標記流. 詞法分析器讀入源文件的字符流, 將他們組織稱有意義的詞素(lexeme)序列,對於每一個詞素,此法分析器產生**詞法單元(token)**做爲輸出.
源碼被分割成一個一個的字符和單詞, 在行尾Loc中都標記出了源碼所在的對應源文件和具體行數, 方便在報錯時定位問題. 相似於下面:
ide
int 'int' [StartOfLine] Loc=<main.m:14:1>
identifier 'main' [LeadingSpace] Loc=<main.m:14:5>
l_paren '(' Loc=<main.m:14:9>
int 'int' Loc=<main.m:14:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:14:14>
comma ',' Loc=<main.m:14:18>
char 'char' [LeadingSpace] Loc=<main.m:14:20>
star '*' [LeadingSpace] Loc=<main.m:14:25>
複製代碼
詞法分析的Token流會被解析成一顆抽象語法樹(abstract syntax tree - AST). 在這裏面每一節點也都標記了其在源碼中的位置.
有了抽象語法樹,clang就能夠對這個樹進行分析,找出代碼中的錯誤。好比類型不匹配,亦或Objective C中向target發送了一個未實現的消息.
AST是開發者編寫clang插件主要交互的數據結構,clang也提供不少API去讀取AST.
把源碼轉化爲抽象語法樹以後,編譯器就能夠對這個樹進行分析處理。靜態分析會對代碼進行錯誤檢查,如出現方法被調用可是未定義、定義可是未使用的變量等,以此提升代碼質量. 也可使用 Xcode 自帶的靜態分析工具(Product -> Analyze).
常見的操做有:
1)當在代碼中使用 ARC 時,編譯器在編譯期間,會作許多的類型檢查. 最多見的是檢查程序是否發送正確的消息給正確的對象,是否在正確的值上調用了正常函數。若是你給一個單純的 NSObject* 對象發送了一個 hello 消息,那麼 clang 就會報錯,一樣,給屬性設置一個與其自身類型不相符的對象,編譯器會給出一個可能使用不正確的警告.
通常會把類型分爲兩類:動態的和靜態的。動態的在運行時作檢查,靜態的在編譯時作檢查。以往,編寫代碼時能夠向任意對象發送任何消息,在運行時,纔會檢查對象是否可以響應這些消息。因爲只是在運行時作此類檢查,因此叫作動態類型。
至於靜態類型,是在編譯時作檢查。當在代碼中使用 ARC 時,編譯器在編譯期間,會作許多的類型檢查:由於編譯器須要知道哪一個對象該如何使用。
2)檢查是否有定義了,可是從未使用過的變量.
3)檢查在 你的初始化方法中中調用 self 以前, 是否已經調用 [self initWith…] 或 [super init] 了.
此處遍歷語法樹,最終生成LLVM IR代碼。LLVM IR是前端的輸出,後端的輸入. Objective C代碼在這一步會進行runtime的橋接:property合成,ARC處理等
LLVM對IR進行優化後,會對代碼進行編譯優化例如針對全局變量優化、循環優化、尾遞歸優化等, 而後會針對不一樣架構生成不一樣的目標代碼,最後以彙編代碼的格式輸出.
在這一階段,彙編器將上一步生成的可讀的彙編代碼轉化爲機器代碼。最終產物就是 以 .o 結尾的目標文件。使用Xcode構建的程序會在DerivedData目錄中找到這個文件.
Tips:什麼是符號(Symbols)? 符號就是指向一段代碼或者數據的名稱。還有一種叫作WeakSymols,也就是並不必定會存在的符號,須要在運行時決定。好比iOS 12特有的API,在iOS11上就沒有.
目標文件(.o)和引用的庫(dylib,a,tbd)連接起來, 最終生成可執行文件(mach-o), 連接器解決了目標文件和庫之間的連接.
這時可執行文件的符號表信息已經有了, 會在運行時動態綁定.
Mach-O是OS X中二進制文件的原生可執行格式,是傳送代碼的首選格式。可執行格式決定了二進制文件中的代碼和數據讀入內存的順序。代碼和數據的順序會影響內存使用和分頁活動,從而直接影響程序的性能.
Mach-O是記錄編譯後的可執行文件,對象代碼,共享庫,動態加載代碼和內存轉儲的文件格式。不一樣於 xml 這樣的文件,它只是二進制字節流,裏面有不一樣的包含元信息的數據塊,好比字節順序,cpu 類型,塊大小等。文件內容是不能夠修改的,由於在 .app 目錄中有個 _CodeSignature 的目錄,裏面包含了程序代碼的簽名,這個簽名的做用就是保證簽名後 .app 裏的文件,包括資源文件,Mach-O 文件都不可以更改.
Mach-O 文件包含三個區域:
Mach-O Header: 包含字節順序,magic,cpu 類型,加載指令的數量等.
Load Commands: 包含不少內容的表,包括區域的位置,符號表,動態符號表等。每一個加載指令包含一個元信息,好比指令類型,名稱,在二進制中的位置等.
Data: 最大的部分,包含了代碼,數據,好比符號表,動態符號表等.
Mach-O文件的結構以下:
Header
保存了Mach-O的一些基本信息,包括了平臺、文件類型、LoadCommands的個數等等.
使用otool -v -h a.out
查看其內容:
Load commands
這一段緊跟Header,加載Mach-O文件時會使用這裏的數據來肯定內存的分佈
Data
包含 Load commands 中須要的各個 segment,每一個 segment 中又包含多個 section。當運行一個可執行文件時,虛擬內存 (virtual memory) 系統將 segment 映射到進程的地址空間上.
使用xcrun size -x -l -m a.out
查看segment中的內容:
Segment __PAGEZERO。 大小爲 4GB,規定進程地址空間的前 4GB 被映射爲不可讀不可寫不可執行。
Segment __TEXT。 包含可執行的代碼,以只讀和可執行方式映射。
Segment __DATA。 包含了將會被更改的數據,以可讀寫和不可執行方式映射。
Segment __LINKEDIT。 包含了方法和變量的元數據,代碼簽名等信息。
生成可執行文件後就是在啓動時進行動態連接了, 進行符號和地址的綁定. 首先會加載所依賴的 dylibs,修正地址偏移,由於 iOS 會用 ASLR 來作地址偏移避免攻擊,肯定 Non-Lazy Pointer 地址進行符號地址綁定,加載全部類,最後執行 load 方法和 clang attribute 的 constructor 修飾函數.
在每次編譯後都會生成一個 dSYM 文件,程序在執行中經過地址來調用方法函數,而 dSYM 文件裏存儲了函數地址映射,這樣調用棧裏的地址能夠經過 dSYM 這個映射表可以得到具體函數的位置。通常都會用來處理 crash 時獲取到的調用棧 .crash 文件將其符號化.
當release的版本 crash的時候,會有一個日誌文件,包含出錯的內存地址, 使用symbolicatecrash工具可以把日誌和dSYM文件轉換成能夠閱讀的log信息,也就是將內存地址,轉換成程序裏的函數或變量和所屬於的 文件名.
相關參考