生成動態鏈接庫是否必須使用 -fPIC 的問題

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

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

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

(2) 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 編譯也合情合理了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章