如果我們在主程式main.c中使用到一個函數foobar(),對main.c進行編譯,編譯器還不知道foobar()函數的地址,在編譯階段生成的main.o中,foobar一個未解析的參照。
連結器將main.o連結成可執行檔案時,必須確定main.o中所參照的foorbar()函數的性質。如果foobar()是一個定義在其他靜態目標模組(靜態庫或者目標檔案)中的函數,那麼連結器會按照靜態連結的規則將main.o中的foobar()函數地址參照重定位。
gcc -o main.out mian.c ./libfoo.a
如果foobar()是定義在某個動態共用庫中的函數,那麼連結器就會把foobar標記為一個動態連結的符號,foobar此時是一個對動態符號的參照。不對它進行重定位。
gcc -o main.out main.c ./libfoo.so
動態庫發展過程有三個階段。
靜態共用庫不是靜態庫,是指共用庫在編譯時已經確定了自己在程序虛擬地址空間的位置。作業系統在某個特定的地址劃分出一些地址塊,為那些已知的模組預留足夠的空間。在連結檢視中,靜態共用庫在程序的虛擬地址空間的地址已經被決定了,如果後續要升級靜態共用庫,還是需要重新編譯,連結可執行ELF。
裝載重定位是指在連結時,對所有絕對地址的參照不作重定位,推遲到裝載時再完成。程式是按照整體進行裝載,程式中指令和資料相對位置不會改變,程式的基地址可能每次裝載時不一樣。一旦模組裝載地址確定,系統就可以對程式中所有的絕對地址進行重定位。
Linux GCC選項
gcc -shared -o libbook.so book.c
使用裝載時重定位,動態連結模組被裝載對映到虛擬地址空間後,指令會被修改,多個程序無法共用同一份指令程式碼。指令被重定位以後對每個應用程式來說是不同的。演進階段2的裝載時重定位,無法解決指令部分需要在多個程序之間共用的需求。也就失去了動態連結節省記憶體的優勢。
要在多個程序之間共用指令部分,需要將指令進行分類。按照是否跨模組分兩類:模組內參照和模組外部參照; 按照參照方式分兩類:指令參照和資料存取。這樣一共有四種組合:模組內部的函數呼叫、跳轉,模組內部的資料存取(比如模組中定義的全域性變數、靜態變數),模組間的函數呼叫、跳轉,模組間的資料存取(比如其他模組中定義的全域性變數)。
型別一 模組內部函數呼叫、跳轉
模組內部的跳轉、函數呼叫都可以使用相對地址呼叫,不需要重定位。
型別二 模組內部資料存取
模組內部指令與模組內部資料之間的相對位置是固定的,只需要相對於當前指令加上固定的偏移量就可以存取模組內部資料。
型別三 模組間的資料存取
模組間的資料存取目的地址要等到裝載時才能確定,ELF檔案的做法是在資料段裡面建一個指向這些模組間全域性變數的指標陣列,也被稱為全域性偏移表(Global Offset Table, GOT),當程式碼需要參照模組間全域性變數時,可以通過GOT中的表項間接的參照。由於GOT是存放在資料段中,所以動態庫在裝載時可以被修改,每個程序都有獨立的副本,相互之間不受影響。在編譯時可以確定GOT相對於當前指令的偏移,編譯器決定GOT內的每一項(4個位元組為一項,一個指標)對應於哪一個全域性變數名稱,也就GOT給出了需要重定位的全域性變數有哪些,以及該全域性變數相對於GOT的位置。動態連結器在裝載模組時會查詢每個變數所在地址,然後填充GOT中的各個項,確保GOT中每個指標所指向的地址是正確的。
型別四 模組間的函數呼叫、跳轉
與型別三類似,也是通過GOT,在GOT中存放的是目標函數的地址,當模組需要呼叫目標函數時,可以通過GOT進行間接跳轉。
使用GCC產生地址無關程式碼
gcc -fPIC -shared -o libbook.so book.c
注意:這裡的-fPIC中的PIC是大寫,也有小寫的-fpic(產生的程式碼相對較小,而且較快),在有些平臺使用小寫的-fpic選項有一些限制,而大寫的-fPIC沒有這個問題。絕大多數情況使用-fPIC。
可以通過以下方法檢視一個so是不是PIC,如果是PIC,以下命令會有輸出,如果不是PIC,則不會有任何輸出。
readelf -d libbook.so | grep TEXTREL