linux的共享庫(動態鏈接庫)

 

Linux共享庫

原文:http://www.linux.org/docs/ldp/howto/Program-Library-HOWTO/shared-libraries.html
一個完整的教程,包括靜態庫、動態庫、動態加載,
http://www.yolinux.com/TUTORIALS/LibraryArchives-StaticAndDynamic.html

1.共享庫
共享庫指的是一種在程序啓動時被載入的庫。當一個共享庫被正確安裝後,在此之後啓動的程序會自動的使用最新版的庫。事實上它還具有更高的複雜性和靈活性,Linux系統通過共享庫的方式允許你完成以下任務:

  • 更新庫並且保持哪些需要使用舊版甚至是不能向後兼容的庫的程序繼續使用舊版庫。

  • 執行個別程序時只覆蓋指定的庫甚至只覆蓋一個庫裏的部分函數。

  • 當庫還在被使用時也能完成上述任務。


1.1 約定

要使共享庫實現上述的幾項特性,需要遵守一些約定。你必須搞明白幾個不同的庫名稱之間的區別,特別是“soname”和“real name”(以及怎樣相互影響)。另外你還必須明白他們放在整個文件系統的什麼地方。

1.1.1 共享庫的名字們

每個共享庫都有一個特別的名字叫“soname”soname由前綴‘lib’、庫名稱、’.so’後面再跟一個點號和一個只要接口有變化就會遞增的數字版本號(有一個例外,最低層的C庫並不以前綴 lib開頭)。一個全規格的soname包括一個它所在的路徑名做的前綴;(這句可能翻譯的有問題,原文是:A fully-qualified soname includes as a prefix the directory it’s in)在一個可運行的系統上一個全規格的soname只是簡單的做城一個指向共享庫真實名字(real name)的符號鏈接。

每個共享庫還有一個真實名(real name),就是包含實際庫代碼的文件的文件名。真實名(real name)是在soname名後加一個點後跟一個子版本號然後再跟一個點再跟一個發佈編號(release number)。最後一個點號和發佈編號(release number)是可選的。子版本號和發佈編號提供了讓你明確知道被安裝的是那個版本來設置配置文件。注意這些數字編號不是必須和庫文檔裏描述的一致,當然保持一致會讓事情跟簡單些。

另外,當編譯器鏈接庫時用到的名字(我稱之爲鏈接名(linker name)),只是簡單的不帶任何數字版本號的soname

管理共享庫的關鍵就是分離好這些名字。程序在內部列出他們需要的共享庫時,只應該使用soname;相反,當你創建一個共享庫時,你只應該使用特別的文件名(帶有版本信息細節的)。當你安裝一個庫的新版本時,你把它裝到幾個特別目錄中的一個然後運行 ldconfig(8)ldconfig檢查已經存在的文件並創建soname作爲符號鏈接指向真實文件名,隨後設置緩衝文件 /etc/ld.so.cache(後文解釋)。

ldconfig並不設置鏈接名(linker name)。典型的這個工作是在安裝庫時來完成,並且鏈接名只是簡單的創建爲指向soname或真實名(real name)的符號鏈接文件,我建議是指向soname,因爲大多數情況下你總是希望鏈接時自動鏈接到升級後的庫文件,我曾經諮詢過H.J.Lu爲什麼ldconfig不設置鏈接名(linker name),他的解釋是典型情況下是想讓程序鏈接到最新的庫,但是也可能在開發環境下你想讓他鏈接到一個老(可能都是不兼容)的版本。因此,ldconfig並不臆測你的程序到底要鏈接那個版本,所創建鏈接名的工作是安裝程序的責任。

因此,/usr/lib/libreadline.so.3是一個全規格的soname,這是由ldconfig設置的,指向某個真實名諸如/usr/lib/libreadline.so.3.0,另外還應該有一個鏈接名,/usr/lib/libreadline.so ,它指向 /usr/lib/libreadline.so.3 .

1.1.2 位置
共享庫必須放在文件系統中某個位置。大多數開源軟件傾向與遵從GNU標準,更詳細的信息請參考info文檔:info:standands#Directroy Variables.GNU標準建議當發佈源碼時所有的庫缺省都裝到/usr/local/lib下(所有的命令文件都裝到/usr/local/bin下)。他們也爲安裝程序打算覆蓋這些缺省行爲時定義了一些規則。

