1. 程式人生 > Android開發 >iOS 開發 連結器的作用

iOS 開發 連結器的作用

首先我們要了解iOS使用的是什麼編譯器?

蘋果公司現在使用的編譯器是LLVM,相比於 Xcode 5 版本前使用的 GCC,編譯速度提高了 3 倍。同時,蘋果公司也反過來主導了 LLVM 的發展,讓 LLVM 可以針對蘋果公司的硬體進行更多的優化。

LLVM是什麼?

LLVM是構架編譯器(compiler)的框架系統,以C++編寫而成,用於優化以任意程式語言編寫的程式的編譯時間(compile-time)、連結時間(link-time)、執行時間(run-time)以及空閒時間(idle-time),對開發者保持開放,併相容已有指令碼。

廣義的LLVM其實就是指整個LLVM編譯器架構,包括了前端、後端、優化器、眾多的庫函式以及很多的模組;而狹義的LLVM其實就是聚焦於編譯器後端功能(程式碼生成、程式碼優化、JIT等)的一系列模組和庫。

總結來說,LLVM 是編譯器工具鏈技術的一個集合。而其中的 lld 專案,就是內建連結器。編譯器會對每個檔案進行編譯,生成 Mach-O(可執行檔案);連結器會將專案中的多個 Mach-O 檔案合併成一個。

LLVM的三個階段

前端(Frontend)-- 優化器(Optimizer)-- 後端(Backend)

編譯的幾個重要過程:

  • 寫好程式碼後,LLVM會預處理程式碼,比如把巨集嵌入對應的位置。
  • 預處理完後,前端可以使用不同的編譯工具對程式碼檔案做詞法分析以形成抽象語法樹AST(語法樹結構上比程式碼更精簡,遍歷起來更快,所以使用AST能夠更快速地進行靜態檢查),然後將分析好的程式碼轉換成LLVM的中間表示IR(intermediate representation);
  • 中間部分的優化器只對中間表示IR操作,通過一系列的pass對IR做優化。IR 是一種更接近機器碼的語言,區別在於和平臺無關,通過 IR 可以生成多份適合不同平臺的機器碼。對於 iOS 系統,IR 生成的可執行檔案就是 Mach-O。
  • 後端負責將優化好的IR解釋成對應平臺的機器碼。

編譯時連結器做了什麼?

連結通常是一個讓人比較費解的過程,為什麼彙編器不直接輸出可執行檔案而是輸出一個目標檔案呢?連結過程到底包含了什麼內容?為什麼要連結?

簡單來講就是隨之軟體規模越來越大,每個程式被分成了多個模組,這些模組的拼接過程就叫:連結(Linking)。

連結的過程:

  • 地址和空間的分配(Address and Storage Alloction)
  • 符號決議(Symbol Resolution)Ps:"決議"更傾向於靜態連結,而"繫結"更傾向於動態連結。
  • 重定位(Relocation)

重定位的過程如下:

假設有個全域性變數叫做var,它在目標檔案A裡面。我們在目標檔案B裡面要訪問這個全域性變數。由於在編譯目標檔案B的時候,編譯器並不知道變數var的目標地址,所以編譯器在沒法確定的情況下,將目標地址設定為0,等待連結器在目標檔案A和B連線起來的時候將其修正。這個地址修正的過程被叫做重定位,每個被修正的地方叫一個重定位入口。

連結的過程本質就是要把多個不同的目標檔案之間相互"粘到一起"。在連結中目標檔案之間互相拼合實際上是目標檔案之間對地址的引用,即對函式和變數地址的引用。比如目標檔案B要用到目標檔案A中的函式“foo”,那麼我們就稱目標檔案A定義(Define)了函式“foo”,稱目標檔案B引用(Reference)了目標檔案A中的函式“foo”。這樣的概念同樣適用於變數。每個函式或變數都有自己獨特的名字,才能避免連結過程中不同變數和函式之間的混淆。

在連結中,我們將函式和變數統稱為符號(Symbol),函式名或變數名就是符號名(Symbol Name)。

由於連結形式的不同,產生了靜態連結和動態連結

靜態連結

整個連結過程分為兩步:

  • 第一步:空間與地址的分配 掃描所有的輸入目標檔案,並獲得它們各個段的長度、屬性和位置,並且將輸入目標檔案中的符號表中所有的符號定義和符號引用收集起來,統一放到一個全域性符號表。這一步中,連結器將能夠獲得所有輸入目標檔案的段長度,並且將它們合併,計算出輸出檔案中各個段合併後的長度與位置,並建立對映關係。
  • 第二步:符號解析與重定位 使用上面第一步中收集到的所有資訊,讀取輸入檔案中斷的資料、重定位資訊,並且進行符號解析與重定位、調整程式碼中的地址等。事實上第二步是連結過程的核心,特別是重定位過程。

