Linux環境下的編譯,鏈接與庫的使用

爲什麼使用ullib有時會出現 undefined reference error 的錯誤?

爲什麼在動態鏈接庫裏ul_log會把日誌輸出到屏幕上?

爲什麼用-static 編譯有時候會報warning?

我們在使用基礎庫或者第三方庫的時候,經常遇到這樣那樣的問題,本文結合公司目前的主要環境,說明庫的原理,使用的注意事項。

從程序到可執行文件

從hello world 說起

includeint main() { printf(“hello worldn”); return 0; }

上面這一段程序任何一個學過C語言的同學都是閉着眼睛都可以寫出來,但是對於將這樣一個源代碼編譯成爲一個可執行文件的過程卻不一定有所瞭解。 上面的程序如果要編譯,很簡單

gcc hello.c 然後./a.out就可以運行,但是在這個簡單的命令後面隱藏了許多複雜的過程

一般來說,可以把這樣的過程分成4個, 預編譯, 編譯, 彙編和鏈接

預編譯 這個過程包括了下面的步驟

宏定義展開,所有的#define 在這個階段都會被展開 預編譯命令的處理,包括#if #ifdef 一類的命令 展開#include 的文件,像上面hello world 中的stdio.h , 把stdio.h中的所有代碼合併到hello.c中 去掉註釋 gcc的預編譯 採用的是預編譯器cpp, 我們可以通過-E參數來看預編譯的結果,如: gcc -E hello.c -o hello.i 生 成的 hello.i 就是經過了預編譯的結果 在預編譯的過程中不會太多的檢查與預編譯無關的語法(#ifdef 之類的還是需要檢查, #include文件路徑需要檢查), 但是對於一些諸如 ; 漏掉的語法錯誤,在這個階段都是看不出來的。 寫過makefile的人都知道, 我們需要加上-Ipath 一系列的參數來標示gcc對頭文件的查找路徑

小提示:

1.在一些程序中由於宏的原因導致編譯錯誤,可以通過-E把宏展開再檢查錯誤 , 這個在編寫 PHP擴展, python擴展這些大量需要使用宏的地方對於查錯誤很有幫助。

  1. 如果在頭文件中,#include 的時候帶上路徑在這個階段有時候是可以省不少事情, 比如 #include , 這樣在gcc的-I參數只需要指定一個路徑,不會由於不小心導致,文件名正好相同出現衝突的麻煩事情. 不過公司由於早期出現了lib2和lib2-64兩個目錄, 以及頭文件輸出在include 目錄下, 靜態發佈等一些歷史原因, 有些時候使用帶完整路徑名的方式不是那麼合適( 比如 #include 中間有一個include 顯的很彆扭).

不過個人認爲所有的#include 都應該是儘量採用從cvs 根路徑下開始寫完整路徑名的方式進行預編譯的過程,只是受限於公司原有習慣和歷史問題而顯的不合適, 當然帶路徑的方式要多寫一些代碼,也是麻煩的事情, 路徑由外部指定相對也會靈活一些.

編譯 這個過程纔是進行語法分析和詞法分析的地方, 他們將我們的C/C++代碼翻譯成爲 彙編代碼, 這也是一個編譯器最複雜的地方

使用命令

gcc -S hello.i -o hello.s 可 以看到gcc編譯出來的彙編代碼, 現代gcc編譯器一般是把預編譯和編譯合在一起,使用cc1 的程序來完成這個過程,在我們的開發機上有些時候一些同學編譯大文件的時候可以用top命令看一個cc1的進程一直在佔用時間,這個時候就是程序在執行編 譯過程. 後面提到的編譯過程都是指 cc1的處理包括了預編譯與編譯.

彙編 現在C/C++代碼已經成爲彙編代碼了,直接使用匯編代碼的編譯器把彙編變成機器碼(注意還不是可執行的) .

gcc -c hello.c -o hello.o 這裏的hello.o就是最後的機器碼, 如果作爲一個靜態庫到這裏可以所已經完成了,不需要後面的過程.

對於靜態庫, 比如ullib, COM提供的是libullib.a, 這裏的.a文件其實是多個.o 通過ar命令打包起來的, 僅僅是爲了方便使用,拋開.a 直接使用.o 也是一樣的

小提示:

  1. gcc 採用as 進行彙編的處理過程,as 由於接收的是gcc生成的標準彙編, 在語法檢查上存在不少缺陷,如果是我們自己寫的彙編代碼給as去處理,經常會出現很多莫名奇妙的錯誤.

鏈接 鏈接的過程,本質上來說是一個把所有的機器碼文件組合成一個可執行的文件 上面彙編的結果得到一個.o文件, 但是這個.o要生成二執行文件只靠它自己是不行的, 它還需要一堆輔助的機器碼,幫它處理與系統底層打交道的事情.

gcc -o hello hello.o 這樣就把一個.o文件鏈接成爲了一個二進制可執行文件. 我們提供的各種庫頭文件在編譯期使用,到了鏈接期就需要用-l, -L的方式來指定我們到底需要哪些庫。 對於glibc中的strlen之類常用的東西編譯器會幫助你去加上可以不需要手動指定。

這個地方也是本文討論的重點, 在後面會有更詳細的說明

小提示:

有些程序在編譯的時候會出現 “linker input file unused because linking not done” 的提示(雖然gcc不認爲是錯誤,這個提示還是會出現的), 這裏就是把 編譯和鏈接 使用的參數搞混了,比如

g++ -c test.cpp -I../../ullib/include -L../../ullib/lib/ -lullib 這樣的寫法就會導致上面的提示, 因爲在編譯的過程中是不需要鏈接的, 它們兩個過程其實是獨立的

靜態鏈接

鏈接的過程 這裏先介紹一下,鏈接器所做的工作

其實鏈接做的工作分兩塊: 符號解析和重定位

符號解析

符號包括了我們的程序中的被定義和引用的函數和變量信息

在命令行上使用 nm ./test

test 是用戶的二進制程序,包括

可以把在二進制目標文件中符號表輸出

00000000005009b8 A bss_start00000000004004cc t call_gmon_start00000000005009b8 b completed.10000000000500788 dCTOR_END0000000000500780 d CTOR_LIST00000000005009a0 D data_start00000000005009a0 W data_start0000000000400630 tdo_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 DGLOBAL_OFFSET_TABLE w gmon_start U gxx_personality_v0@@CXXABI_1.30000000000400448 T _init0000000000500780 A __init_array_end… 當然上面由nm輸出的符號表可以通過編譯命令去除,讓人不能直接看到。

鏈接器解析符號引用的方式是將每一個引用的符號與其它的目標文件(.o)的符號表中一個符號的定義聯繫起來, 對於那些和引用定義在相同模塊的本地符號(注:static修飾的),編譯器在編譯期就可以發現問題,但是對於那些全局的符號引用就比較麻煩了.

下面來看一個最簡單程序:

includeint foo();int main() { foo(); return 0; }

我們把文件命名爲test.cpp, 採用下面的方式進行編譯 g++ -c test.cppg++ -o test test.o 第一步正常結束,並且生成了test.o文件,到第二步的時候報瞭如下的錯誤

test.o(.text+0x5): In function main':: undefined reference tofoo()’collect2: ld returned 1 exit status 由於foo 是全局符號, 在編譯的時候不會報錯,等到鏈接的時候,發現沒有找到對應的符號,就會報出上面的錯誤。但是如果我們把上面的寫法改成下面這樣

include//注意這裏的static static int foo();int main() { foo(); return 0; }

