一些與編譯,鏈接相關的問題(-fPIC)

一些與編譯,鏈接相關的問題(zz)
 地址無關代碼,在64位下編譯動態庫的時候,經常會遇到下面的錯誤 


/usr/bin/ld: /tmp/ccQ1dkqh.o: relocation R_X86_64_32 against `a local symbol' can not be used when making a shared object; recompile with -fPIC


提示說需要-fPIC編譯,然後在鏈接動態庫的地方加上-fPIC的參數編譯結果還是報錯,需要把共享庫所用到的所有靜態庫都採用-fPIC編譯一邊纔可以成功的在64位環境下編譯出動態庫。


這裏的-fPIC指的是地址無關代碼


這裏首先先說明一下裝載時重定位的問題,一個程序如果沒有用到任何動態庫,那麼由於已經知道了所有的代碼,
那麼裝載器在把程序載入內存的過程中就可以直接安裝靜態庫在鏈接的時候定好的代碼段位置直接加載進內存中的對應位置就可以了。
但是在面對動態的庫的時候 ,這種方式就不行了。假設需要載入共享庫A,但是在編譯鏈接的時候使用的共享庫和最後運行的不一定是同一個庫,
在編譯期就沒辦法知道具體的庫長度,在鏈接的時候就沒辦法確定它或者其他動態庫的具體位置。另一個方面動態庫中也會用到一些全局的符號,
這些符號可能是來自其他的動態庫,這在編譯器是沒辦法假設的(如果可以假設那就全是靜態庫了)


基於上面的原因,就要求在載入動態庫的時候對於使用到的符號地址實現重定位。在實現上在編譯鏈接的時候不做重定位操作,地址都採用相對地址,
一但到了需要載入的時候,根據相對地址的偏移計算出最後的絕對地址載入內存中。


但是這種採用裝載時重定位的方式存在一個問題就是相同的庫代碼(不包括數據部分)不能在多個進程間共享(每個代碼都放到了它自己的進程空間中),
這個失去了動態庫節省內存的優勢。


爲了解決這個問題,ELF中的做法是在數據段中建立一個指向那些需要被使用(內部的位置無關簡單採用相對地址訪問就可以實現)的地址列表(也被稱爲全局偏移表,Global offset table, GOT). 可以通過GOT相對應的位置進行間接引用.


對於我們的32位環境來說, 編譯時是否加上-fPIC, 都不會對鏈接產生影響, 只是一份代碼的在內存中有幾個副本的問題(而且對於靜態庫而言結果都是一樣的).但在64位的環境下裝載時重定位的方式存在一個問題就是在我們的64位環境下用來進行位置偏移定位的cpu指令只支持32位的偏移, 但實際中位置的偏移是完全可能超過64位的,所以在這種情況下編譯器要求用戶必須採用fPIC的方式進行編譯的程序纔可以在共享庫中使用


從理論上來說-fPIC由於多一次內存取址的調用,在性能上會有所損失.不過從目前的一些測試中還無法明顯的看出加上-fPIC後對庫的性能有多大的損失,這個可能和我們現在使用的機器緩存以及大量寄存器的存在相關.




小提示:


-fPIC與-fpic 上面的介紹可以看到,gcc要使用地址無關代碼加上-fPIC即可,但是在gcc的手冊中我們可以看到一個-fpic(區別在一個大寫一個小寫)的參數,從功能上來說它們都是一樣的。-fpic在一些特定的環境中(包括硬件環境)可以有針對性的進行優化,產生更小更快的代碼, 但是由於受到平臺的限制,像我們的編譯環境,開發環境,運行環境都不完全統一的情況下面使用fpic有一定未知的風險,所有決大多數情況下我們使用 -fPIC來產生地址無關代碼。
共享內存效率


共享內存在只讀的情況下性能和讀普通內存是一樣的(如果不算第一載入的消耗),而且由於是多個進程共享對cpu cache還顯的相對友好。


同時存在靜態庫和動態庫


前面提到編譯動態庫的時候有提到編譯動態庫可以像編譯靜態庫那樣採用-Lpath -lxx的方式進行, 但這裏存在一個問題,如果在path目錄下既有動態庫又有靜態庫的時候的行爲又是什麼樣地? 事實上在這種情下, 鏈接器優先選擇採用動態庫的方式進行編譯.比如在同一目錄下存在 libx.a 和 libx.so, 那麼在鏈接的時候會優先選擇libx.so進行鏈接. 這也是爲什麼在com組維護的第三方庫(third, third-64)中絕大多數庫的產出物中只有.a的存在, 主要就是爲了避免在默認情況下使用到.so的庫, 導致在上線的時候出現麻煩(特別是一些系統中存在,但又與我們需要使用的版本有出入的庫).


爲了能夠控制動態庫和靜態庫的編譯, 有下面的幾種方式


直接使用要編譯的庫
在前面也提到了在編譯靜態庫的時候有三種方式 
目標文件.o 直接使用
靜態庫文件.a 直接編譯
採用 -L -l方式進行編譯


編譯的時候如果不採用-Lpath -lxx的方式進行編譯, 而且直接寫上 path/libx.a 或者 path/libx.so 進行編譯,那麼在鏈接的時候就是使用我們指定的 .a 或者 .so進行編譯不會出現 所謂的動態庫優先還是靜態庫優先的問題. 但這個方案需要知道編譯庫的路徑,一些情況下並不適合使用。


--static參數


在gcc的編譯的時候加上--static參數, 這樣在編譯的時候就會優先選擇靜態庫進行編譯,而不是按照默認的情況選擇動態庫進行編譯.


不過使用--static參數會帶來另外的問題,不推薦使用,主要會帶來下面的問題


如果只有動態庫,而不存在同名的靜態庫,鏈接的時候也不會報錯,但在運行的時候可能會出現錯誤 /lib/ld64.so.1: bad ELF interpreter:
由於我們程序本身在運行的需要系統中一些庫的支持,在採用--static編譯方式之後,鏈接的就是這些庫的靜態編譯版本,等於使用的是編譯機上的庫,但是我們的運行環境可能和編譯機有所不同,glibc這些動態庫的存在本身的目的就是爲了能讓在一臺機器上編譯好的庫能夠比較方便的移到另外的機器上,程序本身只需要關注接口,至於從接口到底層的部分由每臺機器上的.so來處理.不過這個問題也不是那麼絕對,在一些特殊情況下(比如 glibc, gcc存在大版本差異的時候,主要是gcc2到gcc3有些地方沒有做好,abi不兼容的問題比較突出,真遇到這些情況其實需要換編譯器了) --static編譯反倒可以正常的運行.但是還是不推薦使用, 這些是可以採用其它方法規範在後面的第6點中有說明.另外就是glibc --static編譯可能會產生下面的warning:
warning: Using 'getservbyport_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking這個主要原因是由於getservbyport_r 這樣的接口還是需要動態庫的支持纔可以運行,許多glibc的函數都存在這樣的問題, 特別是網絡編程的接口中是很常見的.
對一些第三方工具不友好,類似valgrind檢查內存泄露爲了不在一些特殊的情況下誤報, 它需要用動態庫的方式替換glibc中的函數,如果靜態編譯那麼valgrind就無法替換這些函數,產生誤報甚至無法報錯. tcmalloc在這種情況下也不能支持.
64位環境中使用的pthread庫,如果是使用的是動態庫那麼採用的是ntpl庫,如果是靜態庫採用的linuxthread庫,使用--static 會導致性能下降
--static之後會導致代碼大小變大,對cpu代碼cache不友好,浪費內存空間,不過對於小代碼問題也不大.


鏈接參數控制
鏈接器中提供了-dn -dy 參數來控制使用的是動態庫還是靜態庫,-dn表示後面使用的是靜態庫,-dy表示使用的是動態庫 


例:


g++ -Lpath -Wl,-dn -lx -Wl,-dy -lpthread


這樣如果在path路徑下有libx.so和libx.a這個時候只會用到 libx.a.


注意在最後的地方如果沒有-Wl,-dy 讓後面的庫都使用動態庫,可能會報出 "cannot find -lgcc_s" 的錯誤,這是由於glibc的.a庫和.so庫名字不同,--static會自動處理,但是 -Wl,-dy卻不會去識別這個問題.


小提示:


如果使用--static, 由於-dy的使用導致後面的庫都是共享庫(dy強制屏蔽了靜態庫),這個時候編譯出來的程序和只有動態庫的情況下強制使用--static編譯一樣都會報錯




運行報錯 "undefined reference to `xxx()' "


