1. 程式人生 > >在x64位Linux上生成動態連結庫必須使用編譯選項-fPIC的問題

在x64位Linux上生成動態連結庫必須使用編譯選項-fPIC的問題

在 Linux 下製作動態連結庫,“標準” 的做法是編譯成位置無關程式碼(Position Independent Code,PIC),然後連結成一個動態連結庫。經常遇到的一個問題是 -fPIC 是不是必需,因為好像不加經常也能正常執行,只是建立 .so 的時候會有一個警告。


搜尋、試驗了一下,答案似乎是這樣:

(1) 通常的建議是始終加上 -fPIC 生成位置無關程式碼;

(2) AMD64 下(ubuntu amd64),必須使用位置無關程式碼,否則連線失敗:

relocation R_X86_64_32S against `a local symbol’ can not be used when making a shared object; recompile with -fPIC



(3) IA32 下,連線成功,但有警告:

warning: creating a DT_TEXTREL in object.

這樣的 .so 檔案可以完全正常工作。


可執行檔案在連結時就知道每一行程式碼、每一個變數會被放到線性地址空間的什麼位置,因此這些地址可以都作為常數寫到程式碼裡面。對動態庫,這就不行了,這要等到載入時才知道。無非下面兩種方法:

(1) 可重定位程式碼(relocatable code):Windows DLL 以及不使用 -fPIC 的 Linux SO。

生成動態庫時假定它被載入在地址 0 處。載入時它會被載入到一個地址(base),這時要進行一次重定位(relocation),把程式碼、資料段中所有的地址加上這個 base 的值。這樣程式碼執行時就能使用正確的地址了。


(2) 位置無關程式碼(
position independent code
):使用 -fPIC 的 Linux SO。

這樣的程式碼本身就能被放到線性地址空間的任意位置,無需修改就能正確執行。通常的方法是獲取指令指標(如 IA32 的 EIP 暫存器)的值,加上一個偏移得到全域性變數/函式的地址。


PIC vs. relocatable:

(1) PIC 的缺點主要就是程式碼有可能長一些。例如 IA32,由於不能直接使用 [EIP+constant] 這樣的定址方式,甚至不能直接將 EIP 的值交給其他暫存器,要用到 GOT(global offset table)來定位全域性變數和函式。這樣導致程式碼的效率略低。

(2) PIC 的載入速度稍快,因為不需要做重定位。

(3) 多個程序引用同一個 PIC 動態庫時,可以共用記憶體。這一個庫在不同程序中的虛擬地址不同,但作業系統顯然會把它們對映到同一塊實體記憶體上。對於可重定位程式碼,則必須為每個庫都在實體記憶體中複製一份副本,因為需要修改其中的地址。當然,主流現代作業系統都啟用了
分頁記憶體機制
,這使得重定位時可以使用 COW(copy
on write)來節省記憶體(32 位 Windows 就是這樣做的);然而,頁面的粒度還是比較大的(例如 IA32 上是 4KiB),至少對於程式碼段來說能節省的相當有限。


注:對於 AMD64,由於 AMD64 實現了 [RIP+constant] 的定址方式,第 (1) 點不成立。

這樣,把動態庫編譯成 PIC 只有好處沒有壞處,因而 Linux AMD64 要求用於生成動態庫的目標檔案必須使用 -fPIC 編譯也合情合理了。