Xcode構建過程的后臺工作(一)構建過程
Xcode構建過程的后臺工作(二)clang構建
Xcode構建過程的后臺工作(三)swift構建
構建過程:鏈接
這是 Xcode構建過程的最后一步。
首先瀏覽一下我們要討論的內容。我們將討論鏈接器是 什么,它所采用的輸入,即dylib和目標文件及其定義。還會講到符號及其內容。最后會舉例總結,因為內容比較難懂。
鏈接器是什么
鏈接器是構建中的最后一個過程。我們所做的是將兩個編譯器構建的所有.o文件 組合成一個可執行文件。它所做的就是移動和修補代碼。 它無法創建代碼,這 很重要,我將在示例中顯示。我們有兩種輸入文件。第一個是目標文件(.o),在構建過程中產生。第二個是庫,包括dylib,tbd和.a文件或靜態文檔。
符號(symbols)
符號是一個代表代碼或數據片段的名稱。當一個函數調用另一個函數,這些片段可能會指向其他符號。符號可以有很多影響鏈接器行為方式的屬性。我只舉一個 弱符號的例子。弱符號的注釋表示當我們在系統上運行或者執行文件時它可能不存在。還有可用性標記,標記這個API用于iOS12,那個API用于iOS11.這就引出了現在的主題鏈接器。鏈接器可以確定哪些符號肯定會出現和哪些符號可以在運行時處理。如前所述,語言可以通過命名修飾將數據編碼為符號。在C++和Swift中都能看到。所以符號就是指代碼和數據的名稱。
目標文件
目標文件就是代碼和數據的集合。它們不可執行。因為是編譯代碼,所以還沒有完成。還有缺失就需要鏈接器整合和修復。每個文件的片段都用符號表示。例如對于printf函數,就以符號代替代碼。對于PetKit的代碼后文會展示。
片段可能引用未定義的符號。 因此,如果您的.o文件引用另一個.o文件中的函數,那么.o文件是未定義的。鏈接器將找到那些未定義的符號并進行匹配。所以目標文件是編譯器操作的輸出。那么什么是庫?庫是定義符號的文件,但不屬于構建的目標。我們有動態庫,那些Mach-O文件,顯示了可執行文件的代碼和數據片段。這些是系統的一部分。這就是我們的框架。你也可能會用自己的框架。
還有TBD文件,基于文本的dylib文件。在為iOS和macOS制作SDK時,會有所有這些dylibs和以及您可能想要使用的功能,如MapKit和WebKit。但是我們不想把所有這些跟SDK一起加載,因為它會很大而且編譯器和鏈接器不需要它,它只在運行程序時有用。因此,我們創建了stub dylib,刪除所有符號的主體,只留下名稱。完成之后,轉用文本表示,這對我們來說更容易使用。目前,它們僅用于分發SDK以減小大小。所以你在項目中看到它們時不必擔心,它們只是符號。
最后是靜態庫(static archives)。靜態庫是使用AR工具構建的.o文件的集合,也可能是lib,它是lib工具的包裝器。根據AR操作文檔,AR創建并維護的文件組,將它們合并為一個庫。聽起來很像TAR文件或ZIP文件,這正是它的本質。實際上,.a格式是UNIX在使用更強大的工具之前使用的原始庫格式。但是現在的編譯器和連接器可以完全理解它們,所以繼續使用它們。它就只是個檔案 文件。值得注意的是,它們孕育了動態鏈接,在過去所有代碼都會被存檔。因此, 不能使用的是一個函數涵蓋所有C庫 。所以行為是,如果.o文件中有符號,我們會將整個.o文件從庫中拉出來。但是不會引入其他.o文件。如果你在它們之間引用符號,只要帶入即可。如果你是非符號行為,比如靜態初始化程序,或者將它們重新導出為您自己的dylib的一部分,您要明確地用到強制加載,或定制加載讓鏈接器提取所有或者這些文件,即便之間沒有關聯。我們通過一個例子串聯起這些內容。
上面是playSound函數的例子,只看寵物不聽聲音有什么樂趣?cat上有一個調用playSound的函數。上圖右邊是生成的程序集。輸出文件是cat.o。字符串purr.aac,是AAC聲音文件。這會被復制到cat.o. 您會注意到名稱purr文件不見了。因為它是靜態的。如果你熟悉C語言,這是非導出命名。沒有其他人可以引用它。既然如此,我們不需要它,排除掉。
然后我們看到Cat purr變成了符號:-[Cat purr]。跟預想的差不多。
然后我們要把這個變量傳遞給playSound。這里出現了兩個指令,這是因為我們不知道這個字符串最后在可執行文件中的位置,我們沒有具體的地址 。 但是我們知道RM64就是這個程序集,它最多可能需要兩條指令。所以編譯器給我們留下了兩條指令。它留下符號偏移量,值為PAGE和PAGEOFF,鏈接器之后回來修復。 最后,既然已經將字符串加載到x0中,我們可以調用playSound,我們寫入__z9playSoundPKc。這是一個變形的符號,如果仔細看會看到cat.mm,這是Objective-C++。playSound實際上是一個C++函數。所以如果你不熟悉,你可以在終端輸入命令。
如果運行Swift-demangle并傳入符號,然后反修飾。沒有用,它不是swift的符號。但是C++ 反修飾器C++ filts告訴我們這實際上是playSound的符號。 除了playSound,它還有一個實參。這個參數是一個const char* 因為C++會將更多信息編入修飾符號中。現在有了.o文件,實際構建中會有更多。那我們該怎么做呢?
首先,構建系統將把所有.o作為輸入傳遞給鏈接器。鏈接器會創建一個文件來放入它們 。這里構建的PetKit,是PetWall的內嵌框架。 因此,我們只要復制,創建一個文本片段用來保存app的所有代碼的。然后復制cat.o到這里。但是要分成兩部分,一個用于字符串,一個用于可執行代碼。現在已知這些東西的絕對地址,因此 鏈接器可以重寫cat.o,以從特定偏移量加載。 你會注意到第二條指令就消失了。它被一個null指令代替,沒有任何行動。但我們無法刪除 指令,因為我們無法創建或刪除代碼,這會打亂所有已完成的工作。所以與其刪除,不如替換為無行動。
最后是分支。我們有一個未定義的符號,我們將繼續瀏覽所有已經導入的.o文件。
所以我們將開始查看靜態庫,上圖是PetSupport.a。在PetSupport.a中有幾個文件,包括PetSounds.o。大家能看到匹配playSound的符號。 所以我們把它拉進來。PetCare.o不能被引入,因為.o文件沒有任何符號能被app的其他部分引用。我們把它拉進來,但現在需要_open,但是我們沒有定義。拉入的對話已經變成_open $stub。為什么呢?因為我們發現open的副本在lib系統的TBD文件中。
我們知道這不是系統庫的一部分,我不會其復制到我的app中。但是我需要在app中加入足夠的信息 以便調用它。
因此,我們創建了一個假函數 ,它只是一個模板,用來代替從lib系統拿走的函數,這里就是open。觀察該函數,它實際上是來自指針open$pointer,然后跳到它。這需要一個函數指針,就像任何普通的C函數指針一樣。然后在數據段中 創建它,如果有全局變量,那么就會出現在這里。但它只是設置為零,如果如果空引用會導致崩潰。然后我們添加一個LINKEDIT部分。
LINKEDIT是鏈接器工具用于為操作系統保留信息的元數據,這就是在運行時解決問題的動態鏈接器。有關這方面的更多信息,請查看2016年的 Optimizing App Startup Time 演講。