分層文件系統(FHS)討論了分發時什麼東西應該放在什麼地方(參見http://www.pathname.com/fhs)。依照FHS,大多數庫應該放在/usr/lib下,但是系統啓動時需要的庫應該放在/lib下,而哪些不是系統部分的庫應該放在 /usr/local/lib

這兩個文檔之間其實並沒有衝突。GNU討論的是源代碼,FHS討論的是發佈版的缺省行爲(誰會選擇覆蓋缺省的源代碼,這通常是系統管理包做的事)。在實踐中這樣的方式工作的非常好,你下載的“最後的(可能是有BUG的)”源碼通常把自己安裝的local下(/usr/local)。一旦代碼成熟後,包管理器就可以把它分發到標準的位置通常就是覆蓋缺省的了。如果你的庫要調用的程序是一個只能通過庫才能調用的程序,你應該把它放到/usr/local/libexec(正式發佈時變爲/usr/libexec)。一個麻煩的情況是Red-Hat家族的系統沒有把/usr/local/lib作爲搜索庫的缺省路徑,關於這個問題輕看下面關於/etc/ld.so.conf的論述。另外標準庫路徑還應該包括針對X-Window/usr/X11R6/lib。還有 /lib/security 是專供 PAM模塊的,不過他們通常都是通過DL庫來加載的(也參看下面的討論)。


1.2 庫是怎麼樣被使用的
在基於GNU glibc庫的系統上,包括所有的linux系統,啓動一個ELF格式的二進制可執行文件會導致一個程序加載器自動加載並運行。在linux系統上,這個加載器的名字是/lib/ld-linux.so.X(X是版本號數字)。這個加載器負責找到並加載這個程序所需要的共享庫。

/etc/ld.so.conf文件中保存這搜索目錄的列表。許多Red-Hat家族的系統在/etc/ld.so.conf文件裏並沒有包含/usr/local/lib目錄,我認爲這是一個BUG,在/etc/ld.so.conf文件里加上/usr/local/lib 目錄可以解決很多程序在Red-Hat家族系統上運行的問題。

如果你只是想更新一個庫裏的少數幾個函數,但是要保留庫裏的其他函數,你可以在/etc/ld.so.preload 文件裏輸入這些覆蓋庫的名字(.o文件),這些“預加載”庫比標準庫集有更高的優先權。這些預加載文件通常用於緊急補丁,而發佈版分發時通常並不包含這類文件。

每次程序啓動時都搜索這些目錄會導致嚴重的效率問題,所以通常會使用緩衝。程序ldconfig(8) 通常用來讀取/etc/ld.so.conf文件,在動態鏈接庫的目錄裏設置正確的符號鏈接(所以他們遵守標準規範),然後寫一個緩衝到/etc/ld.so.cache文件中以供其他程序使用。這樣大大提高裏讀取庫的速度。這裏暗示出無論是加入一個DLL,刪除一個DLL或者是DLL目錄集發生變化是都必須運行ldconfig。運行ldconfig通常是包管理器在安裝一個庫時的一個步驟。這樣在程序啓動時,動態加載實際上是使用了/etc/ld.so.cache然後再加載它需要的庫。

順便說下,在FreeBSD系統裏這個緩衝文件用了一個不同的名字。在FreeBSD中,ELF的緩衝是/var/run/ld-elf.so.hintsa.out的緩衝是/var/run/ld.so.hints。它們也還是通過ldconfig(8)來配置的,所以這個文件名不同的問題只有在極個別的情況下才需要考慮。

1.3 環境變量
有幾個環境變量可以控制整個過程。還有一些環境變量允許你重新定義這些過程。

1.3.1 LD_LIBRARY_PATH
你可以爲本次執行臨時指定一個不同的庫。在Linux裏,環境變量LD_LIBRARY_PATH是一個用冒號分隔的目錄集,這個目錄集在標準庫目錄之前被搜索。這對於調試一個新庫和出於特殊目的使用非標準庫來說非常有用。環境變量LD_PRELOAD列出那些要替換標準庫裏的部分函數的庫,就是/etc/ld.so.reload做的事。所有這些都由加載器/lib/ld-linux.so實現。另外我注意到,LD_LIBRARY_PATH在大多數類Unix系統上都能工作,但並不是所有的。舉個例子,在HP-UX 系統上有這個功能但是環境變量叫SH_LIBPATH,而在AIX上是通過環境變量LIBPATH(語法相同,冒號分隔目錄列表)來實現的。

LD_LIBRARY_PATH對於開發和測試來說是非常方便,但是不要在安裝程序裏爲普通用戶做這樣的修改。請參看 http://www.visi.com/~barr/ldpath.html 解釋了爲什麼使用LD_LIBRARY_PATH是糟糕的。但是對於開發和測試來說它還是非常有用的,而且可以工作在那些在相反的環境下不能工作的問題。如果你不想使用環境變量LD_LIBRARY_PATH,在Linux 裏你甚至可以通過直接帶參數的調用程序加載器。比如,給出一個PATH來代替環境變量LD_LIBRARY_PATH然後運行給定的程序:
/lib/ld-linux.so.2 --library-path PATH EXECUTABLE
直接不帶參數執行ld-linux.so.2會告訴你更多的用法。不過再次聲明,在正常使用環境下不要這樣使用,這只是爲調試而使用的。

1.3.2 LD_DEBUG
GNU C加載器裏還有一個非常有用的環境變量就是 LD_DEBUG,它會觸發所有的dl*函數輸出詳細信息報告它正在做什麼。例如:
export LD_DEBUG = files
command_to_run
當加載這些文件和庫的時候,會顯示給你檢測到那些依賴文件以及以什麼樣的順序加載了那個SO文件。設置LD_DEBUG爲 “bindings”會顯示符號綁定信息,把它設成“libs”會顯示庫搜索路徑,設成“versions"會顯示版本依賴關係。

LD_DEBUG設成“help”,然後運行一個程序,會列出LD_DEBUG可能的選項。再次強調,LD_DEBUG不是給通常情況下使用的,但是調試和測試是非常有用的。

1.3.3 其他環境變量
實際上還有一些環境變量可以控制加載過程。它們都是以LD_RTLD_開頭,這些都是爲了加載過程的低層調試或實現一些特殊的功能。它們大都沒有正式文檔,如果你想要了解它們,最好的方法是閱讀加載器的源代碼(在gcc的代碼裏)。

允許用戶重載動態鏈接庫的加載過程但沒有實現一些特殊的措施這對setuid/setgid程序會是災難性的。因此,在GNU加載器裏(在程序啓動是加載程序的其餘部分),如果程序是setuid setgid那這些環境變量(或者類似的變量)將被忽略或它們能做的事情會有很大的限制。加載器通過檢查程序證書來確定它們是setuid還是setgid,如果uideuid不同,或者gidegid不同,加載器會假定這個程序是setuidsetgid(或是繼承於它們的)那就會對環境變量的控制鏈接的能做出很大的限制。如果你閱讀了GNU glibc的庫源代碼,你就會看到這些限制。查看elf/rtld.csysdeps/generic/dl-sysdep.c。換句話說,如果你讓giduid等於egideuid,然後再運行程序,這些環境變量全部會生效。另外一些類Unix系統採取稍有不同的方法但都爲了同一個目的:一個setgid/setuid程序不應該受環境變量的影響。

1.4 創建共享庫
創建一個共享庫是很容易的。首先,用gcc -fPIC -fpic參數編譯源文件生成那些要放入共享庫裏的目標文件。-fPIC -fpic參數用來允許生成”位置無關代碼“(position independent code )。對於共享庫來說”位置無關代碼“是必須的。下文會解釋這有什麼不同。用gcc-Wl 選項傳遞soname-Wl選項用來向鏈接器傳遞跟在它後面的鏈接選項(在這個例子裏就是 -soname連接器選項),注意 -Wl後面的逗號並不是筆誤,並且選項裏不能包含空格。創建共享庫用如下的格式:
gcc -shared -Wl,-soname,your_soname /
   -o
library_name file_list library_list

這兒有個例子,創建兩個目標文件(a.o b.o),然後創建共享庫包含它們兩個。注意這裏的編譯包括創建調試信息(-g)並且會警告信息全開(-Wall),這對共享庫來說並不需要但我們還是建議加上,編譯器生成目標文件(-c)並且包括必須的參數 -fPIC

gcc -fPIC -g -c -Wall a.c
gcc -fPIC -g -c -Wall b.c gcc -shared -Wl,-soname,libmystuff.so.1 /    -o libmystuff.so.1.0.1 a.o b.o -lc



這裏有幾個要點要注意:

  • Don't strip the resulting library,並且不要使用編譯選項 -fomit-frame-pointer除非你真的必須這樣做,這樣做生成的庫可以工作,但是調試器將對他無能爲力。

 

  • 要用-fPIC -fpic來生成代碼。無論是用-fPIC 還是 -fpic生成的代碼都是目標依賴的。-fPIC總是可以工作的,只是產生出來的代碼比-fpic稍大點,(記住這個很容易,PIC是大寫的,所以產生的代碼就大一點),-fpic生成的代碼更小一點更快一點,但是它是平臺依賴的,比如全局可見符號的數量和代碼的尺寸。當你創建共享庫時連接器會告訴你這個選項是否合適。在不確定的情況下,我總是用-fPIC,因爲它總是可以工作 。

 

  • 在某些情況下,調用gcc創建目標文件時還需要加上一個選項-Wl,-export-dynamic。通常,動態符號表只包括那些被動態目標使用到的符號。使用這個選項當創建ELF文件時會把所有的符號都加入動態符號表裏。(查看ld(1)可以獲得更多的信息)。當存在反向依賴關係時你需要使用這個選項,也就是說當加載一個庫時存在一個未決符號並且按約定它是由加載這個庫的程序來定義的。爲了使反向依賴關係能夠工作,主程序必須讓它的符號也是動態的。注意如果你只工作在linux系統上你可以使用-rdynamic來代替-Wl,-export-dynamic,但是根據ELF文檔的說明,在非linux系統的gcc-rdynamic不一定能正常工作。



在開發過程中,在修改一個同時被很多其他程序在使用的庫時還有一些潛在的問題,並且你不想讓其他程序使用這個開發版的庫,只有你在測試的程序可以使用它。有一個鏈接選項你可以用就是ld的”rpath”,這是在編譯主程序時來設定動態庫的搜索路徑的,對於gcc你可以這樣做:
-Wl,-rpath,$(DEFAULT_LIB_INSTALL_PATH)
如果你在生成庫的客戶端程序時使用這個參數,就不必再爲設置LD_LIBRARY_PATH時要確保不要產生衝突而煩惱,也不必考慮用其他手段來隱藏庫文件。

1.5 安裝和使用共享庫
一旦你創建好了庫,就要考慮安裝它了。最簡單的方法就是簡單的把它拷貝到標準庫目錄下比如/usr/lib,然後運行idconfig(8)
首先你需要在某處創建共享庫,然後你需要設置一些必要的符號鏈接,特別是soname到真實名(real name)的鏈接(最好是從一個無版本號的soname就是一個以.so結尾的soname,給那些對版本沒有特別要求的用戶),最簡單的方法就是運行:

ldconfig -n directory_with_shared_libraries



最後,當你編譯程序時,你要告訴連接器任何你需要的靜態和動態鏈接庫,用 -l -L 選項來完成這個工作。

如果你不想把庫安裝在標準目錄裏,或者你沒有權利修改/usr/lib,那麼你就要用另外的方法了。在這中情況下,你需要把它安裝到某處,然後給你的程序提供足夠的信息讓它能找到庫。這兒有幾種辦法來做這件事。在簡單情況下你可以使用 gcc -L。如果你只有一個特別的程序要用到你這個放在非標準目錄裏的庫你也可以用 rpath 的方法(上文講到過的)。你還可以使用環境變量來控制這件事,特別是你可以用LD_LIBRARY_PATH,就是由冒號分隔的目錄列表來指明在標準目錄之前先搜索哪些目錄。如果你用bash,你可以象這樣來調用my_program

LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH  my_program



如果你只是想覆蓋幾個函數,你可以創建一個包含這幾個新函數的目標文件,然後設置LD_PRELOAD來實現它,這個目標文件裏的函數只是覆蓋了原來的庫裏的函數,而其他函數繼續使用。

通常你可以毫無顧慮的更新一個庫。如果一個接口發生了改變,庫的開發者通常會改變soname。這樣,多個版本的庫可以並存在同一個系統裏,並且每個程序都會鏈接到正確的庫上。然而,如果一個相同soname的升級庫導致一個程序不能運行,你可以強制它使用老版本庫,只要把老ia版本的庫拷到某個地方,把這個不能運行的程序改個名(給它加個.orig表示它是老的),然後你創建一個包裝(Wrapper)腳本用來重置要用的庫並調用改過名後的老程序,你可以把這個老版本的庫放到某個特殊的區域,如果你喜歡,你可以通過數字編號的方法讓多個版本的庫在同一個目錄裏。這個包裝腳本看上去象下面這樣:

  #!/bin/sh
  export LD_LIBRARY_PATH=/usr/local/my_lib:$LD_LIBRARY_PATH
  exec /usr/bin/my_program.orig $*

當你寫自己的程序時請別使用這種方法;請儘量確保你的庫是向後兼容的或者當有不兼容的改變時請遞增你的soname的版本號部分。這個方法只是爲了解決一些很糟糕的問題的應急辦法。

 

ldd(1)程序可以列出一個程序使用了哪些共享庫。舉個例子,你可以鍵入下面命令來看 ls 命令都使用了哪些共享庫。

 

ldd /bin/ls

 

通常你可以看到一個被依賴的soname的列表,每個soname後跟一個它實際指向的目錄,特別的在所有的情況下你都至少可以看到下面兩條:

  • /lib/ld-linux.so.N(這裏N1或以上的數字,通常至少是2),這是一個加載其他所有庫的庫。

  • libc.so.N(這裏N6或更高),這是C庫,甚至其他語言也傾向於使用C庫(至少在實現它們自己的庫時),所以大多數程序至少包含這個。

 

注意:不要在你不信任的程序上運行ldd,在ldd手冊裏有明確的說明,對於ELF對象ldd是通過設置一個特殊的環境變量LD_TRACE_LOADED_OBJECT然後運行這個程序,這使得這個不受信任的程序有可能可以執行任意代碼(取代簡單的輸出一些ldd信息的代碼) 。因此,出於安全的角度,不要用ldd來檢查一個不信任的程序。

  1.  

     

1.6不兼容庫

當一個庫和舊版的存在着二進制級的不兼容就需要改變它的soname,在C裏,有四個原因會產生二進制兼容性問題:

  1. 函數的行爲發生了變化因此它的地址會發生變化

  2. 引出的數據成員發生裏變化(有個例外,如果結構只是在庫內部分配內存,那在結構的尾部添加成員是可以的)

  3. 一個引出的函數被刪除了

  4. 一個引出的函數的接口發生了變化。

如果你避免了這些情況,你就可以保持你的庫是二進制兼容的。換句話說,如果你避免了上述問題,你的庫就是ABIApplication Binary Interface)兼容的。比如,你可以加一個新函數,但是並不刪除舊函數。你可以給結構添加成員,但是你必須確信舊的客戶程序對結構成員的變化並不敏感並且只能在尾部添加成員,只有庫(而不是客戶程序)才能爲結構分配內存,添加的成員對客戶程序來說可以忽略的(或者由庫來操縱它)。注意,如果客戶把結構用在了數組裏,那你不能擴展這個結構。

