目錄[-]
前言
針對同一動態(tài)組件的不同版本鏈接和加載卒暂。
一、概念
DLL HELL字面意思是DLL"災(zāi)難"贫奠,是由于com組件(動態(tài)庫)升級引起的程序不能運行的情況痴腌。
原因
有三種可能的原因?qū)е铝薉LL Hell的發(fā)生:
一是由使用舊版本的DLL替代原來一個新版本的DLL而引起的焰坪。這個原因最普遍,是Windows 9X用戶通常遇到的DLL錯誤之一阁猜。
二是由新版DLL中的函數(shù)無意發(fā)生改變而引起丸逸。盡管在設(shè)計DLL時候應(yīng)該向下兼容,然而要保證DLL完全向下兼容卻是不能的剃袍。
三是由新版DLL的安裝引入一個新的Bug黄刚。
二、linux下的解決方案——命名規(guī)范
Linux 上的Dll 民效,叫sharedlibrary憔维。Linux 系統(tǒng)面臨和Window一樣的問題涛救,如何控制動態(tài)庫的多個版本問題。為解決這個問題业扒,Linux 為解決這個問題检吆,引入了一套命名機(jī)制,如果遵守這個機(jī)制來做程储,就可以避免這個問題蹭沛。但是這只事一個約定,不是強(qiáng)制的章鲤。但是建議遵守這個約定摊灭,否則同樣也會出現(xiàn) Linux 版的Dll hell 問題。
Real Name
首先是共享庫本身的文件名:共享庫的命名必須如 libname.so.x.y.z最前面使用前綴”lib”败徊,中間是庫的名字和后綴”.so”,最后三個數(shù)字是版本號帚呼。x是主版本號(Major Version Number),y是次版本號(Minor Version Number)皱蹦,z是發(fā)布版本號(Release Version Number)萝挤。
主版本號(不兼容):重大升級,不同主版本的庫之間的庫是不兼容的根欧。所以如果要保證向后兼容就不能刪除舊的動態(tài)庫的版本。
次版本號(向下兼容): 增量升級端蛆,增加一些新的接口但保留原有接口凤粗。高次版本號的庫向后兼容低次版本號的庫。
發(fā)布版本號(相互兼容):庫的一些諸如錯誤修改今豆、性能改進(jìn)等嫌拣,不添加新接口,也不更改接口呆躲。主版本號和次版本號相同的前提下异逐,不同發(fā)布版本之間完全兼容。
SO-NAME
嚴(yán)格遵守上述規(guī)定插掂,確實能避免動態(tài)庫因為版本沖突的問題灰瞻,但是讀者可能有疑問:在程序加載或運行的時候,動態(tài)鏈接器是如何知道程序依賴哪些庫辅甥,如何選擇庫的不同版本酝润?
Solaris和Linux等采用SO-NAME( Shortfor shared object name )的命名機(jī)制來記錄共享庫的依賴關(guān)系。每個共享庫都有一個對應(yīng)的“SO-NAME”(共享庫文件名去掉次版本號和發(fā)布版本號)璃弄。比如一個共享庫名為libtest.so.3.8.2,那么它的SO-NAME就是libtest.so.3要销。
在Linux系統(tǒng)中,系統(tǒng)會為每個共享庫所在的目錄創(chuàng)建一個跟SO-NAME相同的并且指向它的軟連接(Symbol Link)夏块。這個軟連接會指向目錄中主版本號相同疏咐、次版本號和發(fā)布版本號最新的共享庫纤掸。也就是說,比如目錄中有兩個共享庫版本分別為:/lib/libtest.so.3.8.2和/lib/libtest.so.3.7.5,么軟連接/lib/libtest.so.3指向/lib/libtest.so.3.8.2浑塞。
建立以SO-NAME為名字的軟連接的目的是借跪,使得所有依賴某個共享庫的模塊,在編譯缩举、鏈接和運行時垦梆,都使用共享庫的SO-NAME,而不需要使用詳細(xì)版本號仅孩。在編譯生產(chǎn)ELF文件時候托猩,如果文件A依賴于文件B,那么A的鏈接文件中的”.dynamic”段中會有DT_NEED類型的字段辽慕,字段的值就是B的SO-NAME京腥。這樣當(dāng)動態(tài)鏈接器進(jìn)行共享庫依賴文件查找時,就會依據(jù)系統(tǒng)中各種共享庫目錄中的SO-NAME軟連接自動定向到最新兼容版本的共享庫溅蛉。
★??readelf -d sharelibrary 可以查看so-name
★??Linux提供了一個工具——ldconfig公浪,當(dāng)系統(tǒng)中安裝或更新一個共享庫時,需要運行這個工具船侧,它會遍歷默認(rèn)所有共享庫目錄欠气,比如/lib,/usr/lib等镜撩,然后更新所有的軟鏈接预柒,使她們指向最新共享庫。
Link Name
當(dāng)我們在編譯器里使用共享庫的時候袁梗,如用GCC的“-l”參數(shù)鏈接共享庫libtXXX.so.3.8.1宜鸯,只需要在編譯器命令行指定 -l XXX 即可,省略了前綴和版本信息遮怜。編譯器會根據(jù)當(dāng)前環(huán)境淋袖,在系統(tǒng)中的相關(guān)路徑(往往由-L參數(shù)指定)查找最新版本的XXX庫。這個XXX就是共享庫的“鏈接名”锯梁。不同類型的庫可能有相同的鏈接名即碗,比如C語言運行庫有靜態(tài)版本(libc.a)也動態(tài)版本(libc.so.x.y.z)的區(qū)別,如果在鏈接時使用參數(shù)”-lc”,那么連接器就會根據(jù)輸出文件的情況(動態(tài)/靜態(tài))來選擇合適版本的庫陌凳。eg. ld使用“-static”參數(shù)時嗎拜姿,”-lc”會查找libc.a;如果使用“-Bdynamic”(默認(rèn)),會查找最新版本的libc.so.x.y.z。
更詳細(xì)可以參見
http://www.linuxidc.com/Linux/2012-04/59071.htm
代碼:???????
/* hello.c - demonstrate library use. */
#include
void hello(void)
{ printf("Hello, library world./n");}
/* libhello.h - demonstrate library use. */
void hello(void);
/* main.c -- demonstrate direct use of the "hello" routine */
#include "hello.h"
int main(void)
{
hello();
return 0;
}
1.生成共享庫冯遂,關(guān)聯(lián)real name 和soname 蕊肥。
???? gcc -g -Wall -fPIC -c hello.c -o hello.o
???? gcc -shared -W,soname,-libhello.so.0 -o libhello.so.0.0.0 hello.o
???? 將會生成共享庫libhello.so.0.0.0.
???? 可以用系統(tǒng)提供的工具查看共享庫的頭:
????? readelf -d libhello.so.0.0.0 | grep libhello
ox00000000000e(SONAME)??? library soname: [libhello.so.0]
2.應(yīng)用程序,引用共享庫。
????? 先手動生成link 名字壁却,以被后面的程序鏈接時用
????? ln -s libhello.so.0.0.0 libhello.so.0
????? gcc -g -Wall -c main.c -o main.o -I.
????? gcc? -o main main.o -lhello -L.
(這里我會出問題批狱。因為:執(zhí)行g(shù)cc? -o main main.o -lhello -L.命令的時候,默認(rèn)會找libhello.so這個文件展东,但是顯然沒有赔硫,直接出錯。我又執(zhí)行:
ln -s libhello.so.0.0.0 libhello.so 之后盐肃,才可以爪膊,到現(xiàn)在,沒明白為什么砸王?)
????? 查看編譯出來的程序:
????? readelf -d main | grep libhello
ox000000000001(NEEDED)??? shared library: [libhello.so.0]
????? 運行該程序推盛,需要指定共享庫的路徑。 有兩種辦法谦铃,第一種使用環(huán)境變量“LD_LIBRARY_PATH”. 兩外一種辦法就是將共享庫拷貝到系統(tǒng)目錄(path 環(huán)境變量指定的其中一個目錄)耘成。
????? 暫停! 我們還沒有解決一個問題是驹闰,程序只知道soname,怎么從soname 找到共享庫瘪菌,即real name 文件呢? 這需要我們定義一個link文件嘹朗,連接到共享庫本身师妙。
????? ln -s libhello.so.0.0.0 libhello.so.0
????? 當(dāng)然這個路徑需要放到LD_LIBRARY_PATH環(huán)境變量中。
???? 這樣就可以運行該程序屹培。
[Note]Linux 系統(tǒng)提供一個命令 ldconifg 專門為生成共享庫的soname 文件疆栏,以便程序在加載時后通過soname
找到共享庫。
同時該命令也為加速加載共享庫惫谤,把系統(tǒng)的共享庫放到一個緩存文件中,這樣可以提高查找速度珠洗×锿幔可以用下面命令看一下系統(tǒng)已有的被緩存起來的共享庫。
???? ld -p
運行程序:
./main這樣许蓖,直接運行是不行了蝴猪。動態(tài)庫必須在運行的時候,也指定路徑膊爪。所以自阱,請將.so文件,復(fù)制到/lib或者/usr/lib下米酬,然后執(zhí)行 ldconfig /lib命令就OK了沛豌。
(
ldconfig是一個動態(tài)鏈接庫管理命令
為了讓動態(tài)鏈接庫為系統(tǒng)所共享,還需運行動態(tài)鏈接庫的管理命令--ldconfig
ldconfig? 命令的用途,主要是在默認(rèn)搜尋目錄(/lib和/usr/lib)以及動態(tài)庫配置文件/etc/ld.so.conf內(nèi)所列的目錄下,搜索出可共享的動態(tài) 鏈接庫(格式如前介紹,lib*.so*),進(jìn)而創(chuàng)建出動態(tài)裝入程序(ld.so)所需的連接和緩存文件.緩存文件默認(rèn)為 ?/etc/ld.so.cache,此文件保存已排好序的動態(tài)鏈接庫名字列表.
ldconfig通常在系統(tǒng)啟動時運行,而當(dāng)用戶安裝了一個新的動態(tài)鏈接庫時,就需要手工運行這個命令.)
3.共享庫,小版本升級,即接口不變.
?? 當(dāng)升級小版本時加派,共享庫的soname 是不變的叫确,所以需要重新把soname 的那個連接文件指定新版本就可以。 調(diào)用ldconfig命令芍锦,系統(tǒng)會幫你做修改那個soname link文件竹勉,并把它指向新的版本呢。這時候你的應(yīng)用程序就自動升級了娄琉。
4.共享庫次乓,主版本升級,即接口發(fā)生變化孽水。
? 當(dāng)升級主版本時票腰,共享庫的soname 就會加1.比如libhello.so.0.0.0 變?yōu)?libhello.so.1.0.0. 這時候再運行l(wèi)dconfig 文件,就會發(fā)現(xiàn)生成兩個連接 文件匈棘。
??? ln -s libhello.so.0---->libhello.so.0.0.0
??? ln -s libhello.so.1----->libhello.so.1.0.0
盡管共享庫升級丧慈,但是你的程序依舊用的是舊的共享庫,并且兩個之間不會相互影響主卫。
??? 問題是如果更新的共享庫只是增加一些接口逃默,并沒有修改已有的接口,也就是向前兼容簇搅。但是這時候它的主版本號卻增加1.
如果你的應(yīng)用程序想調(diào)用新的共享庫完域,該怎么辦? 簡單瘩将,只要手工把soname 文件修改吟税,使其指向新的版本就可以。(這時候ldconfig
文件不會幫你做這樣的事姿现,因為這時候soname 和real name 的版本號主板本號不一致肠仪,只能手動修改)。
? 比如: ln -s libhello.so.0 ---> libhello.so.1.0.0
? 但是有時候备典,主版本號增加异旧,接口發(fā)生變化,可能向前不兼容提佣。這時候再這樣子修改吮蛹,就會報錯,“xx”方法找不到之類的錯誤拌屏。
總結(jié)一下潮针,Linux 系統(tǒng)是通過共享庫的三個不同名字,來管理共享庫的多個版本倚喂。 real name 就是共享庫的實際文件名字每篷,soname
就是共享庫加載時的用的文件名。在生成共享庫的時候,編譯器將soname 綁定到共享庫的文件頭里雳攘,二者關(guān)聯(lián)起來带兜。
在應(yīng)用程序引用共享庫時,其通過link name
來完成吨灭,link時將按照系統(tǒng)指定的目錄去搜索link名字找到共享庫刚照,并將共享庫的soname寫在應(yīng)用程序的頭文件里。當(dāng)應(yīng)用程序加載共享庫時喧兄,就會
通過soname在系統(tǒng)指定的目錄(path or LD_LIBRARY)去尋找共享庫无畔。
當(dāng)共享庫升級時,分為兩種吠冤。一種是主板本不變浑彰,升級小版本和build 號。在這種情況下拯辙,系統(tǒng)會通過更新soname( ldconfig 來維護(hù))郭变,來使用新的版本號。這中情況下涯保,舊版本就沒有用诉濒,可以刪掉。
另外一種是主版本升級夕春,其意味著庫的接口發(fā)生變化未荒,當(dāng)然,這時候不能覆蓋已有的soname及志。系統(tǒng)通過增加一個soname(ldconfig
-p 里面增加一項)片排,使得新舊版本同時存在。原有的應(yīng)用程序在加載時,還是根據(jù)自己頭文件的舊soname 去尋找老的庫文件速侈。
5.如果編譯的時候沒有指定率寡,共享庫的soname,會怎么樣倚搬?
? 這是一個trick 的地方冶共。第一系統(tǒng)將會在生成庫的時候,就沒有soname放到庫的頭里面潭枣。從而應(yīng)用程序連接時候,就把linkname
放到應(yīng)用程序依賴庫里面幻捏∨枥纾或者換句話說就是,soname這時候不帶版本號篡九。
有時候有人直接利用這點來升級應(yīng)用程序谐岁,比如,新版本的庫,直接拷貝到系統(tǒng)目錄下伊佃,就會覆蓋掉已經(jīng)存在的舊的庫文件窜司,直接升級。
這個給程序員很大程度的便利性航揉,如果一步小心塞祈,就會調(diào)到類似windows的Dll hell 陷阱里面。建議不要這樣做帅涂。
【Note】
? 1. 指定共享庫加載的路徑议薪。LD_LIBRARY_PATH 優(yōu)先于 path 環(huán)境變量。
? 2. ldd 可以查看程序媳友,或者共享庫依賴的庫的路徑
? 3. nm 查看共享庫暴露的接口
? 4. ldconfig 可以自動生成soname 的連接文件斯议。并提供catch 加速查找。
? 5.readelf 可以查看動態(tài)庫的信息醇锚,比如依賴的庫哼御,本身的soname。
? 6. objdump 與readelf 類似焊唬。
? 7 ld The GUN linker
? 8. ld.so? dynamic linker or loader
? 9. as the portable GNU assembley
【Reference】
http://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html