Source code file 經過
最終變為 Relocatable object file
( relocatable
代表此 file 可以再與其他 object file 結合),如同上圖中的 main.o
和 sum.o
。
Linker 將所需的 object file 結合為 executable object file
,如同上圖的 prog
。
shell 呼叫作業系統中的 loader 將 executable object file 中的程式碼與資料複製到 main memory 中,並將控制權交給程式,讓它開始執行。
共同的 Functions 可以被寫為 library 達到模組化的作用。
linker 將不同的 relocatable object file 作為輸入,最終產生可以載入且運行的 executable object file 。
在程式碼中,我們不斷的 define 或 reference symbol ,用以下例子來說明。
在 symbol rsolution 階段, linker 需要將每個 symbol reference 連結到正確且唯一的 symbol definition , symbol definition 則從所有的 relocatable object file 中的 symbol table 尋找。
在本篇後面的章節有更細的討論。
在 symbol resolution 章節中, linker 為每個 symbol reference 到一個 symbol definition ,這時 linker 就可以知道每個 module 的 .text
和 .data
確切大小,即可開始做 relocation 。
各個 relocatable object file 都有 code 和 data sections ,這個階段會將分散在各個 file 相同類型的 section 合為單一的 section ,如同上圖所示。 Linker 替每個 symbol definition (每個 function 跟全域變數) 分配一個執行時的唯一 address 。
這邊也呼應到在區別宣告跟定義的時候,會將定義認定為替某個 symbol 分配記憶體空間,當 linker 在執行 linking 的時候,也是找出唯一的一個 strong symbol ,並為它分配一個執行時的唯一 address 。
在 symbol resolution 步驟中,每個 symbol reference 都被連結到某一個 symbol definition ,在這個步驟就將 symbol reference 指到正確的執行時 address 。
經過上述兩個步驟後,最終產生 executable object file ,裏面的 .text
和 .data
section 內的 symbol 都已經被重新定位, loader 可以直接將這些 section 複製到 memory 中即可開始執行程式,不需要再修改任何指令。
reference 這個單字很常在 compile error log 中看到:
這就是 linker 執行 symbol resolution 過程中,它無法為 foo
這個 symbol 找到一個 symbol definition 來 reference 。
Object file 有以下三種格式,object file 可以理解為 module (.c file) 以 byte sequence 的形式儲存在磁碟中。
在 linux 中,使用 Executable and Linkable Format 來作為以上三種 object file 的統一格式,這邊就不細講各個欄位。
.bss
是用來儲存未初始化或是初始為0的全域或 static 變數,在 object file 中,他們是不佔記憶體空間的,在運行時才會分配記憶體,因此,.bss
可以記憶為 "Better Save Space" ,幫助區分.bss
和.data
。
下圖是一個典型的 reloctable object file 的 ELF 格式:
.text
section 中的 binary format command 。在每個 relocatable object file 中都會維護一個 system table ,透過 $ readelf -s
可以看到的 .symtab
section 就是 symbol table , 裏面紀錄著該 module 定義與引用 symbol 的資訊,有以下三種 symbol:
Local variable 並不是 Local symbol ,local symbol 指的是以 static 定義的函式與變數,會被放在 ELF 中的 .bss
或 .data
區塊。 Local variable 是由 compiler 所負責的,執行時放在 stack 中進行管理,因此 Linker 對 local variable 是一無所知的。
執行 Symbol resolution 會面臨到一個問題,我們看以下例子,
[func1.c]
[func2.c]
func1
和 func2
都定義了名為 p
的 global symbol ,若是有人 reference p
這個 symbol 時, Linker 該怎麼去做 Linking ? 換句話說,若是有多個 module 都定義了相同名字的 global symbol , Linker 該怎麼去做 linking 呢?
Linker 利用以下列規則來決定如何做 Linking :
透過以下例子來理解以上的概念:
[foo1.c]
[foo2.c]
因為有兩個 strong symbol , Linker 會報出錯誤。
[foo3.c]
[foo4.c]
在 [foo4.c] 中的 x
是一個 weak symbol ,根據規則2, Linker 會將其 link 到 [foo3.c] 的 x
,但這邊會有一個問題,在 main 中你印出的值將變成 64 ,這對於 main()
的作者來說,應該不是預期的結果且相當討厭。
[bar1.c]
[bar2.c]
在 [bar2.c] 的 x
是一個 weak symbol , Linker 會將其 link 到 [bar1.c] 的 x
,這邊除了存在類似範例 2 的問題,這兩個 symbol 還是不同 type , double 在我的機器上是 8 byte 大小,而 integer 是 4 byte 大小,因此在 main()
中, 因為 x
被轉變為 8 byte 大小, y
會因此被覆蓋… 這是一個細微難以察覺的 bug ,尤其是因為他只會觸發 Linker 爆出一條 warning ,且通常要在程式執行很久之後才會表現出來。 如果你懷疑這類問題,用像 GCC-fno-common
的 flag 來呼叫 linker ,這個 flag 告訴 linker 遇到多重定義的 global symbol 時,觸發一個錯誤,或者使用 -Werror 把所有 warning 視為 error 。
static
extern
來標示你 reference 到外部的全域變數。不過若是在某個 file 忘記使用的話,依然會遇到一樣的問題…範例3的錯誤若是在大型程式中,是個相當難發現的錯誤,這也是為什麼要了解 linking 是如何運作的。
開發程式中,一定都會引用標準函式庫,裏面包含常見的函式,讓大家免於重新造輪子的痛苦,這樣的標準函式庫如同 reloctable object file 一樣可以作為 Linker 的輸入,同樣參與 symbol resolution 的過程, linker 只複製 static libraries 裡被 application 引用的目標 module 。
以下舉例
編譯所下指令為
-static
告訴 compiler driver , Linker 應該產生一個可以完全 Linking 的 Executable object file ,可以直接載入到記憶體執行,無需做其他 linking 動作。-L.
告知 Linker 當前目錄下尋找 library 。-lvector
則是 libvector.a 的縮寫。對於 linker 來說,他會照著輸入到 compiler driver 的順序來做 Linking ,若是有 symbol reference 找不到對應的 definition,就會暫時紀錄下來,從接下來輸入的 library 和 object file 中繼續做 symbol resolution ,當 Linker 都掃描完所有輸入文件後,所有 symbol reference 應該都找到對應的 definition 並產生出 executable object file ,否則 Linker 將報出錯誤。
上述規則會造成做 Linking 時的困擾,如果我們將輸入到 compiler driver 的順序改變一下,如同以下
輸入到 Linker 的順序中, main.o
是最後一個輸入,若是裏面有使用到定義於 library 中的 symbol ,由於他後面沒有任何文件輸入,在 main.o
中的 symbol reference 將連結不到定義,而造成 Linker 報出錯誤…
因此,一般建議將 library 放到 compiler driver 輸入的結尾來避免這種困擾。
Static library 存在以下缺點,也使得後來出現 shared library 來解決這些問題:
printf
, Linker 在執行時,都需要將 printf
的 object file 複製到最終的 executable object file 中。 在程式執行時,這些常用函數又會被複製到執行中的 process 的 text 中。 在一個執行數百個程式的系統中,將對記憶體資源造成極大的浪費。shared libray 是一個 object module ,在載入 (load-time linking)或是執行階段 (run-time linking),都可以被載入到任意的位址,由 dynamic linker 負責將 shared library 和一個在記憶體中的程式做到 linking ,這樣的過程稱為 dynamic linking 。
以上圖來看如何使用 shared library 做到 load-time linking 。
-fpic
代表請 compiler 生成與位置無關的程式碼-shared
指示 Linker 建立共享的 object fileprog21
。什麼是靜態執行 部份 linking ?
以上圖例子來說,關鍵在於沒有任何
libvector.so
的程式碼或是 data 被複製到prog21
中 (這點就是 static library 消耗過多記憶體的原因)。
相反地, Linker 複製了跟libvector.so
有關 relocation 和 symbol table 等資訊,以便在載入或執行prog21
時候可以去做 relocation 。
prog21
到記憶體中, Loader 藉由檢查 executable file 中是否包含 .interp
section 來決定是否已經完全的做完 linking ,如果已經完成所有 linking , loader 會將控制權交給程式開始執行,否則會將 .interp
內包含的 dynamic linker 的執行路徑讀出,載入並執行 dynamic linker 。.dynamic
section 中知道該使用哪些 shared libraries 。以上圖例子來說, Loader (在 linux 中會是 execve)會從
prog21
中發現.interp
section 裏面包含 dynamic linker 的執行路徑(在 linux 中會是 ld-linux.so)並執行, dynamic linker 接著執行以下 relocation 來完成 linking 。
- relocate
libc.so
的.text
和.data
到某個記憶體位址上。- relocate
libvector.so
的.text
和.data
到某個記憶體位址上。- relocate 在
prog21
中的特定 symbol reference ,這些 symbol reference 會連結到libc.so
和libvector.so
中的 symbol defintion 。當所有 linking 都完成,控制權將交給程式,程式即可開始執行。
CS:APP
投影片來源:http://www.cs.cmu.edu/afs/cs/academic/class/15213-f19/www/lectures/14-linking.pdf
上課影片:https://youtu.be/wJRpLEP6rHU