對於C++(包括那些支持模板或遲後編連技術的語言),情況就變得複雜的多了。上面的四條要遵循外,還有很多其他注意事項。所有這些要注意的就一個原因就是編譯後的代碼裏由很多隱性代碼,如果你不瞭解典型的C++實現方法那這些相互依賴關係不是那麼顯而易見的。嚴格的說,它們並不是什麼新問題,它只是C++編譯後的調用代碼可能會讓你很吃驚。下面這張列表(可能還不完整)列出了要保持二進制兼容在C++裏你不能做的事,摘自 Troll Tech's Technical FAQ :

  1. 在虛函數裏添加執行父類虛函數的調用(除非調用舊的實現時對舊的二進制級是安全的),因爲編譯器是在編譯時(而非鏈接時)計算SuperClass::VirtualFunction的地址的。

  2. 添加或刪除虛函數,因爲這會改變每一個子類的虛表(vtbl)的大小和佈局。

  3. 改變那些能被inline函數訪問的數據成員的數據類型或刪除這些數據成員。

  4. 改變類的繼承關係,除非是添加新的葉子類(指最底層的子類)。

  5. 添加或刪除私有數據成員,因爲這會改變每個子類的大小和佈局。

  6. 刪除公共或保護的成員函數除非它們是inline的。

  7. 把公共和保護的成員函數改成inline的。

  8. 改變inline函數做的事,除非老版的仍可以工作。

  9. 在一個跨平臺的程序裏改變一個成員函數的存取權限(公共、保護、私有),因爲有些編譯器會把存取權限添加到函數名裏。

 

有鑑於這張長列表,用C++開發庫需要非常仔細的計劃否則可能造成庫的兼容性問題。所幸,在類Unix系統,包括linux你可以同一時刻裝載多個版本的庫,只要你的磁盤空間夠,那些需要舊版庫的舊程序都能運行。

 

 

 

 

 

 

 

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