1. 連結的目的
原始碼經過編譯後,產生了一系列的目標檔案。每一個目標檔案,具有自己的程式碼片段、資料區段、符號表、重定位表等資訊(參考目標檔案結構介紹)。
在程式碼片段中,編譯產生的機器代碼需要訪問資料區段的資料,或是程式碼片段的函數。但在單個目標檔案中,只有局部的資料和代碼資訊。如下例:
檔案a.c:
unsigned long a = 10;
void main()
{
print_a();
}
檔案b.c:
#include <stdio.h>
extern unsigned long a;
void print_a()
{
printf(“%d”,a);
}
編譯後,a.c和b.c分別產生了a.o和b.o檔案。在a.o中,資料區段中為a,程式碼片段中有main;在b.o中,程式碼片段中有print_a函數。
a.o中,訪問了b.o中的print_a函數,而b.o中訪問了a.o中的資料a。但問題是每一個.o都只能知道自已的局部資料和函數的地址,因此就需要通過連結過程,對a.o和b.o中的指令(調用print_a函數及訪問a資料)進行重點位。這也是靜態連結的主要目的所在。
2.
連結的過程
連結主要分為兩個步驟以:
1)
空間與地址分配
每個.o檔案中都有自己的程式碼片段和資料區段,在多個檔案合并後,這些指令和資料如何組織?通常是採用相似段合并的方式,將多個.o檔案中的程式碼片段合并為一個程式碼片段,資料區段合并為一個資料區段。
這樣做的好處是可以空間消耗(因為作業系統在分配空間時最小單位是頁),並且可以提高記憶體訪問的命中率。
在進行相似段合并時,連結器會根據.o中每個段的長度、屬性和位置等,計算出合并後的長度與位置,然後並每一個.o的中的符號合并到全域符號表,記錄每一個符號的新的位置。
2)
符號解析與重定位
這一步是連結過程中核心,連結器通過重定位表,擷取到每一個需要進行重定位的指令的位置以及符號,然後通過符號表查詢符號的地址,修改指令中訪問的地址。
如果在這個過程中,無法在符號表中找到需要重定位的符號,就會出現連結失敗,也就是我們常見的”undefined reference to ‘xxx’”錯誤。
在符號表中,除了全域符號外,COMMON類型的符號也需要進行重定位,又稱為COMMON塊。COMMON塊通常用來處理弱符號。
編譯器將未初始化的全域變數定義為弱符號。當多個原始碼檔案中存在相同的弱符號時,連結器採用如下策略決定弱符號的大小:
(1) 存在同名的強符號時,以強符號為準
(2) 不存在同名強符號時,以最大的弱符號為準