在運行 g++ -c test.cpp, 馬上就報出下面的錯誤:

test.cpp:19: error: ‘int foo()’ used but never defined 在編譯器就發現foo 無法生成目標文件的符號表,可以馬上報錯,對於一些本地使用的函數使用static一方面可以避免符號污染,另一方面也可以讓編譯器儘快的發現錯誤.

在我們的基礎庫中提供的都是一系列的.a文件,這些.a文件其實是一批的目標文件(.o)的打包結果.這樣的目的是可以方便的使用已有代碼生成的結果,一般情況下是一個.c/.cpp文件生成一個.o文件,在編譯的時候如果帶上一堆的.o文件顯的很不方便,像:

g++ -o main main.cpp a.o b.o c.o 這樣大量的使用.o也很容易出錯,在linux下使用 archive來講這些.o存檔和打包.

所以我們就可以把編譯參數寫成

g++ -o main main.cpp ./libullib.a 我們可以使用 ./libullib.a 直接使用 libullib.a這個庫,不過gcc提供了另外的方式來使用:

g++ -o main main.cpp -L./ -lullib -L指定需要查找的庫文件的路徑, -l 選擇需要使用的庫名字,不過庫的名字需要用 lib+name的方式命名,纔會被gcc認出來. 不過上面的這種方式存在一個問題就是不區分動態庫和靜態庫, 這個問題在後面介紹動態庫的時候還會提到.

當存在多個.a ,並且在庫之間也存在依賴關係,這個時候情況就比較複雜.

如果我們要使用lib2-64/dict, dict又依賴ullib, 這個時候需要寫成類似下面的形式

g++ -o main main.cpp -L../lib2-64/dict/lib -L../lib2-64/ullib/lib -ldict -lullib -lullib 需要寫在-ldict的後面, 這是由於在默認情況對於符號表的解析和查找工作是由後往前(內部實現是一個類似堆棧的尾遞歸). 所以當所使用的庫本身存在依賴關係的時候,越是基礎的庫就越是需要放到後面.否則如果上面把 -ldict -lulib的位置換一下,可能就會出現 undefined reference to xxx 的錯誤. 一般來說對於基礎庫的依賴關係可以在平臺上獲取, 若存在一些第三方的依賴,就只有參考相關的幫助說明了

當然gcc提供了另外的方式的來解決這個問題

g++ -o main main.cpp -L../lib2-64/dict/lib -L../lib2-64/ullib/lib -Xlinker “-(” -ldict -lullib -Xlinker “-)” 可以看到我們需要的庫被 -Xlinker “-(“ 和 -Xlinker “-)”  包含起來,gcc在這裏處理的時候會循環自動查找依賴關係,不過這樣的代價就是延長gcc的編譯時間,如果使用的庫非常的多時候,對編譯的耗時影響還是非常大.

-Xlinker有時候也簡寫成”-Wl, “,它的意思是 它後面的參數是給鏈接器使用的.-Xlinker 和 -Wl 的區別是一個後面跟的參數是用空格,另一個是用”,”

我們通過nm 命令查看目標文件,可以看到類似下面的結果

~/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 TZ12creat_sign32Pc

其中用 U 標示的符號_Z11ul_readfilePcS_Pvi (其實是ullib中的 ul_readfile) ,表示在dict的目標文件中沒有找到ul_readfile函數.

在鏈接的時候,鏈接器就會去其他的目標文件中查找_Z11ul_readfilePcS_Pvi的符號

小提示:

編 譯的時候採用 -Lxxx -lyyy 的形式使用庫,-L和-l這個參數並沒有配對的關係,我們的一些Makefile 爲了維護方便把他們寫成配對的形式,給一些同學造成了誤解. 其實我們完全可以寫成 -Lpath1, -Lpath2, -Lpath3, -llib1 這樣的形式.

在具體鏈接的時候,gcc是以.o文件爲單位, 編譯的時候如果寫 g++ -o main main.cpp libx.o 那麼無論main.cpp中是否使用到libx.o,libx.o中的所有符號都會被載入到mian函數中.但是如果是針對.a,寫成g++ -o main main.cpp -L./ -lx, 這個時候gcc在鏈接的時候只會鏈接有被用到.o, 如果出現libx.a中的某個.o文件中沒有任何一個符號被main用到,那麼這個.o就不會被鏈接到main中

gcc編譯.c文件的時候 和g++ 有一個不一樣的地方, 就是在 g++ 中對於一個函數必須要先定義在再使用,比如上面的例子中需要先定義foo()才能被使用,但對於gcc編譯的.c(如果是.cpp會自動換成C++編譯) 文件, 可以不需要先定義, 而直接使用. 但這樣會出現問題, 如果沒有其他地方使用和這個函數同名的函數那麼鏈接的時候會找不到這個函數. 但是如果碰巧在另外的地方存在一個同名函數,那麼鏈接的時候就會被直接連接到這個函數上, 萬一使用的時候偏偏傳入參數或返回值的類型不對,那麼這個時候就可能出現莫名奇妙的錯誤. 不過我們還是可以用-Wmissing-declarations參數打開這個檢查

重定位

經過上面的符號解析後,所有的符號都可以找到它所對應的實際位置(U表示的鏈接找到具體的符號位置).

as 彙編生成一個目標模塊的時候,它不知道數據和代碼在最後具體的位置,同時也不知道任何外部定義的符號的具體位置,所以as在生成目標代碼的時候,對於位置未知的符號,它會生成一個重定位表目,告訴鏈接器在將目標文件合併成可執行文件時候如何修改地址成最終的位置

g++和gcc 採用gcc 和g++ 在編譯的時候產生的符號有所不同.

在C++中由於要支持函數重載,命名空間等特性,g++會把函數+參數(可能還有命名空間),把函數命變成一個特殊並且唯一的符號名.例如:

int foo(int a); 在gcc編譯後,在符號表中的名字就是函數名foo, 但是在g++編譯後名字可能就變成了_Z3fooi, 我們可以使用 c++filt命令把一個符號還原成它原本的樣子,比如

c++filt _Z3fooi 運行的結果可以得到 foo(int)

由於在C++和純C環境中,符號表存在不兼容問題,C程序不能直接調用C++編譯出來的庫,C++程序也不能直接調用C編譯出來的庫.爲了解決這個問題C++中引入了 extern “C” 的方式.

extern “C” int foo(int a); 這樣在用g++編譯的時候, c++的編譯器會自動把上面的 int foo(int a)當做C的接口進行符號轉化.這樣在純C裏面就可以認出這些符號.

不過這裏存在一個問題,extern “C” 是C++支持的,gcc並不認識,所有在實際中一般採用下面的方式使用++

