Linux環(huán)境下的編譯-鏈接與庫的使用.doc_第1頁
Linux環(huán)境下的編譯-鏈接與庫的使用.doc_第2頁
Linux環(huán)境下的編譯-鏈接與庫的使用.doc_第3頁
Linux環(huán)境下的編譯-鏈接與庫的使用.doc_第4頁
Linux環(huán)境下的編譯-鏈接與庫的使用.doc_第5頁
已閱讀5頁,還剩22頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認領(lǐng)

文檔簡介

Linux環(huán)境下的編譯,鏈接與庫的使用From: /為什么使用ullib有時會出現(xiàn) undefined reference error 的錯誤?為什么在動態(tài)鏈接庫里ul_log會把日志輸出到屏幕上?為什么用-static 編譯有時候會報warning?我們在使用基礎(chǔ)庫或者第三方庫的時候,經(jīng)常遇到這樣那樣的問題,本文結(jié)合公司目前的主要環(huán)境,說明庫的原理,使用的注意事項。從程序到可執(zhí)行文件從hello world 說起include int main() printf(“hello worldn”); return 0; 上面這一段程序任何一個學(xué)過C語言的同學(xué)都是閉著眼睛都可以寫出來,但是對于將這樣一個源代碼編譯成為一個可執(zhí)行文件的過程卻不一定有所了解。 上面的程序如果要編譯,很簡單gcc hello.c 然后./a.out就可以運行,但是在這個簡單的命令后面隱藏了許多復(fù)雜的過程一般來說,可以把這樣的過程分成4個: 預(yù)編譯, 編譯, 匯編和鏈接。預(yù)編譯:這個過程包括了下面的步驟宏定義展開,所有的#define 在這個階段都會被展開 預(yù)編譯命令的處理,包括#if #ifdef 一類的命令 展開#include 的文件,像上面hello world 中的stdio.h , 把stdio.h中的所有代碼合并到hello.c中 去掉注釋 gcc的預(yù)編譯 采用的是預(yù)編譯器cpp, 我們可以通過-E參數(shù)來看預(yù)編譯的結(jié)果,如: gcc -E hello.c -o hello.i 生 成的 hello.i 就是經(jīng)過了預(yù)編譯的結(jié)果 在預(yù)編譯的過程中不會太多的檢查與預(yù)編譯無關(guān)的語法(#ifdef 之類的還是需要檢查, #include文件路徑需要檢查), 但是對于一些諸如 ; 漏掉的語法錯誤,在這個階段都是看不出來的。 寫過makefile的人都知道, 我們需要加上-Ipath 一系列的參數(shù)來標(biāo)示gcc對頭文件的查找路徑小提示:1.在一些程序中由于宏的原因?qū)е戮幾g錯誤,可以通過-E把宏展開再檢查錯誤 , 這個在編寫 PHP擴展, python擴展這些大量需要使用宏的地方對于查錯誤很有幫助。2.如果在頭文件中,#include 的時候帶上路徑在這個階段有時候是可以省不少事情, 比如 #include , 這樣在gcc的-I參數(shù)只需要指定一個路徑,不會由于不小心導(dǎo)致,文件名正好相同出現(xiàn)沖突的麻煩事情. 不過公司由于早期出現(xiàn)了lib2和lib2-64兩個目錄, 以及頭文件輸出在include 目錄下, 靜態(tài)發(fā)布等一些歷史原因, 有些時候使用帶完整路徑名的方式不是那么合適( 比如 #include 中間有一個include 顯的很別扭). 不過個人認為所有的#include 都應(yīng)該是盡量采用從cvs 根路徑下開始寫完整路徑名的方式進行預(yù)編譯的過程,只是受限于公司原有習(xí)慣和歷史問題而顯的不合適, 當(dāng)然帶路徑的方式要多寫一些代碼,也是麻煩的事情, 路徑由外部指定相對也會靈活一些.編譯:這個過程才是進行語法分析和詞法分析的地方, 他們將我們的C/C+代碼翻譯成為 匯編代碼, 這也是一個編譯器最復(fù)雜的地方使用命令gcc -S hello.i -o hello.s 可 以看到gcc編譯出來的匯編代碼, 現(xiàn)代gcc編譯器一般是把預(yù)編譯和編譯合在一起,使用cc1 的程序來完成這個過程,在我們的開發(fā)機上有些時候一些同學(xué)編譯大文件的時候可以用top命令看一個cc1的進程一直在占用時間,這個時候就是程序在執(zhí)行編 譯過程. 后面提到的編譯過程都是指 cc1的處理包括了預(yù)編譯與編譯.匯編: 現(xiàn)在C/C+代碼已經(jīng)成為匯編代碼了,直接使用匯編代碼的編譯器把匯編變成機器碼(注意還不是可執(zhí)行的) .gcc -c hello.c -o hello.o 這里的hello.o就是最后的機器碼, 如果作為一個靜態(tài)庫到這里可以所已經(jīng)完成了,不需要后面的過程.對于靜態(tài)庫, 比如ullib, COM提供的是libullib.a, 這里的.a文件其實是多個.o 通過ar命令打包起來的, 僅僅是為了方便使用,拋開.a 直接使用.o 也是一樣的小提示:1. gcc 采用as 進行匯編的處理過程,as 由于接收的是gcc生成的標(biāo)準(zhǔn)匯編, 在語法檢查上存在不少缺陷,如果是我們自己寫的匯編代碼給as去處理,經(jīng)常會出現(xiàn)很多莫名奇妙的錯誤. 鏈接: 鏈接的過程,本質(zhì)上來說是一個把所有的機器碼文件組合成一個可執(zhí)行的文件 上面匯編的結(jié)果得到一個.o文件, 但是這個.o要生成二執(zhí)行文件只靠它自己是不行的, 它還需要一堆輔助的機器碼,幫它處理與系統(tǒng)底層打交道的事情.gcc -o hello hello.o 這樣就把一個.o文件鏈接成為了一個二進制可執(zhí)行文件. 我們提供的各種庫頭文件在編譯期使用,到了鏈接期就需要用-l, -L的方式來指定我們到底需要哪些庫。 對于glibc中的strlen之類常用的東西編譯器會幫助你去加上可以不需要手動指定。這個地方也是本文討論的重點, 在后面會有更詳細的說明小提示:有些程序在編譯的時候會出現(xiàn) “l(fā)inker input file unused because linking not done” 的提示(雖然gcc不認為是錯誤,這個提示還是會出現(xiàn)的), 這里就是把 編譯和鏈接 使用的參數(shù)搞混了,比如g+ -c test.cpp -I././ullib/include -L././ullib/lib/ -lullib 這樣的寫法就會導(dǎo)致上面的提示, 因為在編譯的過程中是不需要鏈接的, 它們兩個過程其實是獨立的靜態(tài)鏈接鏈接的過程 這里先介紹一下,鏈接器所做的工作,其實鏈接做的工作分兩塊: 符號解析和重定位符號解析符號包括了我們的程序中的被定義和引用的函數(shù)和變量信息在命令行上使用 nm ./testtest 是用戶的二進制程序,包括可以把在二進制目標(biāo)文件中符號表輸出00000000005009b8 A bss_start00000000004004cc t call_gmon_start00000000005009b8 b completed.10000000000500788 d CTOR_END0000000000500780 d CTOR_LIST00000000005009a0 D data_start00000000005009a0 W data_start0000000000400630 t do_global_ctors_aux00000000004004f0 t do_global_dtors_aux00000000005009a8 D dso_handle0000000000500798 d DTOR_END0000000000500790 d DTOR_LIST00000000005007a8 D DYNAMIC00000000005009b8 A edata00000000005009c0 A end0000000000400668 T fini0000000000500780 A fini_array_end0000000000500780 A fini_array_start0000000000400530 t frame_dummy0000000000400778 r FRAME_END0000000000500970 D GLOBAL_OFFSET_TABLE w gmon_start U gxx_personality_v0CXXABI_1.30000000000400448 T _init0000000000500780 A _init_array_end 當(dāng)然上面由nm輸出的符號表可以通過編譯命令去除,讓人不能直接看到。鏈接器解析符號引用的方式是將每一個引用的符號與其它的目標(biāo)文件(.o)的符號表中一個符號的定義聯(lián)系起來,對于那些和引用定義在相同模塊的本地符號(注:static修飾的),編譯器在編譯期就可以發(fā)現(xiàn)問題,但是對于那些全局的符號引用就比較麻煩了下面來看一個最簡單程序:includeint foo();int main() foo(); return 0; 我們把文件命名為test.cpp, 采用下面的方式進行編譯 g+ -c test.cppg+ -o test test.o 第一步正常結(jié)束,并且生成了test.o文件,到第二步的時候報了如下的錯誤test.o(.text+0x5): In function main: undefined reference tofoo()collect2: ld returned 1 exit status 由于foo 是全局符號, 在編譯的時候不會報錯,等到鏈接的時候,發(fā)現(xiàn)沒有找到對應(yīng)的符號,就會報出上面的錯誤。但是如果我們把上面的寫法改成下面這樣include/注意這里的static static int foo();int main() foo(); return 0; 在運行 g+ -c test.cpp, 馬上就報出下面的錯誤:test.cpp:19: error: int foo() used but never defined 在編譯器就發(fā)現(xiàn)foo 無法生成目標(biāo)文件的符號表,可以馬上報錯,對于一些本地使用的函數(shù)使用static一方面可以避免符號污染,另一方面也可以讓編譯器盡快的發(fā)現(xiàn)錯誤在我們的基礎(chǔ)庫中提供的都是一系列的.a文件,這些.a文件其實是一批的目標(biāo)文件(.o)的打包結(jié)果這樣的目的是可以方便的使用已有代碼生成的結(jié)果,一般情況下是一個.c/.cpp文件生成一個.o文件,在編譯的時候如果帶上一堆的.o文件顯的很不方便,像:g+ -o main main.cpp a.o b.o c.o 這樣大量的使用.o也很容易出錯,在linux下使用archive來將這些.o存檔和打包所以我們就可以把編譯參數(shù)寫成g+ -o main main.cpp ./libullib.a 我們可以使用./libullib.a 直接使用libullib.a這個庫,不過gcc提供了另外的方式來使用:g+ -o main main.cpp -L./ -lullib -L指定需要查找的庫文件的路徑, -l 選擇需要使用的庫名字,不過庫的名字需要用lib+name的方式命名,才會被gcc認出來 不過上面的這種方式存在一個問題就是不區(qū)分動態(tài)庫和靜態(tài)庫,這個問題在后面介紹動態(tài)庫的時候還會提到當(dāng)存在多個.a ,并且在庫之間也存在依賴關(guān)系,這個時候情況就比較復(fù)雜如果我們要使用lib2-64/dict, dict又依賴ullib, 這個時候需要寫成類似下面的形式g+ -o main main.cpp -L./lib2-64/dict/lib -L./lib2-64/ullib/lib -ldict -lullib -lullib 需要寫在-ldict的后面,這是由于在默認情況對于符號表的解析和查找工作是由后往前(內(nèi)部實現(xiàn)是一個類似堆棧的尾遞歸) 所以當(dāng)所使用的庫本身存在依賴關(guān)系的時候,越是基礎(chǔ)的庫就越是需要放到后面否則如果上面把-ldict -lulib的位置換一下,可能就會出現(xiàn) undefined reference toxxx 的錯誤 一般來說對于基礎(chǔ)庫的依賴關(guān)系可以在平臺上獲取, 若存在一些第三方的依賴,就只有參考相關(guān)的幫助說明了當(dāng)然gcc提供了另外的方式的來解決這個問題g+ -o main main.cpp -L./lib2-64/dict/lib -L./lib2-64/ullib/lib-Xlinker “-(” -ldict -lullib-Xlinker “-)” 可以看到我們需要的庫被-Xlinker “-(“和-Xlinker “-)” 包含起來,gcc在這里處理的時候會循環(huán)自動查找依賴關(guān)系,不過這樣的代價就是延長gcc的編譯時間,如果使用的庫非常的多時候,對編譯的耗時影響還是非常大.-Xlinker有時候也簡寫成”-Wl, “,它的意思是 它后面的參數(shù)是給鏈接器使用的-Xlinker 和 -Wl 的區(qū)別是一個后面跟的參數(shù)是用空格,另一個是用”,”我們通過nm命令查看目標(biāo)文件,可以看到類似下面的結(jié)果/lib2-64/dict/lib/x.html 1 0000000000009740 T Z11ds_syn_loadPcS 2 0000000000009c62 T Z11ds_syn_seekP16Sdict_search_synPcS1_i 3 0000000000007928 T Z11dsur_searchPcS_S 4 &nbs p; U Z11ul_readfilePcS_Pvi 5 &nbs p; U Z11ul_writelogiPKcz 6 00000000000000a2 T Z12creat_sign32Pc其中用U標(biāo)示的符號_Z11ul_readfilePcS_Pvi(其實是ullib中的 ul_readfile) ,表示在dict的目標(biāo)文件中沒有找到ul_readfile函數(shù)在鏈接的時候,鏈接器就會去其他的目標(biāo)文件中查找_Z11ul_readfilePcS_Pvi的符號小提示:編譯的時候采用-Lxxx -lyyy的形式使用庫,-L和-l這個參數(shù)并沒有配對的關(guān)系,我們的一些Makefile 為了維護方便把他們寫成配對的形式,給一些同學(xué)造成了誤解 其實我們完全可以寫成-Lpath1, -Lpath2, -Lpath3, -llib1 這樣的形式在具體鏈接的時候,gcc是以.o文件為單位, 編譯的時候如果寫g+ -o main main.cpp libx.o 那么無論main.cpp中是否使用到libx.o,libx.o中的所有符號都會被載入到main函數(shù)中但是如果是針對.a,寫成g+ -o main main.cpp -L./ -lx, 這個時候gcc在鏈接的時候只會鏈接有被用到.o, 如果出現(xiàn)libx.a中的某個.o文件中沒有任何一個符號被main用到,那么這個.o就不會被鏈接到main中g(shù)cc編譯.c文件的時候和g+ 有一個不一樣的地方, 就是在g+ 中對于一個函數(shù)必須要先定在再使用,比如上面的例子中需要先定義foo()才能被使用,但對于gcc編譯的.c(如果是.cpp會自動換成C+編譯) 文件, 可以不需要先定義, 而直接使用. 但這樣會出現(xiàn)問題, 如果沒有其他地方使用和這個函數(shù)同名的函數(shù)那么鏈接的時候會找不到這個函數(shù). 但是如果碰巧在另外的地方存在一個同名函數(shù),那么鏈接的時候就會被直接連接到這個函數(shù)上, 萬一使用的時候偏偏傳入?yún)?shù)或返回值的類型不對,那么這個時候就可能出現(xiàn)莫名奇妙的錯誤. 不過我們還是可以用-Wmissing-declarations參數(shù)打開這個檢查重定位經(jīng)過上面的符號解析后,所有的符號都可以找到它所對應(yīng)的實際位置(U表示的鏈接找到具體的符號位置)as 匯編生成一個目標(biāo)模塊的時候,它不知道數(shù)據(jù)和代碼在最后具體的位置,同時也不知道任何外部定義的符號的具體位置,所以as在生成目標(biāo)代碼的時候,對于位置未知的符號,它會生成一個重定位表目,告訴鏈接器在將目標(biāo)文件合并成可執(zhí)行文件時候如何修改地址成最終的位置g+和gcc 采用gcc 和g+ 在編譯的時候產(chǎn)生的符號有所不同在C+中由于要支持函數(shù)重載,命名空間等特性,g+會把函數(shù)參數(shù)(可能還有命名空間),把函數(shù)命變成一個特殊并且唯一的符號名例如:int foo(int a); 在gcc編譯后,在符號表中的名字就是函數(shù)名foo, 但是在g+編譯后名字可能就變成了_Z3fooi, 我們可以使用c+filt命令把一個符號還原成它原本的樣子,比如c+filt _Z3fooi 運行的結(jié)果可以得到foo(int)由于在C+和純C環(huán)境中,符號表存在不兼容問題,程序不能直接調(diào)用C+編譯出來的庫,C+程序也不能直接調(diào)用C編譯出來的庫為了解決這個問題C+中引入了extern “C”的方式extern “C” int foo(int a); 這樣在用g+編譯的時候, c+的編譯器會自動把上面的 int foo(int a)當(dāng)做C的接口進行符號轉(zhuǎn)化這樣在純C里面就可以認出這些符號不過這里存在一個問題,extern “C” 是C+支持的,gcc并不認識,所有在實際中一般采用下面的方式使用c+#ifdef cplusplus extern “C” #endifint foo(int a); #ifdef cplusplus #endif這樣這個頭文件中的接口即可以給gcc使用也可以給g+使用, 當(dāng)然在extern “C” 中的接口是不支持重載,默認參數(shù)等特性在我們的64位編譯環(huán)境中如果有g(shù)cc的程序,使用上面方式g+編譯出來的庫,需要加上-lstdc+, 這是因為,對于我們位環(huán)境下g+編譯出來的庫,需要使用到一個gxx_personality_v0的符號,它所在的位置是/usr /lib64/libstdc+.so.6 (C+的標(biāo)準(zhǔn)庫iostream都在里面,C+程序都需要的). 但是在我們的32位2.96 g+編譯器中是不需要gxx_personality_v0,所有編譯可以不加上 -lstdc+小提示:在linux gcc 中,只有在源代碼使用.c做后綴,并且使用gcc編譯才會被編譯成純C的結(jié)果,其他情況像g+編譯.c文件,或者gcc 編譯.cc, .cpp文件都會被當(dāng)作C+程序編譯成C+的目標(biāo)文件, gcc和g+唯一的不同在于gcc不會主動鏈接-lstdc+ 在 extern “C” 中如果存在默認參數(shù)的接口,在g+編譯的時候不會出現(xiàn)問題,但是gcc使用的時候會報錯因為對于函數(shù)重載,接口的符號表還是和不用默認參數(shù)的時候是 一樣的 編譯器版本問題 目前公司內(nèi)部使用的gcc版本主要分兩種32位gcc 2.96 64位 gcc 3.4.4 (這是編譯機的版本號,我們的開發(fā)機多數(shù)是gcc 3.4.5, 小版本號的差異,目前看來不會對程序會帶來影響) 有時候在32位環(huán)境中經(jīng)常會出現(xiàn)undefined reference error”的錯誤,這個問題多數(shù)是由于 gcc 的版本問題造成的,我們許多的32位機器上的編譯器都是3.x的版本,gcc 從2到3做了很大的改動,c+的符號表的表現(xiàn)有所區(qū)別,導(dǎo)致gcc3的編譯器不能鏈接由gcc2.96編譯出來的庫我們的基礎(chǔ)庫在lib2下的都是采用靜態(tài)發(fā)布(直接發(fā)布最后的二進制庫,而不是在需要的時候重新編譯)不過在gcc3的glibc中考慮了向下兼容性使的可以正常運行由gcc 2.96上編譯出來的二進制程序 我們現(xiàn)在有一種方式是在gcc2.96環(huán)境下編譯出來的二進制程序放到64位機器上去運行,如果我們是一個新的 64位機器環(huán)境上運行程序,實際上這是無法運行的,我們的程序之所以可以這樣做,是由于在我們的位機器上裝上32位程序運行的環(huán)境,包括載入32位程 序的載入器,對應(yīng)的各種動態(tài)庫,可以在64位機器上/usr/lib/rh80目錄下看所使用各種動態(tài)庫,不過這些庫的版本與我們的開發(fā)機編譯機上版本有 所不同,有些時候我們會發(fā)現(xiàn)如果64位機器上的32位程序運行出core, 把core文件放到開發(fā)機上進行調(diào)試會看到出現(xiàn)在glibc的動態(tài)庫的函數(shù)都core在一些很奇怪的位置,根本不是我們程序中調(diào)用的位置,這里很重要的原因就在于動態(tài)庫的版本不一樣符號表沖突 我們在編譯程序的時候時常會遇到類似于multiple definition of foo() 的錯誤這些錯誤的產(chǎn)生都是由于所使用的.o文件中存在了相同的符號造成的比如:libx.cppint foo() return 30; liby.cppint foo() return 20; 將libx.cpp, liby.cpp編譯成libx.o和liby.o兩個文件g+ -o main main.cpp libx.o liby.o 這個時候就會報出multiple definition of foo()的錯誤但是如果把libx.o和liby.o分別打包成libx.a和liby.a用下面的方式編譯g+ -o main main.cpp -L./ -lx -ly 這個時候編譯不會報錯,它會選擇第一個出現(xiàn)的庫,上面的例子中會選擇libx中的foo. 但是注意不是所有的情況都是這樣的,由于鏈接是以.o為單位的,完全可以不用某個.o的時候才不會出錯誤,否則依然會出現(xiàn)multipe的錯誤, 這種情況下的建議是查看一下這些函數(shù)的行為是什么樣子,是否是一致的,如果不一致,還是想辦法規(guī)避, 如果是一致的話可以用 -Wl,allow-multiple-definition 強制編譯過去,這樣會使用第一個碰到的庫,但不推薦這樣做.可以通過g+ -o main main.cpp -L./ -lx -ly-Wl,trace-symbol=_Z3foov的命令查看符號具體是鏈接到哪個庫中,g+ -o main main.cpp -L./ -lx -ly-Wl,cref可以把所有的符號鏈接都輸出(無論是否最后被使用) 小提示:對于一些定義在頭文件中的全局常量,gcc和g+有不同的行為,g+中const也同時是static的,但gcc不是例如: foo.h 中存在一個constint INTVALUE = 2000; 的全局常量有兩個庫 a和b, 他們在生成的時候有使用到了INTVALUE,如果有一個程序main同時使用到了a庫和b庫,在鏈接的時候gcc編譯的結(jié)果就會報錯,但如果a和b都是g+編譯的話結(jié)果卻一切正常這個原因主要是在g+中會把INTVALUE 這種const常量當(dāng)做static的,這樣就是一個局部變量,不會導(dǎo)致沖突,但是如果是gcc編譯的話,這個地方INTVALUE會被認為是一個對外的全局常量是非static的,這個時候就會造成鏈接錯誤小提示 上說了對于a庫和b庫出現(xiàn)同樣符號的情況會有沖突, 但是在實際中有這么一種情況, a庫定義的foo的接口,在有b庫的情況下是一種行為,在沒有b庫的情況下又想要一種行為。為解決這個問題引入了弱連接的機制, 前面我們看到nm后,有些符號前面有T標(biāo)志,這個表示的是這個符號是一個強連接。 如果看有W的表示,那么就表示這個符號是弱連接。如果有一個同名的庫也有相同的符號并且是強連接,那么就可以替代掉他(如果也是弱連接,會存在先后順序用 誰的問題)。 glibc中的符號都是弱連接, 我們可以在我們的程序中編寫 open, write之類的函數(shù)去替換掉glibc中的實現(xiàn)。如果我們要自己寫弱連接的函數(shù)可以采用gcc擴展attribute(weak) const int func();來表示一個符號是弱連接/lib2-64/dict/lib/x.html 1 0000000000009740 T Z11ds_syn_loadPcS 2 0000000000009c62 T Z11ds_syn_seekP16Sdict_search_synPcS1_i 3 0000000000007928 T Z11dsur_searchPcS_S 4 &nbs p; U Z11ul_readfilePcS_Pvi 5 &nbs p; U Z11ul_writelogiPKcz 6 00000000000000a2 T Z12creat_sign32Pc動態(tài)鏈接對于靜態(tài)庫的使用,有下面兩個問題當(dāng)我們需要對某一個庫進行更新的時候,我們必須把一個可執(zhí)行文件再完整的進行一些重新編譯 在程序運行的時候代碼是會被載入機器的內(nèi)存中,如果采用靜態(tài)庫就會出現(xiàn)一個庫需要被copy到多個內(nèi)存程序中,這個一方面占用了一定的內(nèi)存,另一方面對于 的cache不夠友好 鏈接的控制,從前面的介紹中可以看到靜態(tài)庫的連接行為我們不好控制,做不到在運行期替換使用的庫,編譯后的程序就是二進制代碼,有些代碼它們涉及到不同的機器和環(huán)境,假設(shè)在A 機器上編譯了一個程序X, 把它直接放到B機器上去運行,由于A和B環(huán)境存在差異,直接運行X程序可能存在問題,這個時候如果把和機器相關(guān)的這部分做成動態(tài)庫C,并且保證接口一致, 編譯X程序的時候只調(diào)用C的對外接口對于一般的用戶態(tài)的X程序而言,就可以簡單的從環(huán)境放到環(huán)境中但如果是靜態(tài)編譯,就可能做不到這點,需要在機器上重新編譯一次動態(tài)鏈接庫在linux被稱為共享庫(shared library,下文提到的共享庫和動態(tài)鏈接庫都是指代shared library),它主要是為了解決上面列出靜態(tài)庫的缺點而提出的目前在公司內(nèi)部許多產(chǎn)品線也開始逐步采用這種方式。 共享庫的使用 共享庫的使用主要有兩種方式,一種方式和.a的靜態(tài)庫類似由編譯器來控制,其實質(zhì)和二進制程序一樣都是由系統(tǒng)中的載入器(ld-linux.so)載入, 另一種是寫在代碼中,由我們自己的代碼來控制還是以前面的例子為例:g+ -shared -fPIC -o libx.so libx.cpp 編譯的時候和靜態(tài)庫類似,只是加上了 -shared 和 -fPIC, 將輸出命名改為.so然后和可執(zhí)行文件鏈接.a一樣,都是g+ -o main main.cpp -L./ -lx 這樣main就是調(diào)用 libx.so, 在運行的時候可能會出現(xiàn)找不到libx.so的錯誤,這個原因是由于動態(tài)的庫查找路徑的問題,動態(tài)庫默認的查找路徑是由/etc /ld.so.conf文件來指定,在運行可執(zhí)行文件的時候,按照順會去這些目錄下查找需要的共享庫。我們可以通過 環(huán)境變量 LD_LIBRARY_PATH來指定共享庫的查找路徑(注:LD_LIBRARY_PATH的優(yōu)先級比ld.so.conf要高).命令上運行 ldd ./main 我們可以看到這個二進制程序在運行的時候需要使用的動態(tài)庫,例如: libx.so = /home/bnh/tmp/test/libx.so (0x003cb000) libstdc+.so.6 = /usr/lib/libstdc+.so.6 (0x00702000) libm.so.6 = /lib/tls/libm.so.6 (0x00bde000) libgcc_s.so.1 = /lib/libgcc_s.so.1 (0x00c3e000) libc.so.6 = /lib/tls/libc.so.6 (0x00aab000)這里列出了main所需要的動態(tài)庫, 如果有看類似 libx.so=no found的錯誤,就意味著路徑不對,需要設(shè)置LD_LIBRARY_PATH來指定路徑小提示: 有一個特殊的環(huán)境變量LD_PRELOAD, 可以強行替換共享庫中運行的符號。 export LD_PRELOAD= “xxx.so”, 如果你程序運行過程中遇到了和xxx.so同名的符號,這個時候程序會使用到xxx.so中的符號手動載入共享庫 除了采用類型于靜態(tài)庫的方式來使用動態(tài)庫,我們還可以通過由代碼來控制動態(tài)庫的使用。這種方式允許應(yīng)用程序在運行時加載和鏈接共享庫,主要有下面的四個接口載入動態(tài)鏈接庫 void dlopen(constchar filename, int flag); 獲取動態(tài)庫中的符號 void dlsym(void handle, char symbol); 關(guān)閉動態(tài)鏈接庫 void dlclose(void handle); 輸出錯誤信息 constchar *dlerror(void); 看下面的例子:typedefint foo_t();foo_t * foo = (foo_t*) dlsym(handle, “foo”); 通過上面的方式我們可以載入符號”foo”所對應(yīng)的地址,然后通過強制類型轉(zhuǎn)換給一個函數(shù)指針,當(dāng)然這里函數(shù)指針的類型需要和符號的原型類型保持一致,這些一般是由共享庫所對應(yīng)的頭文件提供這 里要注意一個問題,在dlsym中載入的符號表示是和我們使用nm 庫文件所看到符號表要保持一致,這里就有一個前面提到的 gcc和g+符號表的不同,一個 int foo(), 如果是g+編譯,并且沒有extern “C”導(dǎo)出接口,那么用dlsym載入的時候需要用dlsym(handle, “_Z3foov”) 方式才可以載入函數(shù) int foo(),所以建議所以的共享庫對外接口都采用extern “C”的方式導(dǎo)出 純C接口對外使用,這樣在使用上也會比較方便dlopen 的flag 標(biāo)志可以選擇RTLD_GLOBAL , RTLD_NOW, RTLD_LAZY. RTLD_NOW, RTLD_LAZY只是表示載入的符號是一開始就被載入還等到使用的時候被載入,對于多數(shù)應(yīng)用而言沒有什么特別的影響這兩個標(biāo)志都可以通過| 和RTLD_GLOBAL一起連用這里主要是說明RTLD_GLOBAL的功能,考慮這樣的一個情況:我們有一個 main.cpp ,調(diào)用了兩個動態(tài)libA, 和libB,假設(shè)A中有一個對外接口叫做testA,在main.cpp可以通過dlsym獲取到testA的指針,進行使用但是對于libB 中的接口,它是看到不libA的接口,使用testA 是不能調(diào)用到libA中的testA的,但是如果在dlopen 打開libA.so的時候,設(shè)置了RTLD_GLOBAL這個選項,就可以把libA.so中的接口升級為全局可見, 這樣在libB中就可以直接調(diào)用libA中的testA,如果在多個共享庫都有相同的符號,并且有RTLD_GLOBAL選項,那么會優(yōu)先選擇第一個。另 外這里注意到一個問題,RTLD_GLOBAL使的動態(tài)庫之間的對外接口是可見的,但是動態(tài)庫是不能調(diào)用主程序中的全局符號,為了解決這個問題, gcc引入了一個參數(shù)-rdynamic,在編譯載入共享庫的可執(zhí)行程序的時候最后在鏈接的時候加上-rdynamic,會把可執(zhí)行文件中所有的符號變成 全局可見,對于這個可執(zhí)行程序而言,它載入的動態(tài)庫在運行中可以直接調(diào)用主程序中的全局符號,而且如果共享庫(自己或者另外的共享庫 RTLD_GLOBAL) 加中有同名的符號,會選擇可執(zhí)行文件中使用的符號,這在一些情況下可能會帶來一些莫名其妙的運行錯誤。小提示:/usr/sbin/lsof -p pid 可以查看到由pid在運行期所載入的所有共享庫 共享庫無論是通過dlopen方式載入還是載入器載入,實質(zhì)都是通過 mmap的方式把共享庫映射到內(nèi)存空間中去。mmap的參數(shù)MAP_DENYWRITE可以在修改已經(jīng)被載入某個進程文件的時候阻止對于內(nèi)存數(shù)據(jù)的修改, 由于現(xiàn)在內(nèi)核中已經(jīng)禁用這個參數(shù),直接導(dǎo)致的結(jié)果就是如果對mmap的文件進行修改,這個時候的修改會被直接反映到已經(jīng)被mmap映射的空間上。由于內(nèi)核 的不支持,使得共享庫不能在運行期進行熱切換,共享庫在更新的時候需要由載入的程序通過一些外部的方式來判斷,主動使用dlclose,并且dlopen 重新載入共享庫,如果是載入器載入那么需要重啟程序。另外這里的熱切換指的是直接copy覆蓋原有的共享庫,如果是采用mv或者軟連接的方式那么還是安全 的,共享庫被mv后不會影響原來的已經(jīng)載入它的程序。 g+ 加上 -rdynamic 參數(shù)實質(zhì)上相當(dāng)于ld鏈接的時候加上-E或者export-dynamic參數(shù),效果與g+ -Wl,-E或者g+ -Wl,export-dynamic的效果是一樣的。靜態(tài)庫和動態(tài)庫的混合編譯 目前我們多數(shù)的庫都是以靜態(tài)庫的方式提供,但是現(xiàn)在有許多地方出于運維和升級的考慮使用了許多動態(tài)鏈接庫,這樣不可避免的出現(xiàn)了大量的靜態(tài)庫與動態(tài)庫的混合使用,經(jīng)常會出現(xiàn)一些奇怪的錯誤,使用的時候需要有所關(guān)注對于一般情況下,只要靜態(tài)庫與共享庫之間沒有依賴關(guān)系,沒有使用全局變量(包括static變量),不會出現(xiàn)太多的問題,下面以出現(xiàn)的問題作例子來說明使用的注意事項。baidugz與zlib的沖突具體的說明可以參看wiki LibBaidugz baidugz 是百度早期用來解壓壓縮網(wǎng)頁,可以自動識別多數(shù)的網(wǎng)頁壓縮格式具有一定的容錯性,但是由于baidugz是早期zlib版本直接修改而來,出現(xiàn)與系統(tǒng)中版本不一致的時候就可能導(dǎo)致問題。在 /usr/lib64/ 下可以看到 libz.so, 我們在直接使用系統(tǒng)zlib的時候多是在鏈接的時候加上 -lz 就可以了。程序在運行的時候會直接到系統(tǒng)的目錄下去尋找libz.so,并且在運行期被載入。早 期的zlib代碼中有一部分函數(shù)和變量,雖然沒有通過zlib.h對外公開,但是還是采用了extern的方式被其他的.c文件使用(這里涉及到一個問題 就是一個源碼中的變量或接口要被同一個庫中其它地方使用,只能被extern,但extern 后就意味著可以被其它任意使用這個庫的程序看到和使用, 無論是否在對外接口中聲明), 還有個別接口可以使用static但沒有使用static。 這部分對內(nèi)公開(實際上對外也公開了)的接口, 在baidugz的修改過程中沒有被修改,在后來升級64位版本的時候,由于系統(tǒng)中的zlib與baidugz使用的zlib相差過大,zlib在本身的 升級過程中也沒有過多的考慮這個問題(它假設(shè)不會有并存的情況), 導(dǎo)致在鏈接的過程出現(xiàn)錯誤.在編寫動態(tài)庫的過程中,可以static的函數(shù)即使沒有暴露在頭文件也需要盡量static,避免和外界沖突。那種沒有對外公開接口就無所謂加不加static的觀點是存在一定風(fēng)險的.小提示:有 些程序使用 using namespace 這樣的匿名命名空間來規(guī)避沖突的問題,從編譯器角度而言,在代碼中使用確實不會產(chǎn)生沖突。 不過采用dlopen的方式卻還是可以通過強制獲取符號的方式運行在共享庫中使用using namespace 包含起來的函數(shù),但static的函數(shù)是不能被dlopen方式強制獲取的。地址無關(guān)代碼在64位下編譯動態(tài)庫的時候,經(jīng)常會遇到下面的錯誤/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編譯,然后在鏈接動態(tài)庫的地方加上-fPIC的參數(shù)編譯結(jié)果還是報錯,需要把共享庫所用到的所有靜態(tài)庫都采用-fPIC編譯一邊才可 以成功的在64位環(huán)境下編譯出動態(tài)庫。這里的-fPIC指的是地址無關(guān)代碼這里首先先說明一下裝載時重定位的問題,一個程序如果沒有用到任何動態(tài)庫,那么由于已經(jīng)知道了所有的代碼,那么裝載器在把程序載入內(nèi)存的過程中就可以直接安裝靜態(tài)庫在鏈接的時候定好的代碼段位置直接加載進內(nèi)存中的對應(yīng)位置就可以了。但是在面對動態(tài)的庫的時候 ,這種方式就不行了。假設(shè)需要載入共享庫A,但是在編譯鏈接的時候使用的共享庫和最后運行的不一定是同一個庫,在編譯期就沒辦法知道具體的庫長度,在鏈接的時候就沒辦法確定它或者其他動態(tài)庫的具體位置。另一個方面動態(tài)庫中也會用到一些全局的符號,這些符號可能是來自其他的動態(tài)庫,這在編譯器是沒辦法假設(shè)的 (如果可以假設(shè)那就全是靜態(tài)庫了)基于上面的原因,就要求在載入動態(tài)庫的時候?qū)τ谑褂玫降姆柕刂穼崿F(xiàn)重定位。在實現(xiàn)上在編譯鏈接的時候不做重定位操作,地址都采用相對地址,一但到了需要載入的時候,根據(jù)相對地址的偏移計算出最后的絕對地址載入內(nèi)存中。但是這種采用裝載時重定位的方式存在一個問題就是相同的庫代碼(不包括數(shù)據(jù)部分)不能在多個進程間共享(每個代碼都放到了它自己的進程空間中),這個失去了動態(tài)庫節(jié)省內(nèi)存的優(yōu)勢。為了解決這個問題,ELF中的做法是在數(shù)據(jù)段中建立一個指向那些需要被使用(內(nèi)部的位置無關(guān)簡單采用相對地址訪問就可以實現(xiàn))的地址列表(也被稱為全局偏移表,Global offset table, GOT). 可以通過GOT相對應(yīng)的位置進行間接引用.對于我們的32位環(huán)境來說, 編譯時是否加上-fPIC, 都不會對鏈接產(chǎn)生影響, 只是一份代碼的在內(nèi)存中有幾個副本的問題(而且對于靜態(tài)庫而言結(jié)果都是一樣的).但在64位的環(huán)境下裝載時重定位的方式存在一個問題就是在我們的64位環(huán) 境下用來進行位置偏移定位的cpu指令只支持32位的偏移, 但實際中位置的偏移是完全可能超過64位的,所以在這種情況下編譯器要求用戶必須采用fPIC的方式進行編譯的程序才可以在共享庫中使用從理論上來說-fPIC由于多一次內(nèi)存取址的調(diào)用,在性能上會有所損失.不過從目前的一些測試中還無法明顯的看出加上-fPIC后對庫的性能有多大的損失,這個可能和我們現(xiàn)在使用的機器緩存以及大量寄存器的存在相關(guān).小提示:-fPIC與-fpic 上面的介紹可以看到,gcc要使用地址無關(guān)代碼加上-fPIC即可,但是在gcc的手冊中我們可以看到一個-fpic(區(qū)別在一個大寫一個小寫)的參數(shù), 從功能上來說它們都是一樣的。-fpic在一些特定的環(huán)境中(包括硬件環(huán)境)可以有針對性的進行優(yōu)化,產(chǎn)生更小更快的代碼, 但是由于受到平臺的限制,像我們的編譯環(huán)境,開發(fā)環(huán)境,運行環(huán)境都不完全統(tǒng)一的情況下面使用fpic有一定未知的風(fēng)險,所有決大多數(shù)情況下我們使用 -fPIC來產(chǎn)生地址無關(guān)代碼。 共享內(nèi)存效率 共享內(nèi)存在只讀的情況下性能和讀普通內(nèi)存是一樣的(如果不算第一載入的消耗),而且由于是多個進程共享對cpu cache還顯的相對友好。 可以參見mmap性能 同時存在靜態(tài)庫和動態(tài)庫 前 面提到編譯動態(tài)庫的時候有提到編譯動態(tài)庫可以像編譯靜態(tài)庫那樣采用-Lpath -lxx的方式進行, 但這里存在一個問題,如果在path目錄下既有動態(tài)庫又有靜態(tài)庫的時候的行為又是什么樣地? 事實上在這種情下, 鏈接器優(yōu)先選擇采用動態(tài)庫的方式進行編譯.比如在同一目錄下存在 libx.a 和 libx.so, 那么在鏈接的時候會優(yōu)先選擇libx.so進行鏈接. 這也是為什么在com組維護的第三方庫(third, third-64)中絕大多數(shù)庫的產(chǎn)出物中只有.a的存在, 主要就是為了避免在默認情況下使用到.so的庫, 導(dǎo)致在上線的時候出現(xiàn)麻煩(特別是一些系統(tǒng)中存在,但又與我們需要使用的版本有出入的庫).為了能夠控制動態(tài)庫和靜態(tài)庫的編譯, 有下面的幾種方式直接使用要編譯的庫在前面也提到了在編譯靜態(tài)庫的時候有三種方式: 目標(biāo)文件.o 直接使用 靜態(tài)庫文件.a 直接編譯 采用 -L -l方式進行編譯 編譯的時候如果不采用-Lpath -lxx的方式進行編譯, 而且直接寫上 path/libx.a 或者 path/libx.so 進行編譯,那么在鏈接的時候就是使用我們指定的 .a 或者 .so進行編譯不會出現(xiàn) 所謂的動態(tài)庫優(yōu)先還是靜態(tài)庫優(yōu)先的問題. 但這個方案需要知道編譯庫的路徑,一些情況下并不適合使用。 static參數(shù)在gcc的編譯的時候加上static參數(shù), 這樣在編譯的時候就會優(yōu)先選擇靜態(tài)庫進行編譯,而不是按照默認的情況選擇動態(tài)庫進行編譯.不過使用static參數(shù)會帶來另外的問題,不推薦使用,主要會帶來下面的問題如果只有動態(tài)庫,而不存在同名的靜態(tài)庫,鏈接的時候也不會報錯,但在運行的時候可能會出現(xiàn)錯誤 /lib/ld64.so.1: bad ELF interpreter: 由于我們程序本身在運行的需要系統(tǒng)中一些庫的支持,包括libc, libm, phtread等庫,在采用static編譯方式之后,鏈接的就是這些庫的靜態(tài)編譯版本(glibc還是提供了靜態(tài)編譯的版本),我們等于使用的是編 譯機上的庫,但是我們的運行環(huán)境可能和編譯機有所不同,glibc這些動態(tài)庫的存在本身的目的就是為了能讓在一臺機器上編譯好的庫能夠比較方便的移到另外 的機器上,程序本身只需要關(guān)注接口,至于從接口到底層的部分由每臺機器上的.so來處理不過這個問題也不是那么絕對,在一些特殊情況下(比如 glibc, gcc存在大版本差異的時候,主要是gcc2到gcc3有些地方?jīng)]有做好,abi不兼容的問題比較突出,真遇到這些情況其實需要換編譯器了) static編譯反倒可以正常的運行但是還是不推薦使用, 這些是可以采用其它方法規(guī)范在后面的第6點中有說明另外就是glibc static編譯可能會產(chǎn)生下面的warning: warning: Using getservbyport_r in statically linked applications requires at runtime the shared libraries from the glibc version used for linking 這個主要原因是由于getservbyport_r這樣的接口還是需要動態(tài)庫的支持才可以運行,許多glibc的函數(shù)都存在這樣的問題, 特別是網(wǎng)絡(luò)編程的接口中是很常見的對一些第三方工具不友好,類似valgrind檢查內(nèi)存泄露為了不在一些特殊的情況下誤報(最典型的就是strlen可以參考valgrind的 wikiValgrind運行的程序不能夠使用-static來進行鏈接中的case3), 它需要用動態(tài)庫的方式替換glibc中的函數(shù),如果靜態(tài)編譯那么valgrind就無法替換這些函數(shù),產(chǎn)生誤報甚至無法報錯 tcmalloc在這種情況下也不能支持. 我們目前64位環(huán)境中使用的pthread庫,如果是使用的是動態(tài)庫那么采用的是ntpl庫,如果是靜態(tài)庫采用的linuxthread庫,使用 static 會導(dǎo)致性能下降(可以參考32/64位性能調(diào)研) static之后會導(dǎo)致代碼大小變大,對cpu代碼cache不友好,浪費內(nèi)存空間,不過對于小代碼問題也不大 早期使用static的一個原因是需要使用一些第三方面庫, 但是最后

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論