動態連結

動態連結解決了浪費記憶體和磁碟空間、模組更新困難等問題,要解決空間浪費和更新困難這兩個問題的辦法就是把程式模組互相分割開來,行程獨立的檔案,而不再將它他們靜態地連結在一起。簡單是將,就是不對哪些組成程式的目標檔案進行連結,等到程式要執行時才進行連結。也就是說,把連結這個過程推遲到了執行時再進行,這就是動態連結(Dynamic Linking)的基本思想。

連結的共用庫分為靜態庫和動態庫:靜態庫是編譯時連結的庫,需要連結進你的 Mach-O 檔案裡,如果需要更新就要重新編譯一次,無法動態載入和更新;而動態庫是執行時連結的庫,使用 dyld 就可以實現動態載入。

由於動態共享物件會被多個程式使用,導致它在虛擬地址空間中的位置難以確定。不同模組的目標裝載地址如果有相同的,那麼同時匯入這兩個模組就會出問題。如果都不一樣也不行,因為可能存在的模組太多了。沒有那麼多記憶體。所以動態共享物件需要在裝載時重定位。

裝載時重定位就是:在連結時,對所有絕對地址的引用不做重定位,而把這一步推遲到裝載時再完成。一旦模組裝在地址確定,即目標地址確定,那麼系統就對程式中所有的絕對地址進行重定位。

Mach-O 檔案是編譯後的產物,而動態庫在執行時才會被連結,並沒參與 Mach-O 檔案的編譯和連結,所以 Mach-O 檔案中並沒有包含動態庫裡的符號定義。也就是說,這些符號會顯示為“未定義”,但它們的名字和對應的庫的路徑會被記錄下來。執行時通過 dlopen 和 dlsym 匯入動態庫時,先根據記錄的庫路徑找到對應的庫,再通過記錄的名字元號找到繫結的地址。

dlopen()函式用來開啟一個動態庫,並將其載入到程式的地址空間,完成初始化過程,它的C原型定義為

/*第一個引數是被載入的路徑,如果是路徑是絕對路徑(以“/”開始的路徑),則該函式將會嘗試直接開啟該動態庫;如果是相對路徑,那麼dlopen()會嘗試在以一定的順序去查詢該動態庫檔案
第二個引數flag表示函式符號的解析方式。
RTLD_LAZY:表示使用延遲繫結,函式第一次被用到時才進行繫結,即PLT機制;
RTLD_NOW:表示當模組被載入時即完成所有的函式繫結工作,如果有任何未定義的符號音樂繫結工作沒法完成,那麼dlopen()就返回錯誤;
RTLD_GLOBAL:可以跟上面兩者任意一個一起使用(通過常量的“或”操作),它表示將被載入的模組的全域性符號合併到程式的全域性符號中,使得以後載入的模組可以使用這些符號。
*/
void * dlopen(const char *filename,int flag);

複製程式碼

dlsym 函式基本是執行時裝載的核心部分,我們可以通過這個函式找到所需要的符號,它的定義如下:

/*
第一個引數是由dlopen()返回的動態庫的控制程式碼;
第二個引數即所需要查詢的符號的名字,一個以“\0”結尾的C字串。如果dlsym()找到了相應的符號,則返回該符號的值,沒有找到相應的符號,則返回NULL。
*/
void * dlsym(void * handle,char * symbol);

複製程式碼

dlopen 會把共享庫載入執行程式的地址空間,載入的共享庫也會有未定義的符號,這樣會觸發更多的共享庫被載入。dlopen 也可以選擇是立刻解析所有引用還是滯後去做。dlopen 開啟動態庫後返回的是引用的指標,dlsym 的作用就是通過 dlopen 返回的動態庫指標和函式符號,得到函式的地址然後使用。

使用 dyld 載入動態庫,有兩種方式:有程式啟動載入時繫結和符號第一次被用到時繫結。為了減少啟動時間,大部分動態庫使用的都是符號第一次被用到時再繫結的方式。

載入過程開始會修正地址偏移,iOS 會用 ASLR 來做地址偏移避免攻擊,確定 Non-Lazy Pointer 地址進行符號地址繫結,載入所有類,最後執行 load 方法和 Clang Attribute 的 constructor 修飾函式。

每個函式、全域性變數和類都是通過符號的形式定義和使用的,當把目標檔案連結成一個 Mach-O 檔案時,連結器在目標檔案和動態庫之間對符號做解析處理。

參考連結:

戴銘的iOS開發高手課

《程式設計師的自我修養》