對於動態鏈接庫,實際的符號定位是在運行期進行的.在編譯.so的時候,如果沒有把它需要的庫和他一起進行聯編,比如libx.so 需要使用uldict, 但是忘記在編譯libx.so的時候加上-luldict的話,在編譯libx.so的時候不會報錯,因爲這個時候libx.so被認爲是一個庫,它裏面存在一些不知道具體實現的符號是合法的,是可以在運行期指定或者編譯另外的二進制程序的時候指定.


如果是採用 g++ -Lpath -lx 的方式進行編譯,鏈接器會發現所需要的uldict的符號表找不到從而報錯,但是如果是程序採用dlopen的方式載入,由於是運行期,這個程序在這個地方就直接運行報錯了.另外還有一種情況就是一個對外的接口在動態庫中已經聲明定義了,但是忘記實現了,這個時候也會產生類似的錯誤.


如果在運行期報出這樣的錯誤,就要注意是否是由於某些庫沒有鏈接進來或者某些接口沒有實現的原因產生


=================================================


收集了一些與編譯,鏈接相關的問題, 有問題隨時歡迎提問.


純C程序如何使用ullib這些用g++編譯出來的庫


上文已經介紹過了, 在g++的環境中直接編譯的結果會導致符合表與gcc編譯的結果不同導致不能混合編譯.


