編譯器
iOS編譯和打包時(shí),編譯器直接將代碼編譯成機(jī)器碼,然后直接在CPU上運(yùn)行。而不用使用解釋器運(yùn)行代碼。因?yàn)檫@樣執(zhí)行效率更高,運(yùn)行速度更快。C,C++,OC都是使用的編譯器生成相關(guān)的可執(zhí)行文件。
解釋器:解釋器會(huì)在運(yùn)行時(shí)解釋執(zhí)行代碼,獲取一段代碼后就會(huì)將其翻譯成目標(biāo)代碼(就是字節(jié)碼(Bytecode)),然后一句一句地執(zhí)行目標(biāo)代碼。也就是說(shuō)是在運(yùn)行時(shí)才去解析代碼,比直接運(yùn)行編譯好的可執(zhí)行文件自然效率就低,但是跑起來(lái)之后可以不用重啟啟動(dòng)編譯,直接修改代碼即可看到效果,類似熱更新,可以幫我們縮短整個(gè)程序的開(kāi)發(fā)周期和功能更新周期。
編譯器:把一種編程語(yǔ)言(原始語(yǔ)言)轉(zhuǎn)換為另一種編程語(yǔ)言(目標(biāo)語(yǔ)言)的程序叫做編譯器
采用編譯器生成機(jī)器碼執(zhí)行的好處是效率高,缺點(diǎn)是調(diào)試周期長(zhǎng)。
解釋器執(zhí)行的好處是編寫(xiě)調(diào)試方便,缺點(diǎn)是執(zhí)行效率低。
編譯器分為前端和后端
- 前端:前端負(fù)責(zé)語(yǔ)法分析、詞法分析,生成中間代碼
- 后端:后端以中間代碼作為輸入,進(jìn)行架構(gòu)無(wú)關(guān)的代碼優(yōu)化,接著針對(duì)不同架構(gòu)生成不同的機(jī)器碼
在2007年之前LLVM使用GCC作為前端來(lái)對(duì)用戶程序進(jìn)行語(yǔ)義分析產(chǎn)生 IF(Intermidiate Format)。GCC系統(tǒng)龐大而笨重,因此,Apple決定從零開(kāi)始寫(xiě)C、C++、Objective-C語(yǔ)言的前端Clang,以求完全替代掉GCC。
現(xiàn)在蘋(píng)果公司使用的編譯器是 LLVM,前端是Clang,相比于 Xcode 5 版本前使用的 GCC,編譯速度提高了 3 倍。同時(shí),蘋(píng)果公司也反過(guò)來(lái)主導(dǎo)了 LLVM 的發(fā)展,讓 LLVM 可以針對(duì)蘋(píng)果公司的硬件進(jìn)行更多的優(yōu)化。
Clang于2007年開(kāi)始開(kāi)發(fā),C編譯器最早完成,在2009年的時(shí)候,Objective-C編譯器已經(jīng)完全可以用于生產(chǎn)環(huán)境,而在一年之后,Clang基本實(shí)現(xiàn)了對(duì)C++編譯的支持。
對(duì)于Apple來(lái)說(shuō)Objective C/C/C++使用的編譯器前端是clang,后端都是LLVM
LLVM 是編譯器工具鏈技術(shù)的一個(gè)集合。而其中的 lld 項(xiàng)目,就是內(nèi)置鏈接器。編譯器會(huì)對(duì)每個(gè)文件進(jìn)行編譯,生成 Mach-O(可執(zhí)行文件);鏈接器會(huì)將項(xiàng)目中的多個(gè) Mach-O 文件合并成一個(gè)。
編譯過(guò)程
- 預(yù)處理:Clang會(huì)預(yù)處理你的代碼,比如把宏嵌入到對(duì)應(yīng)的位置、注釋被刪除,條件編譯被處理
- 詞法分析:詞法分析器讀入源文件的字符流,將他們組織稱有意義的詞素(lexeme)序列,對(duì)于每個(gè)詞素,此法分析器產(chǎn)生詞法單元(token)作為輸出。并且會(huì)用Loc來(lái)記錄位置。
- 語(yǔ)法分析:這一步是把詞法分析生成的標(biāo)記流,解析成一個(gè)抽象語(yǔ)法樹(shù)(abstract syntax tree -- AST),同樣地,在這里面每一節(jié)點(diǎn)也都標(biāo)記了其在源碼中的位置。
AST 是抽象語(yǔ)法樹(shù),結(jié)構(gòu)上比代碼更精簡(jiǎn),遍歷起來(lái)更快,所以使用 AST 能夠更快速地進(jìn)行靜態(tài)檢查。 - 靜態(tài)分析:把源碼轉(zhuǎn)化為抽象語(yǔ)法樹(shù)之后,編譯器就可以對(duì)這個(gè)樹(shù)進(jìn)行靜態(tài)分析處理。靜態(tài)分析會(huì)對(duì)代碼進(jìn)行錯(cuò)誤檢查,如出現(xiàn)方法被調(diào)用但是未定義、定義但是未使用的變量等,以此提高代碼質(zhì)量。當(dāng)然,還可以通過(guò)使用 Xcode 自帶的靜態(tài)分析工具(Product -> Analyze)進(jìn)行手動(dòng)分析。最后 AST 會(huì)生成 IR,IR 是一種更接近機(jī)器碼的語(yǔ)言,區(qū)別在于和平臺(tái)無(wú)關(guān),通過(guò) IR 可以生成多份適合不同平臺(tái)的機(jī)器碼。
靜態(tài)分析的階段會(huì)進(jìn)行類型檢查,比如給屬性設(shè)置一個(gè)與其自身類型不相符的對(duì)象,編譯器會(huì)給出一個(gè)可能使用不正確的警告。在此階段也會(huì)檢查時(shí)候有未使用過(guò)的變量等。 - 中間代碼生成和優(yōu)化:此階段LLVM 會(huì)對(duì)代碼進(jìn)行編譯優(yōu)化,例如針對(duì)全局變量?jī)?yōu)化、循環(huán)優(yōu)化、尾遞歸優(yōu)化等,最后輸出匯編代碼xx.ll文件。
生成匯編代碼: 匯編器LLVM會(huì)將匯編碼轉(zhuǎn)為機(jī)器碼。此時(shí)的代碼就是.o文件,即二進(jìn)制文件。 - 鏈接:連接器把編譯產(chǎn)生的.o文件和(dylib,a,tbd)文件,生成一個(gè)mach-o文件。mach-o文件級(jí)可執(zhí)行文件。編譯過(guò)程全部結(jié)束,生成了可執(zhí)行文件Mach-O
連接器
Mach-O 文件里面的內(nèi)容,主要就是代碼和數(shù)據(jù):代碼是函數(shù)的定義;數(shù)據(jù)是全局變量的定義,包括全局變量的初始值。不管是代碼還是數(shù)據(jù),它們的實(shí)例都需要由符號(hào)將其關(guān)聯(lián)起來(lái)。
為什么呢?因?yàn)?Mach-O 文件里的那些代碼,比如 if、for、while 生成的機(jī)器指令序列,要操作的數(shù)據(jù)會(huì)存儲(chǔ)在某個(gè)地方,變量符號(hào)就需要綁定到數(shù)據(jù)的存儲(chǔ)地址。你寫(xiě)的代碼還會(huì)引用其他的代碼,引用的函數(shù)符號(hào)也需要綁定到該函數(shù)的地址上。而鏈接器的作用,就是完成變量、函數(shù)符號(hào)和其地址綁定這樣的任務(wù)。而這里我們所說(shuō)的符號(hào),就可以理解為變量名和函數(shù)名。
那為什么要讓鏈接器做符號(hào)和地址綁定這樣一件事兒呢?
如果地址和符號(hào)不做綁定的話,要讓機(jī)器知道你在操作什么內(nèi)存地址,你就需要在寫(xiě)代碼時(shí)給每個(gè)指令設(shè)好內(nèi)存地址。寫(xiě)這樣的代碼的過(guò)程,就像你直接在和不同平臺(tái)的機(jī)器溝通,連編譯生成 AST 和 IR 的步驟都省掉了,甚至優(yōu)化平臺(tái)相關(guān)的代碼都需要你自己編寫(xiě)。
可讀性和可維護(hù)性都會(huì)很差,比如修改代碼后對(duì)地址的維護(hù)就會(huì)讓你崩潰。而這種“崩潰”的罪魁禍?zhǔn)拙褪谴a和內(nèi)存地址綁定得太早。
用匯編語(yǔ)言來(lái)讓這種綁定滯后。隨著編程語(yǔ)言的進(jìn)化,我們很快就發(fā)現(xiàn),采用任何一種高級(jí)編程語(yǔ)言,都可以解決代碼和內(nèi)存綁定過(guò)早產(chǎn)生的問(wèn)題,同時(shí)還能掃掉使用匯編寫(xiě)程序的煩惱。
鏈接器為什么還要把項(xiàng)目中的多個(gè) Mach-O 文件合并成一個(gè)
項(xiàng)目中文件之間的變量和接口函數(shù)都是相互依賴的,所以這時(shí)我們就需要通過(guò)鏈接器將項(xiàng)目中生成的多個(gè) Mach-O 文件的符號(hào)和地址綁定起來(lái)。
沒(méi)有這個(gè)綁定過(guò)程的話,單個(gè)文件生成的 Mach-O 文件是無(wú)法正常運(yùn)行起來(lái)的。因?yàn)?,如果運(yùn)行時(shí)碰到調(diào)用在其他文件中實(shí)現(xiàn)的函數(shù)的情況時(shí),就會(huì)找不到這個(gè)調(diào)用函數(shù)的地址,從而無(wú)法繼續(xù)執(zhí)行。
鏈接器在鏈接多個(gè)目標(biāo)文件的過(guò)程中,會(huì)創(chuàng)建一個(gè)符號(hào)表,用于記錄所有已定義的和所有未定義的符號(hào)。鏈接時(shí)如果出現(xiàn)相同符號(hào)的情況,就會(huì)出現(xiàn)“l(fā)d: dumplicate symbols”的錯(cuò)誤信息;如果在其他目標(biāo)文件里沒(méi)有找到符號(hào),就會(huì)提示“Undefined symbols”的錯(cuò)誤信息。
鏈接器做了什么
- 在項(xiàng)目文件中查找目標(biāo)代碼文件里沒(méi)有定義的變量
- 掃描項(xiàng)目中的不同文件,將所有符號(hào)定義和引用地址收集起來(lái),并放到全局符號(hào)表中
- 計(jì)算合并后長(zhǎng)度及位置,生成同類型的段進(jìn)行合并,建立綁定
- 對(duì)項(xiàng)目中不同文件里的變量進(jìn)行地址重定位
- 去除無(wú)用函數(shù):鏈接器在整理函數(shù)的調(diào)用關(guān)系時(shí),會(huì)以 main 函數(shù)為源頭,跟隨每個(gè)引用,并將其標(biāo)記為 live。跟隨完成后,那些未被標(biāo)記 live 的函數(shù),就是無(wú)用函數(shù)。然后,鏈接器可以通過(guò)打開(kāi) Dead code stripping 開(kāi)關(guān),來(lái)開(kāi)啟自動(dòng)去除無(wú)用代碼的功能。并且,這個(gè)開(kāi)關(guān)是默認(rèn)開(kāi)啟的。
動(dòng)態(tài)庫(kù)鏈接
在真實(shí)的 iOS 開(kāi)發(fā)中,你會(huì)發(fā)現(xiàn)很多功能都是現(xiàn)成可用的,比如 系統(tǒng)庫(kù)、GUI 框架、I/O、網(wǎng)絡(luò)等。鏈接這些共享庫(kù)到你的 Mach-O 文件,也是通過(guò)鏈接器來(lái)完成的。
鏈接的共用庫(kù)分為靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù):靜態(tài)庫(kù)是編譯時(shí)鏈接的庫(kù),需要鏈接進(jìn)你的 Mach-O 文件里,如果需要更新就要重新編譯一次,無(wú)法動(dòng)態(tài)加載和更新;而動(dòng)態(tài)庫(kù)是運(yùn)行時(shí)鏈接的庫(kù),使用 dyld 就可以實(shí)現(xiàn)動(dòng)態(tài)加載。
Mach-O 文件是編譯后的產(chǎn)物,而動(dòng)態(tài)庫(kù)在運(yùn)行時(shí)才會(huì)被鏈接,并沒(méi)參與 Mach-O 文件的編譯和鏈接,所以 Mach-O 文件中并沒(méi)有包含動(dòng)態(tài)庫(kù)里的符號(hào)定義。也就是說(shuō),這些符號(hào)會(huì)顯示為“未定義”,但它們的名字和對(duì)應(yīng)的庫(kù)的路徑會(huì)被記錄下來(lái)。運(yùn)行時(shí)通過(guò) dlopen 和 dlsym 導(dǎo)入動(dòng)態(tài)庫(kù)時(shí),先根據(jù)記錄的庫(kù)路徑找到對(duì)應(yīng)的庫(kù),再通過(guò)記錄的名字符號(hào)找到綁定的地址。
dlopen 會(huì)把共享庫(kù)載入運(yùn)行進(jìn)程的地址空間,載入的共享庫(kù)也會(huì)有未定義的符號(hào),這樣會(huì)觸發(fā)更多的共享庫(kù)被載入。dlopen 也可以選擇是立刻解析所有引用還是滯后去做。dlopen 打開(kāi)動(dòng)態(tài)庫(kù)后返回的是引用的指針,dlsym 的作用就是通過(guò) dlopen 返回的動(dòng)態(tài)庫(kù)指針和函數(shù)符號(hào),得到函數(shù)的地址然后使用。
使用 dyld 加載動(dòng)態(tài)庫(kù),有兩種方式:有程序啟動(dòng)加載時(shí)綁定和符號(hào)第一次被用到時(shí)綁定。為了減少啟動(dòng)時(shí)間,大部分動(dòng)態(tài)庫(kù)使用的都是符號(hào)第一次被用到時(shí)再綁定的方式。
加載過(guò)程開(kāi)始會(huì)修正地址偏移,iOS 會(huì)用 ASLR 來(lái)做地址偏移避免攻擊,確定 Non-Lazy Pointer 地址進(jìn)行符號(hào)地址綁定,加載所有類,最后執(zhí)行 load 方法和 Clang Attribute 的 constructor 修飾函數(shù)。每個(gè)函數(shù)、全局變量和類都是通過(guò)符號(hào)的形式定義和使用的,當(dāng)把目標(biāo)文件鏈接成一個(gè) Mach-O 文件時(shí),鏈接器在目標(biāo)文件和動(dòng)態(tài)庫(kù)之間對(duì)符號(hào)做解析處理。
dylib 這種格式,表示是動(dòng)態(tài)鏈接的,編譯的時(shí)候不會(huì)被編譯到執(zhí)行文件中,在程序執(zhí)行的時(shí)候才 link,這樣就不用算到包大小里,而且不更新執(zhí)行程序就能夠更新庫(kù)。
系統(tǒng)上的動(dòng)態(tài)鏈接器會(huì)使用共享緩存,共享緩存在 /var/db/dyld/。當(dāng)加載 Mach-O 文件時(shí),動(dòng)態(tài)鏈接器會(huì)先檢查是否有共享緩存。每個(gè)進(jìn)程都會(huì)在自己的地址空間映射這些共享緩存,這樣做可以起到優(yōu)化 App 啟動(dòng)速度的作用。
編譯和啟動(dòng)速度
編譯階段由于有了鏈接器,代碼可以寫(xiě)在不同的文件里,每個(gè)文件都能夠獨(dú)立編成 Mach-O 文件進(jìn)行標(biāo)記。編譯器可以根據(jù)你修改的文件范圍來(lái)減少編譯,通過(guò)這種方式提高每次編譯的速度。
這也是為什么文件越多,鏈接器鏈接 Mach-O 文件所需綁定的遍歷操作就會(huì)越多,編譯速度也會(huì)越慢。
開(kāi)發(fā)時(shí)啟動(dòng)優(yōu)化:對(duì)于大型APP項(xiàng)目在開(kāi)發(fā)調(diào)試階段,是不是代碼改完以后可以先不去鏈接項(xiàng)目里的所有文件,只編譯當(dāng)前修改的文件動(dòng)態(tài)庫(kù),通過(guò)運(yùn)行時(shí)加載動(dòng)態(tài)庫(kù)及時(shí)更新,看到修改的結(jié)果。這樣調(diào)試的速度,不就能夠得到質(zhì)的提升了么?