Linux 如何解決共享庫的版本控制

查志強發表於2014-06-21

換句話說,soname不是真實存在的檔案,只是在此庫中和將來呼叫此庫的檔案中儲存的一個名字,在載入是去找這個名字,使用時建立一個軟連線來指向真實檔案,這樣真實檔案的版本號就可以升級了

 

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


相關文章