gcc使用g++編譯的庫原則:


1. g++編譯庫的時候需要把被外界使用的接口按照純C++可以接受的方式用extern "C" 包起來,並且加上__cplusplus宏的判斷,可以參考public/mcpack, public/nshead中的寫法. 對於一些特殊情況,比如已經是g++編譯出來的庫又不適合修改,比如ullib, 分詞庫等,可以自己寫一個 xxx.cpp的程序,在xxx.cpp對需要使用的接口再做一次純C接口的封裝,同時用extern "C"把純C接口導出使用.使用g++編譯,並且在鏈接的時候加上ullib等庫即可. 2. gcc編譯g++庫在我們的64位環境中需要在最後加上-lstdc++


gcc使用g++編譯的庫多見於需要將基礎庫與php擴展,apache mod進行聯編;


g++使用gcc編譯出來的庫: 這個比較簡單,只需要gcc編譯的提供的頭文件採用了extern "C"封裝即可.


在同樣的環境下用同樣的方式編譯出來的程序md5是否都一樣


如果環境完全一樣包括編譯路徑,環境變量等都是一樣的,一般情況下確實是一樣的,但是許多環境的情況我們很難做到一樣,比如程序使用一些DATA這樣與時間相關宏就會導致每次編譯的結果都是不一樣的,有時候甚至內存的多少也會影響編譯的結果


鏈接和運行的時候,靜態庫和動態庫路徑的查找順序都是什麼?


鏈接的時候查找順序:


-L 指定的路徑, 從左到右依次查找
由 環境變量 LIBRARY_PATH 指定的路徑,使用":"分割從左到右依次查找
/etc/ld.so.conf 指定的路徑順序
/lib 和 /usr/lib (64位下是/lib64和/usr/lib64)


動態庫調用的查找順序:


ld的-rpath參數指定的路徑, 這是寫死在代碼中的
ld腳本指定的路徑
LD_LIBRARY_PATH 指定的路徑
/etc/ld.so.conf 指定的路徑
/lib和/usr/lib(64位下是/lib64和/usr/lib64)


