Linux 如何解決共用庫的版本控制
Linux 系統,也同樣面臨和Window一樣的問題,如何控制動態庫的多個版本問題。Window之前沒有處理好,為此專門有個名詞來形容這個問題 “Dll hell”,其嚴重影響軟體的升級和維護。 Dll hell 是指windows 上動態庫新版本覆蓋舊版本,但是卻不相容老版本。常常發生在程式升級之後,動態庫更新,原有程式運行不起來;或者裝新軟體,但是已有的軟體運行不起來。 同樣Linux作業系統,也有同樣的問題,那麼它是怎麼解決的呢?
Linux 為解決這個問題,引入了一套機制,如果遵守這個機制來做,就可以避免這個問題。 但是這隻事一個約定,不是強制的。但是建議遵守這個約定,否則同樣也會出現 Linux 版的Dll hell 問題。 下面來介紹一個這個機制。 這個機制是通過檔案名稱,來控制dll (shared library) 的版本。
Linux 上的Dll ,叫shared library,其有三個名字,分別又不同的目的。
第一個是共用庫本身的檔案名稱(real name),其通常包含版本號碼,常常是是這樣: libmath.so.1.1.1234 。 lib是Linux 上的庫的約定首碼,math 是共用庫名子,so 是共用庫的尾碼名,1.1.1234的是共用庫的版本號碼,其主要版本號+小版本號碼+build號。主板號,代表當前動態庫的版本,如果動態庫的介面有變化,那麼這個版本號碼就要加1;後面的兩個版本號碼(小版本號碼 和 build 號)是告訴你詳細的資訊,比如為一個hot-fix 而產生的一個版本,其小版本號碼加1,build號也應有變化。 這個檔案名稱包含共用庫的代碼。
第二個是動態庫的soname( Short for shared object name),其是應用程式載入dll 時候,其尋找共用庫用的檔案名稱。其格式為
lib + math+.so + ( major version number)
其只包含major version number,換句話說,也就是只要其介面沒有變,應用程式都可以用,不管你其後minor build version or build version。
問題來了,程式運行時怎麼通過soname 找個real name? Soname 存在哪裡?如果與real name 關聯起來?什麼時候存的?
這就是接下來要介紹的第三個共用庫的名字,link name,顧名思義,就是在編譯過程,link 階段用的檔案名稱。 其將sonmae 和real name 關聯起來。
第三個名字,共用庫的串連名(link name),是專門為build 階段串連而用的名字。這個名字就是lib + math +.so ,比如libmath.so。其是不帶任何版本資訊的。在共用庫編譯過程中,串連(link) 階段,編譯器將產生一個共用庫及real name,同時將共用庫的soname,寫在共用庫檔案裡的檔案頭裡面。可以用命令 readelf -d sharelibrary 去查看。
在應用程式引用共用庫時,其會用到共用庫的link name。在應用程式的link階段,其通過link名字找到動態庫,並且把共用庫的soname 提取出來,寫在自己的共用庫的頭裡面。當應用程式載入時候就會通過soname 去在給定的路徑下尋找該共用庫。
下面通過這個代碼來說明一下系統是如何做的,並且介紹系統的一些設施和工具:
代碼:
1. File libhello.c
/* hello.c - demonstrate library use. */ #include <stdio.h> void hello(void) { printf("Hello, library world.\n");} |
2. File libhello.h
/* libhello.h - demonstrate library use. */ void hello(void); |
3. File main.c
/* 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.
可以用系統提供的工具查看共用庫的頭:
readelf -d libhello.so.0.0.0 | grep libhello
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.
查看編譯出來的程式:
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
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 可以查看動態庫的資訊,比如依賴的庫,本身的somae。
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
Linux 系統,也同樣面臨和Window一樣的問題,如何控制動態庫的多個版本問題。Window之前沒有處理好,為此專門有個名詞來形容這個問題 “Dll hell”,其嚴重影響軟體的升級和維護。 Dll hell 是指windows 上動態庫新版本覆蓋舊版本,但是卻不相容老版本。常常發生在程式升級之後,動態庫更新,原有程式運行不起來;或者裝新軟體,但是已有的軟體運行不起來。 同樣Linux作業系統,也有同樣的問題,那麼它是怎麼解決的呢?
Linux 為解決這個問題,引入了一套機制,如果遵守這個機制來做,就可以避免這個問題。 但是這隻事一個約定,不是強制的。但是建議遵守這個約定,否則同樣也會出現 Linux 版的Dll hell 問題。 下面來介紹一個這個機制。 這個機制是通過檔案名稱,來控制dll (shared library) 的版本。
Linux 上的Dll ,叫shared library,其有三個名字,分別又不同的目的。
第一個是共用庫本身的檔案名稱(real name),其通常包含版本號碼,常常是是這樣: libmath.so.1.1.1234 。 lib是Linux 上的庫的約定首碼,math 是共用庫名子,so 是共用庫的尾碼名,1.1.1234的是共用庫的版本號碼,其主要版本號+小版本號碼+build號。主板號,代表當前動態庫的版本,如果動態庫的介面有變化,那麼這個版本號碼就要加1;後面的兩個版本號碼(小版本號碼 和 build 號)是告訴你詳細的資訊,比如為一個hot-fix 而產生的一個版本,其小版本號碼加1,build號也應有變化。 這個檔案名稱包含共用庫的代碼。
第二個是動態庫的soname( Short for shared object name),其是應用程式載入dll 時候,其尋找共用庫用的檔案名稱。其格式為
lib + math+.so + ( major version number)
其只包含major version number,換句話說,也就是只要其介面沒有變,應用程式都可以用,不管你其後minor build version or build version。
問題來了,程式運行時怎麼通過soname 找個real name? Soname 存在哪裡?如果與real name 關聯起來?什麼時候存的?
這就是接下來要介紹的第三個共用庫的名字,link name,顧名思義,就是在編譯過程,link 階段用的檔案名稱。 其將sonmae 和real name 關聯起來。
第三個名字,共用庫的串連名(link name),是專門為build 階段串連而用的名字。這個名字就是lib + math +.so ,比如libmath.so。其是不帶任何版本資訊的。在共用庫編譯過程中,串連(link) 階段,編譯器將產生一個共用庫及real name,同時將共用庫的soname,寫在共用庫檔案裡的檔案頭裡面。可以用命令 readelf -d sharelibrary 去查看。
在應用程式引用共用庫時,其會用到共用庫的link name。在應用程式的link階段,其通過link名字找到動態庫,並且把共用庫的soname 提取出來,寫在自己的共用庫的頭裡面。當應用程式載入時候就會通過soname 去在給定的路徑下尋找該共用庫。
下面通過這個代碼來說明一下系統是如何做的,並且介紹系統的一些設施和工具:
代碼:
1. File libhello.c
/* hello.c - demonstrate library use. */ #include <stdio.h> void hello(void) { printf("Hello, library world.\n");} |
2. File libhello.h
/* libhello.h - demonstrate library use. */ void hello(void); |
3. File main.c
/* 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.
可以用系統提供的工具查看共用庫的頭:
readelf -d libhello.so.0.0.0 | grep libhello
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.
查看編譯出來的程式:
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
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 可以查看動態庫的資訊,比如依賴的庫,本身的somae。
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