前言 html
針對同一動態組件的不一樣版本連接和加載。
1、概念
DLL HELL字面意思是DLL"災難",是因爲com組件(動態庫)升級引發的程序不能運行的狀況。
緣由
有三種可能的緣由致使了DLL Hell的發生:
一是由使用舊版本的DLL替代原來一個新版本的DLL而引發的。這個緣由最廣泛,是Windows 9X用戶一般遇到的DLL錯誤之一。
二是由新版DLL中的函數無心發生改變而引發。儘管在設計DLL時候應該向下兼容,然而要保證DLL徹底向下兼容倒是不能的。
三是由新版DLL的安裝引入一個新的Bug。
2、linux下的解決方案——命名規範
Linux 上的Dll ,叫sharedlibrary。Linux 系統面臨和Window同樣的問題,如何控制動態庫的多個版本問題。爲解決這個問題,Linux 爲解決這個問題,引入了一套命名機制,若是遵照這個機制來作,就能夠避免這個問題。可是這隻事一個約定,不是強制的。可是建議遵照這個約定,不然一樣也會出現 Linux 版的Dll hell 問題。
Real Name
首先是共享庫自己的文件名:共享庫的命名必須如 libname.so.x.y.z最前面使用前綴」lib」,中間是庫的名字和後綴」.so」,最後三個數字是版本號。x是主版本號(Major Version Number),y是次版本號(Minor Version Number),z是發佈版本號(Release Version Number)。
主版本號(不兼容):重大升級,不一樣主版本的庫之間的庫是不兼容的。因此若是要保證向後兼容就不能刪除舊的動態庫的版本。
次版本號(向下兼容): 增量升級,增長一些新的接口但保留原有接口。高次版本號的庫向後兼容低次版本號的庫。
發佈版本號(相互兼容):庫的一些諸如錯誤修改、性能改進等,不添加新接口,也不更改接口。主版本號和次版本號相同的前提下,不一樣發佈版本之間徹底兼容。
SO-NAME
嚴格遵照上述規定,確實能避免動態庫由於版本衝突的問題,可是讀者可能有疑問:在程序加載或運行的時候,動態連接器是如何知道程序依賴哪些庫,如何選擇庫的不一樣版本?
Solaris和Linux等採用SO-NAME( Shortfor shared object name )的命名機制來記錄共享庫的依賴關係。每一個共享庫都有一個對應的「SO-NAME」(共享庫文件名去掉次版本號和發佈版本號)。好比一個共享庫名爲libtest.so.3.8.2,那麼它的SO-NAME就是libtest.so.3。
在Linux系統中,系統會爲每一個共享庫所在的目錄建立一個跟SO-NAME相同的而且指向它的軟鏈接(Symbol Link)。這個軟鏈接會指向目錄中主版本號相同、次版本號和發佈版本號最新的共享庫。也就是說,好比目錄中有兩個共享庫版本分別爲:/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,而不須要使用詳細版本號。在編譯生產ELF文件時候,若是文件A依賴於文件B,那麼A的連接文件中的」.dynamic」段中會有DT_NEED類型的字段,字段的值就是B的SO-NAME。這樣當動態連接器進行共享庫依賴文件查找時,就會依據系統中各類共享庫目錄中的SO-NAME軟鏈接自動定向到最新兼容版本的共享庫。
★ readelf -d sharelibrary 能夠查看so-name
★ Linux提供了一個工具——ldconfig,當系統中安裝或更新一個共享庫時,須要運行這個工具,它會遍歷默認全部共享庫目錄,好比/lib,/usr/lib等,而後更新全部的軟連接,使她們指向最新共享庫。
Link Name
當咱們在編譯器裏使用共享庫的時候,如用GCC的「-l」參數連接共享庫libtXXX.so.3.8.1,只須要在編譯器命令行指定 -l XXX 便可,省略了前綴和版本信息。編譯器會根據當前環境,在系統中的相關路徑(每每由-L參數指定)查找最新版本的XXX庫。這個XXX就是共享庫的「連接名」。不一樣類型的庫可能有相同的連接名,好比C語言運行庫有靜態版本(libc.a)也動態版本(libc.so.x.y.z)的區別,若是在連接時使用參數」-lc」,那麼鏈接器就會根據輸出文件的狀況(動態/靜態)來選擇合適版本的庫。eg. ld使用「-static」參數時嗎,」-lc」會查找libc.a;若是使用「-Bdynamic」(默認),會查找最新版本的libc.so.x.y.z。
更詳細能夠參見 linux
http://www.linuxidc.com/Linux/2012-04/59071.htm 程序員
代碼: windows
/* hello.c - demonstrate library use. */
#include <stdio.h>
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.生成共享庫,關聯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. 性能
能夠用系統提供的工具查看共享庫的頭: ui
readelf -d libhello.so.0.0.0 | grep libhello spa
ox00000000000e(SONAME) library soname: [libhello.so.0]
2.應用程序,引用共享庫。
先手動生成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.
(這裏我會出問題。由於:執行gcc -o main main.o -lhello -L.命令的時候,默認會找libhello.so這個文件,可是顯然沒有,直接出錯。我又執行:
ln -s libhello.so.0.0.0 libhello.so 以後,才能夠,到如今,沒明白爲何?)
查看編譯出來的程序:
readelf -d main | grep libhello
ox000000000001(NEEDED) shared library: [libhello.so.0]
運行該程序,須要指定共享庫的路徑。 有兩種辦法,第一種使用環境變量「LD_LIBRARY_PATH」. 兩外一種辦法就是將共享庫拷貝到系統目錄(path 環境變量指定的其中一個目錄)。
暫停! 咱們尚未解決一個問題是,程序只知道soname,怎麼從soname 找到共享庫,即real name 文件呢? 這須要咱們定義一個link文件,鏈接到共享庫自己。
ln -s libhello.so.0.0.0 libhello.so.0
固然這個路徑須要放到LD_LIBRARY_PATH環境變量中。
這樣就能夠運行該程序。
[Note]Linux 系統提供一個命令 ldconifg 專門爲生成共享庫的soname 文件,以便程序在加載時後經過soname 找到共享庫。 同時該命令也爲加速加載共享庫,把系統的共享庫放到一個緩存文件中,這樣能夠提升查找速度。能夠用下面命令看一下系統已有的被緩存起來的共享庫。
ld -p
運行程序:
./main這樣,直接運行是不行了。動態庫必須在運行的時候,也指定路徑。因此,請將.so文件,複製到/lib或者/usr/lib下,而後執行 ldconfig /lib命令就OK了。
(
ldconfig是一個動態連接庫管理命令
爲了讓動態連接庫爲系統所共享,還需運行動態連接庫的管理命令--ldconfig
ldconfig 命令的用途,主要是在默認搜尋目錄(/lib和/usr/lib)以及動態庫配置文件/etc/ld.so.conf內所列的目錄下,搜索出可共享的動態 連接庫(格式如前介紹,lib*.so*),進而建立出動態裝入程序(ld.so)所需的鏈接和緩存文件.緩存文件默認爲 /etc/ld.so.cache,此文件保存已排好序的動態連接庫名字列表.
ldconfig一般在系統啓動時運行,而當用戶安裝了一個新的動態連接庫時,就須要手工運行這個命令.)
3.共享庫,小版本升級,即接口不變.
當升級小版本時,共享庫的soname 是不變的,因此須要從新把soname 的那個鏈接文件指定新版本就能夠。 調用ldconfig命令,系統會幫你作修改那個soname link文件,並把它指向新的版本呢。這時候你的應用程序就自動升級了。
4.共享庫,主版本升級,即接口發生變化。
當升級主版本時,共享庫的soname 就會加1.好比libhello.so.0.0.0 變爲 libhello.so.1.0.0. 這時候再運行ldconfig 文件,就會發現生成兩個鏈接 文件。
ln -s libhello.so.0---->libhello.so.0.0.0
ln -s libhello.so.1----->libhello.so.1.0.0
儘管共享庫升級,可是你的程序依舊用的是舊的共享庫,而且兩個之間不會相互影響。
問題是若是更新的共享庫只是增長一些接口,並無修改已有的接口,也就是向前兼容。可是這時候它的主版本號卻增長1. 若是你的應用程序想調用新的共享庫,該怎麼辦? 簡單,只要手工把soname 文件修改,使其指向新的版本就能夠。(這時候ldconfig 文件不會幫你作這樣的事,由於這時候soname 和real name 的版本號主板本號不一致,只能手動修改)。
好比: ln -s libhello.so.0 ---> libhello.so.1.0.0
可是有時候,主版本號增長,接口發生變化,可能向前不兼容。這時候再這樣子修改,就會報錯,「xx」方法找不到之類的錯誤。
總結一下,Linux 系統是經過共享庫的三個不一樣名字,來管理共享庫的多個版本。 real name 就是共享庫的實際文件名字,soname 就是共享庫加載時的用的文件名。在生成共享庫的時候,編譯器將soname 綁定到共享庫的文件頭裏,兩者關聯起來。 在應用程序引用共享庫時,其經過link name 來完成,link時將按照系統指定的目錄去搜索link名字找到共享庫,並將共享庫的soname寫在應用程序的頭文件裏。當應用程序加載共享庫時,就會經過soname在系統指定的目錄(path or LD_LIBRARY)去尋找共享庫。
當共享庫升級時,分爲兩種。一種是主板本不變,升級小版本和build 號。在這種狀況下,系統會經過更新soname( ldconfig 來維護),來使用新的版本號。這中狀況下,舊版本就沒有用,能夠刪掉。
另一種是主版本升級,其意味着庫的接口發生變化,固然,這時候不能覆蓋已有的soname。系統經過增長一個soname(ldconfig -p 裏面增長一項),使得新舊版本同時存在。原有的應用程序在加載時,仍是根據本身頭文件的舊soname 去尋找老的庫文件。
5.若是編譯的時候沒有指定,共享庫的soname,會怎麼樣?
這是一個trick 的地方。第一系統將會在生成庫的時候,就沒有soname放到庫的頭裏面。從而應用程序鏈接時候,就把linkname 放到應用程序依賴庫裏面。或者換句話說就是,soname這時候不帶版本號。 有時候有人直接利用這點來升級應用程序,好比,新版本的庫,直接拷貝到系統目錄下,就會覆蓋掉已經存在的舊的庫文件,直接升級。 這個給程序員很大程度的便利性,若是一步當心,就會調到相似windows的Dll hell 陷阱裏面。建議不要這樣作。
【Note】
1. 指定共享庫加載的路徑。LD_LIBRARY_PATH 優先於 path 環境變量。
2. ldd 能夠查看程序,或者共享庫依賴的庫的路徑
3. nm 查看共享庫暴露的接口
4. ldconfig 能夠自動生成soname 的鏈接文件。並提供catch 加速查找。
5.readelf 能夠查看動態庫的信息,好比依賴的庫,自己的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