一般情況鏈接的時候我們採用-L的方式指定查找路徑, 調用動態鏈接庫的時候採用LD_LIBRARY_PATH的方式指定鏈接路徑.


另外注意一個問題,就是隻要查找到第一個就會返回,後面的不會再查找. 比如-L./A -L./B -lx 在A中有libx.a B中有libx.a和libx.so, 這個時候會使用在./A的libx.a 而不會遵循動態庫優先的原則,因爲./A是先找到的,並且沒有同名動態庫存在.
哪些情況會出現 "undefined reference error" 的錯誤?


這裏再總結一下這個問題可能出現的場景:


沒有指定對應的庫(.o/.a/.so) 使用了庫中定義的實體,但沒有指定庫(-lXXX)或者沒有指定庫路徑(-LYYY),會導致該錯誤,
連接庫參數的順序不對 在默認情況下,對於-l 使用庫的要求是越是基礎的庫越要寫在後面,無論是靜態還動態
gcc/ld版本不匹配 gcc/ld的版本的兼容性問題,由於gcc2 到 gcc3大版本的兼容性存在問題(其實gcc3.2到3.4也一定程度上存在這樣的問題) 當在高版本機器上使用低版本的機器就會導致這樣的錯誤, 這個問題比較常見在32位的環境上, 另外就在32位環境不小心使用了64位的庫或者反過來64位環境使用了32位的庫.
C/C++相互依賴和鏈接 gcc和g++編譯結果的混用需要保證能夠extern "C" 兩邊都可以使用的接口,在我們的64位環境中gcc鏈接g++的庫還需要加上 -lstdc++,具體見前文對於混合編譯的說明
運行期報錯 這個問題基本上是由於程序使用了dlopen方式載入.so, 但.so沒有把所有需要的庫都鏈接上,具體參加上文中對於靜態庫和動態庫混合使用的說明




可以把兩個.o直接合併成一個.o文件嗎?


可以,命令是 ld -r a.o b.o -o x.o, 不過不推薦這樣做,這樣做唯一的好處是靜態庫在鏈接的時候如果使用到了a.o中的符號也可以同時把b.o中的符號鏈接進來,可以避免--whole-archive的應用.


但是不推薦這樣做,無形中增加了對源文件維護的麻煩


爲什麼使用inline,並沒有把代碼inline進程序?


首先加了inline的函數是否可以被inline這個是由編譯器決定,很多時候即時是指定了inline但還是無法被inline


另外注意到gcc中,只有在使用-O以上的優化後inline纔會起作用,沒有-O, -O2, -O3這些優化手段,無論是否加上了-finline-functions gcc都是不會進行inline優化的,這個時候的inline相當於一個普通函數(其實還是有一點區別,在符號表中表示是不一樣的).程序在編譯的時候加上了-finline-functions 但如果沒有-OX(X>=1)的配合, -finline-functions其實是無效的,不會起作用也不會報錯


gcc裏面爲了能夠支持在不加-OX(X>=1)的情況下能夠將函數inline, 提供了一個擴展always_inline, 將函數寫成下面這樣


__attribute__((always_inline)) int foo()
{
...
}


就可以在不加-OX(X>=1)的情況下把foo inline進程序,不過always_inline 這個擴展只在gcc3以後支持,32位環境中使用的2.96 gcc是不支持的.


64位機器上可以編譯出32位程序嗎?


理論上是可以的, 在64位機器上的64位gcc中提供了-m32的參數,可以指定進行32位的編譯, 但是編譯問題雖然解決鏈接問題卻還是存在,在64位的機器上可以用進行鏈接的庫主要有2個一個是供64位程序使用的,另外一個供gcc2.96編譯程序在64位機器上運行的,這兩個庫都不能給gcc -m32出來的結果提供鏈接環境(32位庫不能連接64位庫,給gcc2.96的庫太老的不兼容), 所以在編譯機器環境上是不能直接編譯出可執行的32位程序(編譯成.o文件還是可以的)