ifdef cplusplus extern “C” { #endifint foo(int a); #ifdef cplusplus } #endif

這樣這個頭文件中的接口即可以給gcc使用也可以給g++使用, 當然在extern “C” { } 中的接口是不支持重載,默認參數等特性

在 我們的64位編譯環境中如果有gcc的程序使用上面方式g++編譯出來的庫,需要加上-lstdc++, 這是因爲,對於我們64位環境下g++編譯出來的庫,需要使用到一個 gxx_personality_v0 的符號,它所在的位置是/usr /lib64/libstdc++.so.6 (C++的標準庫iostream都在裏面,C++程序都需要的). 但是在我們的32位2.96 g++編譯器中是不需要gxx_personality_v0,所有編譯可以不加上 -lstdc++

小提示:

在linux gcc 中, 只有在源代碼使用 .c做後綴,並且使用gcc編譯纔會被編譯成純C的結果,其他情況像 g++ 編譯.c文件,或者gcc 編譯.cc, .cpp文件都會被當作C++程序編譯成C++的目標文件, gcc和g++唯一的不同在於gcc不會主動鏈接-lstdc++ 在 extern “C” { }中如果存在默認參數的接口,在g++編譯的時候不會出現問題,但是gcc使用的時候會報錯.因爲對於函數重載,接口的符號表還是和不用默認參數的時候是一樣的. 編譯器版本問題 目前公司內部使用的gcc版本主要分兩種

32位 gcc 2.96 64位 gcc 3.4.4 (這是編譯機的版本號,我們的開發機多數是gcc 3.4.5, 小版本號的差異,目前看來不會對程序會帶來影響) 有時候在32位環境中經常會出現"undefined reference error”的錯誤,這個問題多數是由於 gcc 的版本問題造成的,我們許多的32位機器上的編譯器都是3.x的版本,gcc 從2到3做了很大的改動,c++的符號表的表現有所區別,導致gcc3的編譯器不能鏈接由gcc2.96編譯出來的庫.我們的基礎庫在lib2下的都是採 用靜態發佈(直接發佈最後的二進制庫,而不是在需要的時候重新編譯).不過在gcc3的glibc中考慮了向下兼容性使的可以正常運行由gcc 2.96上編譯出來的二進制程序. 我們現在有一種方式是在gcc2.96環境下編譯出來的二進制程序放到64位機器上去運行,如果我們是一個新的 64位機器環境上運行程序,實際上這是無法運行的,我們的程序之所以可以這樣做,是由於在我們的64位機器上裝上32位程序運行的環境,包括載入32位程 序的載入器,對應的各種動態庫,可以在64位機器上/usr/lib/rh80目錄下看所使用各種動態庫,不過這些庫的版本與我們的開發機編譯機上版本有 所不同,有些時候我們會發現如果64位機器上的32位程序運行出core, 把core文件放到開發機上進行調試會看到出現在glibc的動態庫的函數都core在一些很奇怪的位置,根本不是我們程序中調用的位置,這裏很重要的原 因就在於動態庫的版本不一樣

符號表衝突 我們在編譯程序的時候時常會遇到類似於

multiple definition of `foo()’ 的錯誤.

這些錯誤的產生都是由於所使用的.o文件中存在了相同的符號造成的.

比如:

libx.cpp

int foo() { return 30; } liby.cpp

int 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 這 個時候編譯不會報錯,它會選擇第一個出現的庫,上面的例子中會選擇libx中的foo. 但是注意不是所有的情況都是這樣的,由於鏈接是以.o爲單位的,完全可以不用某個.o的時候纔不會出錯誤,否則依然會出現multipe的錯誤, 這種情況下的建議是查看一下這些函數的行爲是什麼樣子,是否是一致的,如果不一致,還是想辦法規避, 如果是一致的話可以用 -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編譯的結果就會報錯,但如果a和b都是g++編譯的話結果卻一切正常.

這個原因主要是在g++中會把INTVALUE 這種const常量當做static的,這樣就是一個局部變量,不會導致衝突,但是如果是gcc編譯的話,這個地方INTVALUE會被認爲是一個對外的全局常量是非static的,這個時候就會造成鏈接錯誤

小提示 上說了對於a庫和b庫出現同樣符號的情況會有衝突, 但是在實際中有這麼一種情況, a庫定義的foo的接口,在有b庫的情況下是一種行爲,在沒有b庫的情況下又想要一種行爲。爲解決這個問題引入了弱連接的機制, 前面我們看到nm後,有些符號前面有T標誌,這個表示的是這個符號是一個強連接。 如果看有W的表示,那麼就表示這個符號是弱連接。如果有一個同名的庫也有相同的符號並且是強連接,那麼就可以替代掉他(如果也是弱連接,會存在先後順序用 誰的問題)。 glibc中的符號都是弱連接, 我們可以在我們的程序中編寫 open, write之類的函數去替換掉glibc中的實現。

如果我們要自己寫弱連接的函數可以採用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 TZ12creat_sign32Pc


動態鏈接

對於靜態庫的使用,有下面兩個問題

當我們需要對某一個庫進行更新的時候,我們必須把一個可執行文件再完整的進行一些重新編譯 在程序運行的時候代碼是會被載入機器的內存中,如果採用靜態庫就會出現一個庫需要被copy到多個內存程序中,這個一方面佔用了一定的內存,另一方面對於CPU的cache不夠友好 鏈接的控制,從前面的介紹中可以看到靜態庫的連接行爲我們不好控制,做不到在運行期替換使用的庫 編譯後的程序就是二進制代碼,有些代碼它們涉及到不同的機器和環境,假設在A 機器上編譯了一個程序X, 把它直接放到B機器上去運行,由於A和B環境存在差異,直接運行X程序可能存在問題,這個時候如果把和機器相關的這部分做成動態庫C,並且保證接口一致, 編譯X程序的時候只調用C的對外接口.對於一般的用戶態的X程序而言,就可以簡單的從A環境放到B環境中.但如果是靜態編譯,就可能做不到這點,需要在B 機器上重新編譯一次. 動態鏈接庫在linux被稱爲共享庫(shared library,下文提到的共享庫和動態鏈接庫都是指代shared library),它主要是爲了解決上面列出靜態庫的缺點而提出的.目前在公司內部許多產品線也開始逐步採用這種方式。 共享庫的使用 共享庫的使用主要有兩種方式,一種方式和.a的靜態庫類似由編譯器來控制,其實質和二進制程序一樣都是由系統中的載入器(ld-linux.so)載入,另一種是寫在代碼中,由我們自己的代碼來控制.

還是以前面的例子爲例:

g++ -shared -fPIC -o libx.so libx.cpp 編譯的時候和靜態庫類似,只是加上了 -shared 和 -fPIC, 將輸出命名改爲.so

然後和可執行文件鏈接.a一樣,都是

g++ -o main main.cpp -L./ -lx 這 樣main就是調用 libx.so, 在運行的時候可能會出現找不到libx.so的錯誤, 這個原因是由於動態的庫查找路徑的問題, 動態庫默認的查找路徑是由/etc /ld.so.conf文件來指定,在運行可執行文件的時候,按照順會去這些目錄下查找需要的共享庫。我們可以通過 環境變量 LD_LIBRARY_PATH來指定共享庫的查找路徑(注:LD_LIBRARY_PATH的優先級比ld.so.conf要高).

命令上運行 ldd ./main 我們可以看到這個二進制程序在運行的時候需要使用的動態庫,例如:

    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)

這裏列出了mian所需要的動態庫, 如果有看類似 libx.so=>no found的錯誤,就意味着路徑不對,需要設置LD_LIBRARY_PATH來指定路徑

小提示: 有一個特殊的環境變量LD_PRELOAD, 可以強行替換共享庫中運行的符號。 export LD_PRELOAD= “xxx.so”, 如果你程序運行過程中遇到了和xxx.so同名的符號,這個時候程序會使用到xxx.so中的符號

手動載入共享庫 除了採用類型於靜態庫的方式來使用動態庫,我們還可以通過由代碼來控制動態庫的使用。

這種方式允許應用程序在運行時加載和鏈接共享庫,主要有下面的四個接口

載入動態鏈接庫

void dlopen(constchar filename, int flag); 獲取動態庫中的符號 void dlsym(void handle, char symbol); 關閉動態鏈接庫 void dlclose(void handle); 輸出錯誤信息 constchar *dlerror(void); 看下面的例子:

typedefint foo_t();foo_t * foo = (foo_t*) dlsym(handle, “foo”); 通過上面的方式我們可以載入符號”foo”所對應的地址,然後通過強制類型轉換給一個函數指針,當然這裏函數指針的類型需要和符號的原型類型保持一致,這些一般是由共享庫所對應的頭文件提供.

這 裏要注意一個問題,在dlsym中載入的符號表示是和我們使用nm 庫文件所看到符號表要保持一致,這裏就有一個前面提到的 gcc和g++符號表的不同,一個 int foo(), 如果是g++編譯,並且沒有extern “C”導出接口,那麼用dlsym載入的時候需要用 dlsym(handle, “_Z3foov”) 方式纔可以載入函數 int foo(), 所以建議所以的共享庫對外接口都採用 extern “C”的方式導出 純C接口對外使用,這樣在使用上也會比較方便

dlopen 的flag 標誌可以選擇 RTLD_GLOBAL , RTLD_NOW, RTLD_LAZY. RTLD_NOW, RTLD_LAZY只是表示載入的符號是一開始就被載入還等到使用的時候被載入,對於多數應用而言沒有什麼特別的影響.這兩個標誌都可以通過| 和RTLD_GLOBAL一起連用

這裏主要是說明RTLD_GLOBAL的功能,考慮這樣的一個情況:

我們有一個 main.cpp ,調用了兩個動態 libA, 和 libB, 假設A中有一個對外接口叫做 testA, 在main.cpp可以通過dlsym獲取到testA的指針,進行使用.但是對於libB 中的接口,它是看到不libA的接口,使用testA 是不能調用到libA中的testA的,但是如果在dlopen 打開libA.so的時候,設置了RTLD_GLOBAL這個選項,就可以把libA.so中的接口升級爲全局可見, 這樣在libB中就可以直接調用libA中的testA,如果在多個共享庫都有相同的符號,並且有RTLD_GLOBAL選項,那麼會優先選擇第一個。

另 外這裏注意到一個問題, RTLD_GLOBAL使的動態庫之間的對外接口是可見的,但是動態庫是不能調用主程序中的全局符號,爲了解決這個問題, gcc引入了一個參數-rdynamic,在編譯載入共享庫的可執行程序的時候最後在鏈接的時候加上-rdynamic,會把可執行文件中所有的符號變成 全局可見,對於這個可執行程序而言,它載入的動態庫在運行中可以直接調用主程序中的全局符號,而且如果共享庫(自己或者另外的共享庫 RTLD_GLOBAL) 加中有同名的符號,會選擇可執行文件中使用的符號,這在一些情況下可能會帶來一些莫名其妙的運行錯誤。

小提示:

/usr/sbin/lsof -p pid 可以查看到由pid在運行期所載入的所有共享庫 共享庫無論是通過dlopen方式載入還是載入器載入,實質都是通過 mmap的方式把共享庫映射到內存空間中去。mmap的參數MAP_DENYWRITE可以在修改已經被載入某個進程文件的時候阻止對於內存數據的修改, 由於現在內核中已經禁用這個參數,直接導致的結果就是如果對mmap的文件進行修改,這個時候的修改會被直接反映到已經被mmap映射的空間上。由於內核 的不支持,使得共享庫不能在運行期進行熱切換,共享庫在更新的時候需要由載入的程序通過一些外部的方式來判斷,主動使用dlclose,並且dlopen 重新載入共享庫,如果是載入器載入那麼需要重啓程序。另外這裏的熱切換指的是直接copy覆蓋原有的共享庫,如果是採用mv或者軟連接的方式那麼還是安全 的,共享庫被mv後不會影響原來的已經載入它的程序。 g++ 加上 -rdynamic 參數實質上相當於ld鏈接的時候加上-E或者–export-dynamic參數,效果與g++ -Wl,-E或者g++ -Wl,–export-dynamic的效果是一樣的。

靜態庫和動態庫的混合編譯 目前我們多數的庫都是以靜態庫的方式提供,但是現在有許多地方出於運維和升級的考慮使用了許多動態鏈接庫,這樣不可避免的出現了大量的靜態庫與動態庫的混合使用,經常會出現一些奇怪的錯誤,使用的時候需要有所關注

對於一般情況下,只要靜態庫與共享庫之間沒有依賴關係,沒有使用全局變量(包括static變量),不會出現太多的問題,下面以出現的問題作例子來說明使用的注意事項。

baidugz與zlib的衝突

具體的說明可以參看wiki LibBaidugz baidugz 是百度早期用來解壓壓縮網頁,可以自動識別多數的網頁壓縮格式具有一定的容錯性,但是由於baidugz是早期zlib版本直接修改而來,出現與系統中版本不一致的時候就可能導致問題。

在 /usr/lib64/ 下可以看到 libz.so, 我們在直接使用系統zlib的時候多是在鏈接的時候加上 -lz 就可以了。程序在運行的時候會直接到系統的目錄下去尋找libz.so,並且在運行期被載入。

早 期的zlib代碼中有一部分函數和變量,雖然沒有通過zlib.h對外公開,但是還是採用了extern的方式被其他的.c文件使用(這裏涉及到一個問題 就是一個源碼中的變量或接口要被同一個庫中其它地方使用,只能被extern,但extern 後就意味着可以被其它任意使用這個庫的程序看到和使用, 無論是否在對外接口中聲明), 還有個別接口可以使用static但沒有使用static。 這部分對內公開(實際上對外也公開了)的接口, 在baidugz的修改過程中沒有被修改,在後來升級64位版本的時候,由於系統中的zlib與baidugz使用的zlib相差過大,zlib在本身的 升級過程中也沒有過多的考慮這個問題(它假設不會有並存的情況), 導致在鏈接的過程出現錯誤.

在編寫動態庫的過程中,可以static的函數即使沒有暴露在頭文件也需要儘量static,避免和外界衝突。那種沒有對外公開接口就無所謂加不加static的觀點是存在一定風險的.

小提示:

有 些程序使用 using namespace {} 這樣的匿名命名空間來規避衝突的問題,從編譯器角度而言,在代碼中使用確實不會產生衝突。 不過採用dlopen的方式卻還是可以通過強制獲取符號的方式運行在共享庫中使用using namespace {}包含起來的函數,但static的函數是不能被dlopen方式強制獲取的。

地址無關代碼

在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還顯的相對友好。 可以參見mmap性能 同時存在靜態庫和動態庫 前 面提到編譯動態庫的時候有提到編譯動態庫可以像編譯靜態庫那樣採用-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: 由於我們程序本身在運行的需要系統中一些庫的支持,包括libc, libm, phtread等庫,在採用–static編譯方式之後,鏈接的就是這些庫的靜態編譯版本(glibc還是提供了靜態編譯的版本),我們等於使用的是編 譯機上的庫,但是我們的運行環境可能和編譯機有所不同,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檢查內存泄露爲了不在一些特殊的情況下誤報(最典型的就是strlen可以參考valgrind的 wikiValgrind運行的程序不能夠使用-static來進行鏈接中的case3), 它需要用動態庫的方式替換glibc中的函數,如果靜態編譯那麼valgrind就無法替換這些函數,產生誤報甚至無法報錯. tcmalloc在這種情況下也不能支持. 我們目前64位環境中使用的pthread庫,如果是使用的是動態庫那麼採用的是ntpl庫,如果是靜態庫採用的linuxthread庫,使用–static 會導致性能下降(可以參考32/64位性能調研) –static之後會導致代碼大小變大,對cpu代碼cache不友好,浪費內存空間,不過對於小代碼問題也不大. 早期使用–static的一個原因是需要使用一些第三方面庫, 但是最後運行的線上機器和編譯機的庫存在一些行爲不一致問題或者不兼容問題, 需要靜態編譯進去.對於這個問題,com的建議是採用third和third-64下的庫, 通過我們自己編譯的庫來控制, com目前維護的第三方庫相關資料見第三方庫, 如果有新的第三方庫需求可以聯繫com組. 採用我們自己編譯的第三方庫可以避免由於庫底層和運行機器環境不兼容造成問題,同時也可以避免使用–static參數. 同時第三方庫的編譯參數也可以由我們根據實際情況進行定製, 只需要做到編譯機編譯的結果可以在線上機器(或者測試機)上正常運行. 早 期有一些測試表明在32位的環境下, 採用–static全部使用靜態庫可以使程序性能有1%~3%的提高,這個主要原因在於-fPIC產生的二次尋址問題導致(glibc那些庫都是採用了 -fPIC的方式進行編譯).但對於我們的很多程序本身調用glibc的地方就不是很多(如果這裏會產生瓶頸,設法減少這一次函數調用效果是會更好的), 加上我們機器緩存大,代碼小,這種的性能提高實在是很有限了. 在運行了多個程序的機器上反倒可能由於cache不友好有反效果

特別的是在ullib 3.1.27以後版本引入了comlog 輸出網絡日誌,由於網絡庫的存在 會報出這樣鏈接錯誤, 建議去除

小提示:

編譯機差異 我們目前的環境的下, 編譯機,開發機,線上機器都存在一些不同的地方 32位環境機器差異比較大, 開發機和編譯機上是2.96 gcc, glibc版本 2.2.5 .線上32位機器現在已經很少了, 但我們經常把32位程序放到64位機器上運行, 64位機器下的32位環境中的glibc是2.2.93版本,其它各種庫的版本也存在差異,使用動態庫編譯出問題的概率較大,特別使用了某些庫的特殊接口 64位機器環境相對比較接近, 開發機上是gcc.3.4.5, 編譯機是3.4.4 相差一個小版本號. 線上機器3.4.5和3.4.4都存在,內核版本也有略微不同 雖然編譯機和線上機器環境存在略微的差異,不過這些差距並不大,目前的還沒有因爲這個原因造成問題. 對原來使用–static編譯的老程序的建議 首先要確定使用–static編譯的原因是什麼, 根據不同的原因採用不同的方案 性能原因, 即使是經過測試認爲–static可以提升性能, 提升的也是非常有限的,不建議爲了這一點性能而使用–static, 另外注意就是原來在32位下有性能優勢當升級到64位機器上可能就沒有優勢了 使用的第三方庫比如-lz等, 如果出現不加–static, 會出現在線上機器不能運行. 這裏建議使用由我們自己維護的第三方庫第三方庫,像基礎庫那樣使用. 如果有不同版本和新庫的需求請聯繫com組 存在動態庫和靜態庫的混合應用,推薦使用下一章節的 -dn, -dy參數

Linux環境編譯,鏈接與庫的使用(四)

鏈接參數控制

鏈接器中提供了-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的方式載入,由於是運行期,這個程序在這個地 方就直接運行報錯了. 另外還有一種情況就是一個對外的接口在動態庫中已經聲明定義了,但是忘記實現了,這個時候也會產生類似的錯誤.

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

日誌庫問題 其實不只是日誌庫存在這樣的問題,其他需要同時被多個動態庫以及主程序同時使用的函數其實都存在這樣的問題.這裏主要以日誌庫的問題爲例來說明這些問題.

有 一個程序,它通過dlopen的方式調用了一個.so文件. 在這個主程序中和.so中都使用了日誌庫,主程序中使用ul_openlog打開了日誌, 在.so中沒有用ul_openlog打開日誌.這個時候發現,主程序中的日誌正常輸出,但.so中的日誌卻直接輸出到了標準出錯.

這個問 題的原因在前面的其實已經提到了,在默認情況下主程序中使用的接口對於.so是不可見的,.so所在的代碼空間與主程序的代碼空間是隔離的,這個時 候.so調用的ul_writelog其實是沒有經過ul_openlog的那塊代碼空間,由於ul_log庫使用了一些static變量(如果是帶 comlog的ul_openlog那還有全局符號),只有在ul_writelog, ul_openlog都是在同一塊空間上的時候纔會起作用.

這個問題的一個最簡單的解決方案是

在主程序的鏈接的時候加入-rdynamic,仍然鏈接libullib.a庫 編譯動態鏈接庫時,不加鏈接libullib.a庫 其實動態庫這裏是否鏈了libullib.a已經不重要了,在有-rdynamic的情況下,.so中如果有與主程序同名的函數那麼會優先調用主程序中的函數, 動態庫不鏈接libullib.a倒是可以省點空間

但是這種方式在某些情況還是不能完全解決問題

假 設有A.so, B.so,主程序main, 在A.so中調用了ul_openlog, B.so中沒有調用ul_openlog, 但調用了ul_writelog. 在主程序中沒有調用ul_log中的任何接口和使用任何變量.這種情況下即時使用了-rdynamic還是會導致在 A.so中正常輸出日誌,但在B.so中卻把日誌輸出到標準出錯.

這個問題的主要原因在於,gcc在鏈接的時候是以.o爲單位的,如果一 個.o中的符號沒有被外部所使用,那麼在鏈接的時候就不會把這個.o中的符號給鏈接進行.so或者二進制程序中.在上面的問題中主程序裏面沒有調用到日誌 庫中的任何符號,所以在鏈接的時候就不會把ullib中的ul_openglog和ul_writelog給鏈接進行主程序中,這個時候即使有 -rdynamic也是做不到讓.so中的動態鏈接庫都使用.

這個問題一般有下面幾種方案:

載入A.so的時候使用RTLD_GLOBAL參數,把A.so中的所有的符號都變成對外可見,這樣A.so和B.so的ul_writelog都在一塊代碼空間中了 編譯主程序的時候鏈接ullib的地方由-lullib改爲 -Wl,–whole-archive -lullib -Wl,–no-whole-archive, 同時加上-rdynamic. -Wl, –whole-archive是鏈接參數,它表示把ullib的中所有的符號都鏈接進主程序中,而不管主程序是否調用了ullib中的符 號.-Wl,–no-whole-archive表示後面的鏈接取消–whole-archive的行爲,畢竟其他的庫沒有必要採用這種方式全部鏈接 進來. 在主程序中隨便調一下ul_log中的符號,比如可以先隨便ul_openlog一下,然後ul_closelog, 後面再進行動態庫調用. 把libullib.a用 ar x 命令還原成多個.o文件,採用直接鏈接的方式使用ul_log.o. 上 面的幾個問題的產生主要還在於靜態鏈接和動態鏈接混用,而兩種鏈接方式又存在不一樣的地方.事實上如果我們把ullib庫 採用動態鏈接庫的方式編譯成 libullib.so, 採用前面的方式在編譯期鏈接libullib.so,並且設置LD_LIBRARY_PATH, 上面的2個問題都不會存在. 編譯期使用的.so,是全局可見的,不需要-rdynamic也可以被dlopen的動態庫所用到,由於是動態鏈接庫,所以 包含了所有的符號,不會像靜態庫那樣只包含了所用到的.o中的符號. 事實上這也是多數第三方程序的解決方案

主程序中使用-rdynamic會對後續的升級造成一些麻煩:

加上-rdynamic後,像日誌庫這樣的基礎如果需要升級, 那麼就必須要升級主程序, 使用的.so無論如何升級所用到的ul_writelog都是主程序中的. 主程序中除了日誌庫,還會有其他庫或者函數的存在, 這些函數如果不是static的就有可能與 dlopen打開的so中的函數混到一起,造成困惑 如果.so中需要打印它自己的日誌, 那樣需要comlog本身功能的支持纔可以實現,而不能簡單的使用ul_writelog來實現 但是如果主程序中沒有使用-rdynamic,那麼又有下面的這些麻煩 dlopen打開的動態庫日誌是打印自己的, 不能和主程序統一在一起. 如果.so的程序和主程序open的是同一個日誌,這相當於多進程打日誌, 那必須要comlog的支持,ullog本身不支持多進程打同一個日誌. 如果主程序中用dlopen+RTLD_GLOBAL的打開了某個.so 日誌的問題就可能影響到其他的.so中的調用. 同時對於一些老程序, 升級前後-rdynamic 可能也會產生影響,比如兩次ul_openlog 這裏對於類似日誌庫這種需要全局狀態變量支持的庫提出另外方案

  1. 編譯一個專門的.so, 這個.so中包括了其它.so中所需要的所有和全局量相關的接口 2. 主程序不使用-rdynamic編譯, 但打開上面的.so的時候,採用RTLD_GLOBAL方式,並且是第一個打開 3. 除了打開第一個 .so, 其它的.so都不使用RTLD_GLOBAL方式, 並且在編譯的時候都不把和第一.so相關的庫聯編 4. 第一個.so的升級需要保證沒有其它.so在運行纔可以dlclose, 重新dlopen

這個問題首先需要明確需求, 到底是希望每個.so打自己獨立的日誌,還是和主線程統一

這 裏要注意另外一個問題就是目前的ullib 日誌庫情況比老的ullog要複雜, 在comlog中引入一些extern 出來的全局變量 在採用dlopen的時候,對於一般符號,一般都是主程序和動態庫在兩塊空間中, 但是對於使用extern出來的變量主程序和動態庫都是在一塊空間中(注:由於32位下不用-fPIC也可以編譯so, 在沒有-fPIC的情況也是分開的, 但是由於64位一定要-fPIC所以一定會出現同一塊空間的問題), 對於這個問題的解決方案是在動態庫鏈接的時候加上 -Wl,-Bsymbolic 參數將動態庫的空間和主程序的空間強行分開。

上面有提到 編譯動態鏈接庫時,不加鏈接libullib.a庫,但主程序使用-rdynamic, 這裏主要是爲了避免使用到了不同的ullib導致調用了一些不同的內部符號,導致出現另外的麻煩

對於動態庫中的日誌建議採用下面的幾個方案:

  1. 動態庫完全打自己的,日誌,編譯二進制程序不要用-rdynamic, 動態庫鏈接的編譯加上 -Wl,-Bsymbolic 參數 , 鏈接ullib, 在動態庫中自己open,自己控制等級 1. 動態庫不鏈接ullib, 編譯二進制程序用-rdynamic , 這樣可以正確的使用主程序中的日誌庫,也規避了版本不一致帶來的問題, 但是這樣失去了對於動態庫日誌的控制, 而且存在升級的不便, 日誌的升級是由主程序控制的。

小提示:

有關動態庫使用的例子還可以參考 SoTips 在運行期可以通過設置環境變量LD_DEBUG查看每個符號具體鏈接到了什麼地方,每個符號具體的查找過程和綁定過程.可以這樣使用 export LD_DEBUG=help 隨便運行一個程序就可以看到對於LD_DEBUG的使用說明

export LD_DEBUG=files./main 可以看到整個裝載過程

版本管理

系 統中存在了大量的動態和靜態庫,並且每個庫都會隨着庫的升級和更新,形成各種的版本,這些版本之間又存在了各種各樣的兼容或者不兼容的問題.linux中 是如何維護和管理這些庫的?這裏介紹了linux在這方面所作的一些工作.下面的這些都是基於我們現在使用的64位開發環境中的情況, 與32位的老版本存在了一定程度上的不兼容.

命名 在linux系統中對於一個共享庫的命名一般是 libname.so.x.y.z

這個與百度目前通常使用的版本項目版本是類似,我們的版本號多了一個4位版本,作爲開發過程中的小版本號,最後發佈的其實也是按照3位版本號進行的.

不過在linux中 也有不少不遵守上面的命名的,比如glibc的動態庫叫libc-2.3.5.so, 版本號在.so前面

這裏又存在了另一個問題,由於版本號和命名捆綁在一起,那麼當庫升級的時候有怎麼把版本號給對應上呢?

編譯和載入的版本 我們先看一下pthread庫在系統中的情況,(下面是以64位開發機爲例,32位路徑有所不同其它都一樣)

在 /usr/lib64/ 中我們可以找到libpthread.so, 不過可以到libpthread.so其實很下,cat 一下可以看到這其實是一個文本文件

libpthread.so的內容:

/ GNU ld script Use the shared library, but some functions are only in the static library, so try that secondarily. /OUTPUT_FORMAT(elf64-x86-64)GROUP ( /lib64/libpthread.so.0 /usr/lib64/libpthread_nonshared.a ) 這 個libpthread.so 其實並不是什麼共享庫,它其實是一個ld的鏈接腳本,這個腳本的意思是,輸出的是elf64-x86-64格式,使用的動 態庫是/lib64/libc.so.6 靜態庫是/usr/lib64/libc_nonshared.a, 這樣我們在編譯的時候不用考慮使用的是哪一個版本的libpthread.so,也不需要靠它的動態庫叫什麼,靜態庫叫什麼,只需要都是指定  -lpthread就可以了,具體實際是哪個版本,交給腳本去處理.

我們再看/lib64/libpthread.so.0, 其實這是一個軟鏈接,它實際指向的是 libpthread-0.10.so, libpthread-0.10.so纔是它真正使用的.so. pthread它通過這樣的方式進行鏈接有什麼作用呢?

我 先隨便寫個使用了pthread的程序,然後用readelf -d 查看,這時候可以看到pthread那一行指向的是 libpthread.so.0!, 而不是libpthread-0.10.so. 其實pthread這樣處理主要是從版本的兼容性方面進行考慮.在我們編譯的階段通過ld腳本,統一了編譯時候-l使用的命字,我們編譯的時候不需要去考 慮什麼.so的版本號問題.編譯完了實際指向的是 libpthread.so.0,而不是最後的libpthread-0.10.so, 這樣的好處在下面的幾個方面:

如果pthread升級了, 比如升級後叫做libpthread-0.20.so,作爲開發者可以保證它和libpthread-0.10.so是兼容的,那麼我們可以大膽的把 libpthread.so.0的軟鏈接指向libpthread-0.20.so,這不會出現什麼問題. 如果libpthread-0.20.so與libpthread-0.10.so是不兼容的,那麼我們可以新建立一個叫做 libpthread.so.1的符號鏈接指向libpthread-0.20.so,這樣老的程序在運行由於它認爲自己使用的是 libpthread.so.0而不會指向libpthread.so.1, 而用新版本編譯出來的程序會自動依賴到libpthread.so.1,而不會出現依賴了老的libpthread-0.10.so而導致不兼容.需要依 賴老版本的程序在運行的過程中也不會出現因爲依賴的庫替換了不兼容的庫導致出現問題. 符號版本 上面的這個過程很好的解決了在同一臺機器上編譯和使用動態庫的更新問題.但是還有一些問題上面的方式無法完全解決

比如我現在的程序是使用了libpthread-0.20.so編譯出來的程序,但是在運行的機器上只有libpthread-0.10.so, 這個時候有2種選擇 1. 警報,但讓程序繼續運行,這有可能會出core 2. 直接禁止運行

現在的問題,很有可能我雖然用了新的庫進行編譯,但我只用到了老的接口,用libpthread-0.10.so就可以勝任我的工作, 但無論警報也好還是直接禁止運行也好這些都顯的不合適.

爲 了能解決這個問題, linux在符號中引入了版本的機制.我們可以用nm /lib64/libc.so.6 查看glibc中的符號表(我們32位環境中的glibc符號表被清空了,只能通過readelf進行查看). 我們可以看有不少接口被寫成類似於 tmpfile@@GLIBC_2.2.5 的形式, 在符號中帶有版本號信息, 這方式可以使程序在編譯的時候記錄下它編譯的時候使用的共享庫所對應的接口的版本號,這樣如果共享庫升級了, 當程序載入共享庫的時候會檢查編譯時記錄下來的版本是否與當前共享庫接口的版本是否兼容,如果兼容那麼就可以正常運行,如果有不兼容的情況就在載入庫的時 候報錯. 這種機制儘可能的考慮高版本與低版本的兼容性問題,如果我們打開glibc的源碼包,我們可以到許多目錄下都有一個叫Versions的文件,這個文件就 是用來描述各接口的兼容版本.

共享庫可執行 我們直接運行 /lib64/tls/libc.so.6 , 可以看到,在運行後出現了一段的文字,表示了庫的版本,作者, 編譯時間等信息.這種方式提供了一種給用戶確認和了解動態庫的方式. 這種方式的實現也很簡單, 下面是一個demo

include#include#include#ifdef cplusplus extern “C” { #endif#ifndef i386 / * @brief 使用的動態鏈接庫 * */#define LD_SO_PATH “/lib64/ld-linux-x86-64.so.2” #else#define LD_SO_PATH “/lib/ld-linux.so.2” #endif#if defined( DATE) && defined( TIME) / * @brief 編譯時間 * /#define BUILD_DATE ( DATE “” TIME) #else#define BUILD_DATE “unknown” #endif/** * @brief 設置入口位置 */constchar interp[] attribute((section(“.interp”))) = LD_SO_PATH; / * @brief .so文件運行的入口函數 * /void so_main() { //printf輸出可以方便外部grep printf(“Ld.so : %sn”, LD_SO_PATH); //這個可以規避由於-O2開關導致,((section(“.interp”))) 被優化沒的問題 printf(“Project : %sn”, “mc_pack php extension for mcpack2”); printf(“Version : %sn”, VERSION); printf(“CVS : %sn”, “public/php-ex/php-mcpack”); printf(“CVSTag : %sn”, CVSTAG); printf(“BuildDate : %sn”, BUILD_DATE); exit(0); }#ifdef cplusplus } #endif

LD_SO_PATH 表示了所使用的加載器,這裏要注意32位和64位的區別,這裏把過程簡化了用宏__i386來判斷32位與64位

section(“.interp”) 設置了運行使用的載入器

so_main是運行程序的地方,這裏寫上共享編譯的信息, 當然這個地方可以換成別的什麼名字, 在編譯的時候需要加上 -Wl,-e,so_main, 指明瞭動態庫的實際的入口位置(這裏是so_name, 也可以換成xxx)

採用的編譯參數

g++ xxx.cpp -fPIC -c -Wl, -e,so_main 在把生成的.o與共享的主程序鏈接到一起,就可以直接運行共享庫了

總結和建議

上面介紹庫(包括靜態庫和共享庫)鏈接的過程,並對應其實現進行了分析,並且針對編譯中出現的問題進行分析和解決.

這對於我們使用庫是有一定的幫助的. 這裏對於庫的編寫和使用提出一些建議

靜態庫 儘量不要使用–static參數, 對於一些特殊的必須要使用.so的情況,可以考慮使用-Wl,-dn 的方式進行.

64位環境中編譯加上-fPIC 雖然是靜態庫,但很可能會被用作共享庫的一部分

鏈接的時候注意鏈接的順序,越是基礎庫越是在後面

共享庫 開發

對外接口儘量使用基本類型,不要使用C++類, 如果一定需要建議採用指針的方式

對外接口採用extern “C”的方式提供, 不要直接使用一般的C++接口,這主要是從接口的方便性考慮,畢竟都不希望採用”_fooIV”這種形式的接口進行訪問.

共享庫的生成文件需要帶上我們的4位版本號, 格式與linux的格式相同, 如 mcpack.so.1.1.0.0

共享庫需要能夠自運行, 運行需要輸出版本號, 編譯時間,庫的簡單說明, DEMO見上文中共享庫可執行部分. 輸出格式建議:

採用printf輸出,考慮到以後可能可以用腳本分析 輸出格式參考DEMO的樣式

小心使用-rdynamic參數, 能夠不使用就不要使用

儘量保證主程序的編譯依賴與動態庫的編譯依賴相同,特別是在主程序代碼中使用-rdynamic.

發佈

動 態庫發佈到output下除了要有帶版本號的.so,還需要同時有一個未帶版本號的.so, 比如在output/lib目錄需要有mcpack.so.1.1.0.0 和一個軟鏈接 mcpack.so 指向mcpack.1.1.0.0, 這裏的mcpack.so軟鏈接主要是給其它程序編譯期使用的.

上線

嚴禁對於.so採取直接copy覆蓋的方式

更新.so, 可以採用兩種方式:

軟鏈接方式, 程序運行使用的是確定mcpack.so軟鏈接,上線的時候採用的是把mcpack.so鏈接指向mcpack.so.1.1.0.0, 由於原來的程序還在,這樣不會出現問題 mv方式, 舊的.so需要先mv成另外的文件名,然後再放入新的.so 需要確認.so已經被載入, 由於.so的使用本身有2種方式,需要考慮下面兩種情況 dlopen方式,這種重新載入可以適用於熱切換,這個要求RD根據代碼邏輯來控制, 一般可以從日誌看出 LD_LIBRARY_PATH指定路徑,這種情況一般第三方庫的情況比較多,這個時候需要把程序重啓 所有操作完畢之後,需要用/usr/sbin/lsof -p pid 查看載入的動態庫是否是我們需要的 不要在終端或者.bash_profile 等全局環境中加入LD_LIBRARY_PATH, 在啓動需要.so的程序腳本中加入即可.

FAQ

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

編譯問題太複雜了,有沒有簡單的解決方案? 這裏建議大家使用com組提供的Comake自動編譯構建工具,來解決這些存在複雜依賴的情況, 在comake中 通過簡單的描述就可以規避大量的編譯鏈接問題

我是純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進行聯編, 這可以參考 public/php-ex/php-wordseg中的實現

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是先找到的,並且沒有同名動態庫存在.

我們一般使用ullib庫都是直接獲取.a進行編譯,但是public下的庫都是需要我們自己手動編譯後纔可以使用,爲什麼要這樣做? 這個地方涉及到一些歷史問題, lib2和lib2-64下的庫都是以庫的形式發佈這裏有兩個原因:

類似ullib這樣的庫到處都要用,每次都編譯比較花時間 類似於wordseg這樣的庫有保密需求,不方便向所有工程師透露原代碼 一般我們把發佈到lib2和lib2-64下的庫稱爲靜態發佈, 發佈到public下的庫都是實時發佈,它們的區別就在於public下的庫每次都會重新進行編譯. 但是這種靜態發佈會給庫的升級帶來一些麻煩:

靜態編譯的庫之間有依賴關係,一些接口性的升級會超成連鎖反應,導致多個庫都需要升級. 比如最簡單的情況: ullib庫中有一個叫foo(char str)的接口, 由於是老代碼當時沒有考慮到foo的接口上需要使用 const ,現在希望能夠滿足編程規範採用const接口變成foo(const charstr)的形式.這個時候假設其他靜態發佈的庫libX也使用了這個接口. 在前面的介紹中我們提到了在c++中會根據接口類型不同導致符號不同,我們把foo(char str)改爲foo(const charstr)導致現在編譯出來的ullib中符好是const charstr的形式,而不是charstr的形式, 這樣在聯合編譯libX和ullib的時候,libX就會說foo(char str)這個接口找不到, 這個時候一種解決方案就是libX也升一次級, 這樣又帶來另外的問題就是強迫大家都必須把相關的庫都升上去,這個往往是不現實的, 因爲某些原因這個過程往往需要分成幾步走( 比如需要考慮新分詞和老分詞的策略區別, 版本相差太多的風險性). 目前這方面COM採用的解決方式是在.cpp文件中依然保留foo(char str)的接口,這樣可以使得老庫在依賴新的ullib的時候不會出問題. 但是這個方案還是不能本質解決問題, 比如新的uldict使用新的ullib編譯出來, 現在需要使用一個新的dict和一個老的ullib聯合編譯,由於dict依賴了ullib中的const char的接口,但老的ullib又不存在const char這樣的接口, 這個時候聯編又會發現符號找不到.我們只能做到老的使用新的不出問題,但新的使用老的還是存在一些問題,目前只能建議用戶升級到最新的ullib上.如果 升級存在困難請聯繫com組.

靜態發佈受到編譯器和環境的限制 我們發佈的是使用32位gcc 2.96和64位gcc3.4編譯出來的庫,如果換到另外的gcc環境中又需要重新編譯,特別是存在有多個靜態發佈的庫需要重新編譯. 上面的這些問題,如果是在採用實時編譯的話都可以不會出問題的,我們可以保證了代碼級別上的兼容,但對於已經存在的符號表的兼容確實比較麻煩.

但是事實上這些庫採用和public的類似在scmpf上進行實現編譯其實也是可以做到,只是目前還是保留過去的編譯方式,比較靜態編譯目前出現的問題都是可以規避的.

另外在third和third-64下的第三方庫也是採用靜態發佈,這個主要考慮是第三方庫的編譯比較消耗時間.

使用cvs co libsrc/ullib 的方式可以直接獲取到ulllib的原代碼

哪些情況會出現 ”undefined reference error” 的錯誤? 這裏再總結一下這個問題可能出現的場景:

沒有指定對應的庫(.o/.a/.so) 使用了庫中定義的實體,但沒有指定庫(-lXXX)或者沒有指定庫路徑(-LYYY),會導致該錯誤, 比如 使用uldict, 由於uldict中使用到了md5簽名需要庫crypto的支持,需要在-lullib之後加上-lcrypto. 連接庫參數的順序不對 在默認情況下,對於-l 使用庫的要求是越是基礎的庫越要寫在後面,無論是靜態還動態,這裏可以參考上文件中靜態鏈接的章節. gcc/ld版本不匹配 gcc/ld的版本的兼容性問題,由於gcc2 到 gcc3大版本的兼容性存在問題(其實gcc3.2到3.4也一定程度上存在這樣的問題) 當在高版本機器上使用低版本的機器就會導致這樣的錯誤, 這個問題比較常見在我們32位的環境上, 許多32位的機器上是gcc3.2的編譯環境,但我們提供的是有gcc2.96編譯出來的結果,導致ullib庫不能被編譯器所認識. 另外就在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位機器上gcc2.96使用的程序覆蓋了64位機器給32位程序使用的庫導致鏈接失敗, 如 果沒有覆蓋的機器是正常的。

我將-lub -lub_log這樣連在一起使用是否有問題? 如 果確認所使用libub.a, libub_log.a它們的版本一致,這樣使用是沒有問題的,無論是libub中的ub_log的部分還是ub_log.a中的ub_log的部分,他 們都是屬於同樣的二進制代碼,無論怎麼鏈接都可以正常工作. 但是如果版本不同可能會有不可預知的後果(如最經典的baidugz和zlib的衝突).

爲什麼編寫的動態鏈接庫不能直接運行? 在共享庫的總結中介紹瞭如何實現共享庫可以自己運行,但是有些時候會出現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

staticint s_test = test_init(); //初始化s_testint 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是存在在程序退出前的執行析構等操作

dlopen是否可以載入主程序的符號? dlopen除了可以通過指定文件名載入共享庫中的符號其實也是可以載入主程序的符號,只不過要注意這個時候主程需要使用-rdynamic

在dlopen的時候使用dlopen(NULL, …) 的方式載入符號,這個時候可以載入此時運行程序中的所有全局符號,包括2個部分

dlopen具體共享的時候採用了RTLD_GLOBAL方式打開 主程序在鏈接的時候使用了-rdynamic參數 例: extern “C” { externint foo(int a, int b); } int foo(int a, int b) { fprintf(stderr, “%d %dn”, a, b); } typedefint (foo_t)(int, int);int main() { void handle = dlopen(NULL, RTLD_NOW); char *estr = dlerror(); if (estr != NULL) { fprintf(stderr, “myerror %sn”, estr); exit(-1); } foo_t fun = (foo_t)dlsym(handle, “foo”); estr = dlerror(); if (estr != NULL) { fprintf(stderr, “error %sn”, estr); exit(-1); } fun(3, 4); return 0; } 上面的程序採用g++ -o test test.cpp -ldl -rdynamic編譯後, fun可以被正常執行

有些程序對於庫的編譯依賴版本不一致,會有問題嗎? 一個例子:

一個程序依賴A庫和C庫, A庫依賴B庫的版本1, 但C庫依賴B庫的版本2

幾個注意: 1. 程序具體依賴到那個庫是在最後鏈接的時候決定的 2. 在平臺上SCM會選擇最高版本的那個B庫進行鏈接 3. 在編譯期,庫的依賴只是頭文件的依賴。

嚴格來說這樣是有一定的風險, 對於lib2和public下的庫,主要由com組在升級過程中會盡可能的規避這樣的風險,讓大家一般情況下都不需要關心這個問題。 一般來說, com組都會保證頭文件的編譯依賴的向下兼容

在scmpf平臺上對於編譯的問題是可以強制指定編譯依賴的。

另 外的一個問題就是,由於在平臺上只會針對一個版本進行編譯,不至於出現。 但是在線下編譯的時候,對於上面的情況可能還是會出現一些鏈接錯誤, 這個主要原因是由於A庫由版本1的B庫編譯,然後升級了, C庫由版本2個B庫編譯, 最後編程序的時候鏈接是版本2的B庫造成的, 遇到這種問題 , 一般建議是把A庫用版本2重新編譯一邊。因爲向下兼容一般只能保證做到高版本支持低版本,在不重新編譯的情況要做到兼容比較困難,另一方面意義也不大。



http://liuxun.org/blog/bian-yi--lian-jie-yu-ku-de-shi-yong-yi/點擊打開鏈接

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