iOS開發你不知道的事-編譯&連結

Cooci_和諧學習_不急不躁發表於2019-05-10

對於平常的應用程式開發,我們很少需要關注編譯連結過程。我們平常Xcode開發就是整合的的開發環境(IDE),這樣的IDE一般都將編譯連結的過程一步完成,通常將這種編譯連結合併在一起的過程稱為構建,即使使用命令列來編譯一個原始碼檔案,簡單的一句gcc hello.c命令就包含了非常複雜的過程!

正是因為整合開發環境的強大,很多系統軟體的執行機制與機理被掩蓋,其程式的很多莫名其妙的錯誤讓我們無所適從,面對程式執行時種種效能瓶頸我們束手無策。我們看到的是這些問題的現象,但是卻很難看清本質,所有這些問題的本質就是軟體執行背後的機理及支撐軟體執行的各種平臺和工具,如果能深入瞭解這些機制,那麼解決這些問題就能夠遊刃有餘。

編譯流程分析

現在我們通過一個C語言的經典例子,來具體瞭解一下這些機制:

#include <stdio.h>
int main(){
    printf("Hello World");
    return 0;
}
複製程式碼

在linux下只需要一個簡單的命令(假設原始碼檔名為hello.c):

$ gcc hello.c
$ ./a.out
Hello World
複製程式碼

其實上述過程可以分解為四步:

  • 預處理(Prepressing)
  • 編譯(Compilation)
  • 彙編(Assembly)
  • 連結(Linking)

iOS開發你不知道的事-編譯&連結

預編譯

首先是原始碼檔案hello.c和相關的標頭檔案(如stdio.h等)被預編譯器cpp預編譯成一個.i檔案。第一步預編譯的過程相當於如下命令(-E 表示只進行預編譯):

$ gcc –E hello.c –o hello.i
複製程式碼

還可以下面的表達

$ cpp hello.c > hello.i
複製程式碼

預編譯過程主要處理原始碼檔案中以”#”開頭的預編譯指令。比如#include、#define等,主要處理規則如下:

  • 將所有的#define刪除,並展開所有的巨集定義
  • 處理所有條件預編譯指令,比如#if,#ifdef,#elif,#else,#endif
  • 處理#include預編譯指令,將被包含的檔案插入到該預編譯指令的位置。
  • 刪除所有的註釋///**/
  • 新增行號和檔名標識,比如#2 “hello.c” 2。
  • 保留所有的#pragma編譯器指令

截圖個大家看看效果

iOS開發你不知道的事-編譯&連結

經過預編譯後的檔案(.i檔案)不包含任何巨集定義,因為所有的巨集已經被展開,並且包含的檔案也已經插入到.i檔案中,所以當我們無法判斷巨集定義是否正確或標頭檔案包含是否正確時,可以檢視預編譯後的檔案來確定問題。

編譯(compliation)

編譯過程就是把預處理完的檔案進行一系列的:詞法分析語法分析語義分析優化後生產相應的彙編程式碼檔案,此過程是整個程式構建的核心部分,也是最複雜的部分之一。其編譯過程相當於如下命令:

$ gcc –S hello.i –o hello.s
複製程式碼

iOS開發你不知道的事-編譯&連結
通過上圖我們不難得出,通過命令得到彙編輸出檔案hello.s.

彙編(assembly)

彙編器是將彙編程式碼轉變成機器可以執行的指令,每一個彙編語句幾乎對應一條機器令。所以彙編器的彙編過程相對於編譯器來講比較簡單,它沒複雜的語法,也沒有語義,也不需要做指令優化,只是根據彙編指令和機器指令的對照表一一翻譯就可以了。其彙編過程相當於如下命令:

as hello.s –o hello.o
複製程式碼

或者

gcc –c hello.s –o hello.o
複製程式碼

或者使用gcc命令從C原始碼檔案開始,經過預編譯、編譯和彙編直接輸出目標檔案:

gcc –c hello.c –o hello.o
複製程式碼

連結(linking)

  連結通常是一個讓人比較費解的過程,為什麼彙編器不直接輸出可執行檔案而是輸出一個目標檔案呢?為什麼要連結?下面讓我們來看看怎麼樣呼叫ld才可以產生一個能夠正常執行的Hello World程式:

注意預設情況沒有gcc / 記得 :
$ brew install gcc
複製程式碼

連結相應的庫

iOS開發你不知道的事-編譯&連結

下面在貼出我們的寫出的原始碼是如何變成目的碼的流程圖:

iOS開發你不知道的事-編譯&連結

主要通過我們的編譯器做了以下任務:掃描、語法分析、語義分析、原始碼優化、程式碼生成和目的碼優化

到這我們就可以得到以下的檔案,不知道你是否有和我一起操作,玩得感覺還是不錯,繼續往下面看

iOS開發你不知道的事-編譯&連結

iOS的編譯器

iOS現在為了達到更牛逼的速度和優化效果,採用了LLVM

LLVM採用三相設計,前端Clang負責解析,驗證和診斷輸入程式碼中的錯誤,然後將解析的程式碼轉換為LLVM IR,後端LLVM編譯把IR通過一系列改進程式碼的分析和優化過程提供,然後被髮送到程式碼生成器以生成本機機器程式碼。

iOS開發你不知道的事-編譯&連結
編譯器前端的任務是進行:

  • 語法分析
  • 語義分析
  • 生成中間程式碼(intermediate representation )

在這個過程中,會進行型別檢查,如果發現錯誤或者警告會標註出來在哪一行。

iOS開發你不知道的事-編譯&連結

以上圖解內容所做的是事情和gcc編譯一模模一樣樣!

iOS程式-詳細編譯過程

  • 1.寫入輔助檔案:將專案的檔案結構對應表、將要執行的指令碼、專案依賴庫的檔案結構對應表寫成檔案,方便後面使用;並且建立一個 .app 包,後面編譯後的檔案都會被放入包中;
  • 2.執行預設指令碼:Cocoapods 會預設一些指令碼,當然你也可以自己預設一些指令碼來執行。這些指令碼都在 Build Phases中可以看到;
  • 3.編譯檔案:針對每一個檔案進行編譯,生成可執行檔案 Mach-O,這過程 LLVM 的完整流程,前端、優化器、後端;
  • 4.連結檔案:將專案中的多個可執行檔案合併成一個檔案;
  • 5.拷貝資原始檔:將專案中的資原始檔拷貝到目標包;
  • 6.編譯 storyboard 檔案:storyboard 檔案也是會被編譯的;
  • 7.連結 storyboard 檔案:將編譯後的 storyboard 檔案連結成一個檔案;
  • 8.編譯 Asset 檔案:我們的圖片如果使用 Assets.xcassets 來管理圖片,那麼這些圖片將會被編譯成機器碼,除了 iconlaunchImage
  • 9.執行 Cocoapods 指令碼:將在編譯專案之前已經編譯好的依賴庫和相關資源拷貝到包中。
  • 10.生成 .app
  • 11.將 Swift 標準庫拷貝到包中
  • 12.對包進行簽名
  • 13.完成打包

編譯過程的確是個比較複雜的過程,還有連結!並不是說難就不需要掌握,我個人建議每一個進階路上iOS開發人員,都是要了解一下的。不需要你多麼牛逼,但是你能在平時的交流討論,面試中能點出一個兩個相應的點,我相信絕對是逼格滿滿!

相關文章