爲什麼編寫的動態鏈接庫不能直接運行?


在共享庫的總結中介紹瞭如何實現共享庫可以自己運行,但是有些時候會出現undefined reference error的錯誤導致共享庫不能被運行。


這種情況產生的原因是:動態庫中採用了類似 static int val = func(xxx);的寫法, 其中val 是一個全局變量(或者靜態全局變量)。 動態庫被載入內存中使用的時候會直接先運行func這個函數,如果func是來自其他的庫(比如一些情況下主程序使用-rdynamic編譯,動態庫使用主程序的空間), 在編譯動態鏈接的庫的時候又沒有被鏈接上, 這個時候就會出現這樣的問題。


對於這樣的問題主要考慮下面的解決方案:


1. 不要採用static int val = func(xxx);這種寫法
將使用的靜態庫鏈接進共享庫, 但這裏要注意-rdynamic的影響,必要的時候需要保證和主程序使用的庫版本是相同的。
讓共享庫不可運行也是一種解決方案






是否可以在main函數開始前就執行程序?


如果在main函數開始前執行代碼,一般有下面的兩種方法


採用 int val = func(xxx)的方式,在func(xxx)中執行
聲明一個class, 把需要運行的函數寫在class. 並且定義一個全局(或者static)的類變量


在實現上,編譯器把它們放到一個特殊的符號 _init 中,在程序被載入內存的時候被執行


但是這種方式我們不推薦使用,特別是在這些執行代碼中存在庫與庫之間的依賴關係的時候, 比如下面的場景:


libA.cpp
class Aclass
{
public:
Aclass()
{
int * u = Bfunc(); //這是另外一個庫libB中的函數
int c = u[0];
}


}


static Aclass s_test;


libB.cpp


static int *s_test = test_init(); //初始化s_test


int *Bfunc()
{
return s_test;
}


上面的程序中有2個庫,A庫有一個static變量的構造函數依賴了 B庫中的一個函數, B庫中的這個函數又操作了一個由函數test_init初始化的static變量.


按照程序的要求我們必須要讓test_init()這個函數在Aclass這個函數之前運行, 但是可惜的在某些情況我們很難做到這點, 這裏涉及到鏈接器對庫鏈接和初始化順序的問題.


在默認情況下, test_init()和s_test的構造函數的執行順序是按照鏈接的時候-l的順序從右到左, 比如-lB -lA 那麼Aclass的構造函數會在test_init()前執行,這個時候就會出現問題,需要保證-lA -lB的順序纔可以正常.


這裏又涉及到另外一個問題, 就是 正常情況既然A依賴B, 那麼在鏈接的時候肯定需要 保證 -lA在-lB. 但是這裏我們只能說需要把越基礎的庫放在越後面,而不是必需放在最後面.還是上面的例子. 如果這個時候有一個test.cpp 使用了 A庫, 並且在test中沒有直接使用到B庫中的東西, 這個時候如果-lB放在-lA前面,鏈接器會報錯, 因爲符號在從左往右展開的時候, 由於test沒有使用到B的東西,所以沒有做任何展開, 從這個角度而言在鏈接A的時候就找不到符號. 但是如果在test中有使用到B中和test_init相關聯的函數,那麼這個時候如果把-lB放在-lA的前面展開B函數的時候會把test_init導出, 這樣導致A會認爲已經存在了test_init, 從而不報編譯錯誤. 但是這樣的結果就是test_init的初始化順序被放到Aclass之後, 那麼在程序運行的時候就可能導致錯誤.


對這種問題解決,主要有幾種考慮


採用 單例模式, 採用類似 if (NULL == ptr) ptr = new xxx; return ptr的方式通過用戶態的判斷來控制,不過有些時候需要考慮些多線程問題,
瞭解依賴關係, 把-lB放到-lA的後面
不允許這種方式的存在.


在使用全局變量的時候 需要特別注意這種初始化的順序問題.小提示:構造初始化等,是在_init中處理, 另一個方面_fini是存在在程序退出前的執行析構等操作
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章