Linux共享庫、靜態庫、動態庫詳解

rainbow70626發表於2024-04-20

1. 介紹

使用GNU的工具我們如何在Linux下建立自己的程式函式庫?一個“程式函式庫”簡單的說就是一個檔案包含了一些編譯好的程式碼和資料,這些編譯好的程式碼和資料可以在事後供其他的程式使用。程式函式庫可以使整個程式更加模組化,更容易重新編譯,而且更方便升級。

程式函式庫可分為3種型別:靜態函式庫(static libraries)、共享函式庫(shared libraries)、動態載入函式庫(dynamically loaded libraries):

1、靜態函式庫,是在程式執行前就加入到目標程式中去了 ;

2、動態函式庫同共享函式庫是一個東西(在linux上叫共享物件庫, 檔案字尾是.so ,windows上叫動態載入函式庫, 檔案字尾是.dll)

Linux中命名系統中共享庫的規則:

2. 靜態函式庫

靜態函式庫實際上就是簡單的一個普通的目標檔案的集合,一般來說習慣用“.a”作為檔案的字尾。可以用ar這個程式來產生靜態函式庫檔案。Ar是archiver的縮寫。靜態函式庫現在已經不在像以前用得那麼多了,主要是共享函式庫與之相比較有很多的優勢的原因。慢慢地,大家都喜歡使用共享函式庫了。不過,在一些場所靜態函式庫仍然在使用,一來是保持一些與以前某些程式的相容,二來它描述起來也比較簡單。

靜態庫函式允許程式設計師把程式link起來而不用重新編譯程式碼,節省了重新編譯程式碼的時間。不過,在今天這麼快速的計算機面前,一般的程式的重新編譯也花費不了多少時間,所以這個優勢已經不是像它以前那麼明顯了。靜態函式庫對開發者來說還是很有用的,例如你想把自己提供的函式給別人使用,但是又想對函式的原始碼進行保密,你就可以給別人提供一個靜態函式庫檔案。理論上說,使用ELF格式的靜態庫函式生成的程式碼可以比使用共享函式庫(或者動態函式庫)的程式執行速度上快一些,大概1-5%。

建立一個靜態函式庫檔案,或者往一個已經存在地靜態函式庫檔案新增新的目的碼,可以用下面的命令:

ar rcs my_library.a file1.o file2.o

這個例子中是把目的碼file1.o和file2.o加入到my_library.a這個函式庫檔案中,如果my_library.a不存在則建立一個新的檔案。在用ar命令建立靜態庫函式的時候,還有其他一些可以選擇的引數,可以參加ar的使用幫助。這裡不再贅述。

一旦你建立了一個靜態函式庫,你可以使用它了。你可以把它作為你編譯和連線過程中的一部分用來生成你的可執行程式碼。如果你用gcc來編譯產生可執行程式碼的話,你可以用“-l”引數來指定這個庫函式。你也可以用ld來做,使用它的“-l”和“-L”引數選項。具體用法可以參考info:gcc。

3. 共享函式庫

共享函式庫中的函式是在當一個可執行程式在啟動的時候被載入。如果一個共享函式庫正常安裝,所有的程式在重新執行的時候都可以自動載入最新的函式庫中的函式。對於Linux系統還有更多可以實現的功能:
1、升級了函式庫但是仍然允許程式使用老版本的函式庫。
2、當執行某個特定程式的時候可以覆蓋某個特定的庫或者庫中指定的函式。
3、可以在庫函式被使用的過程中修改這些函式庫。

3.1. 一些約定
如果你要編寫的共享函式庫支援所有有用的特性,你在編寫的過程中必須遵循一系列約定。你必須理解庫的不同的名字間的區別,例如它的“soname”和“real name”之間的區別和它們是如何相互作用的。你同樣還要知道你應該把這些庫函式放在你檔案系統的什麼位置等等。下面我們具體看看這些問題。

3.1.1. 共享庫的命名

每個共享函式庫都有個特殊的名字,稱作“soname”。soname名字命名必須以“lib”作為字首,然後是函式庫的名字,然後是“.so”,最後是版本號資訊。不過有個特例,就是非常底層的C庫函式都不是以lib開頭這樣命名的。
每個共享函式庫都有一個真正的名字(“real name”),它是包含真正庫函式程式碼的檔案。真名有一個主版本號,和一個發行版本號。最後一個發行版本號是可選的,可以沒有。主版本號和發行版本號使你可以知道你到底是安裝了什麼版本的庫函式。另外,還有一個名字是編譯器編譯的時候需要的函式庫的名字,這個名字就是簡單的soname名字,而不包含任何版本號資訊。

管理共享函式庫的關鍵是區分好這些名字。當可執行程式需要在自己的程式中列出這些他們需要的共享庫函式的時候,它只要用soname就可以了;反過來,當你要建立一個新的共享函式庫的時候,你要指定一個特定的檔名,其中包含很細節的版本資訊。當你安裝一個新版本的函式庫的時候,你只要先將這些函式庫檔案複製到一些特定的目錄中,執行ldconfig這個實用就可以。ldconfig檢查已經存在的庫檔案,然後建立soname的符號連結到真正的函式庫,同時設定/etc/ld.so.cache這個緩衝檔案。這個我們稍後再討論。

ldconfig並不設定連結的名字,通常的做法是在安裝過程中完成這個連結名字的建立,一般來說這個符號連結就簡單的指向最新的soname或者最新版本的函式庫檔案。最好把這個符號連結指向soname,因為通常當你升級你的庫函式後,你就可以自動使用新版本的函式庫類。

我們來舉例看看:/usr/lib/libreadline.so.3 是一個完全的完整的soname,ldconfig可以設定一個符號連結到其他某個真正的函式庫檔案,例如是/usr/lib/libreadline.so.3.0。同時還必須有一個連結名字,例如 /usr/lib/libreadline.so就是一個符號連結指向/usr/lib/libreadline.so.3。

3.1.2. 檔案系統中函式庫檔案的位置

共享函式庫檔案必須放在一些特定的目錄裡,這樣透過系統的環境變數設定,應用程式才能正確的使用這些函式庫。大部分的原始碼開發的程式都遵循GNU的一些標準,我們可以看info幫助檔案獲得相信的說明,info資訊的位置是:info:standards#Directory_Variables。GNU標準建議所有的函式庫檔案都放在/usr/local/lib目錄下,而且建議命令可執行程式都放在/usr/local/bin目錄下。這都是一些習慣問題,可以改變的。

檔案系統層次化標準FHS(Filesystem Hierarchy Standard)(http://www.pathname.com/fhs)規定了在一個發行包中大部分的函式庫檔案應該安裝到/usr/lib目錄下,但是如果某些庫是在系統啟動的時候要載入的,則放到/lib目錄下,而那些不是系統本身一部分的庫則放到/usr/local/lib下面。

上面兩個路徑的不同並沒有本質的衝突。GNU提出的標準主要對於開發者開發原始碼的,而FHS的建議則是針對發行版本的路徑的。具體的位置資訊可以看/etc/ld.so.conf裡面的配置資訊。

3.2. 這些函式庫如何使用

在基於GNU glibc的系統裡,包括所有的linux系統,啟動一個ELF格式的二進位制可執行檔案會自動啟動和執行一個program loader。對於Linux系統,這個loader的名字是/lib/ld-linux.so.X(X是版本號)。這個loader啟動後,反過來就會load所有的其他本程式要使用的共享函式庫。

到底在哪些目錄裡查詢共享函式庫呢?這些定義預設的是放在/etc/ld.so.conf檔案裡面,我們可以修改這個檔案,加入我們自己的一些特殊的路徑要求。大多數RedHat系列的發行包的/etc/ld.so.conf檔案裡面不包括/usr/local/lib這個目錄,如果沒有這個目錄的話,我們可以修改/etc/ld.so.conf,自己手動加上這個條目。

如果你想覆蓋某個庫中的一些函式,用自己的函式替換它們,同時保留該庫中其他的函式的話,你可以在 /etc/ld.so.preload中加入你想要替換的庫(.o結尾的檔案),這些preloading的庫函式將有優先載入的權利。

當程式啟動的時候搜尋所有的目錄顯然會效率很低,於是Linux系統實際上用的是一個高速緩衝的做法。ldconfig預設情況下讀出/etc/ld.so.conf相關資訊,然後設定適當地符號連結,然後寫一個cache到 /etc/ld.so.cache這個檔案中,而這個/etc/ld.so.cache則可以被其他程式有效的使用了。這樣的做法可以大大提高訪問函式庫的速度。這就要求每次新增加一個動態載入的函式庫的時候,就要執行ldconfig來更新這個cache,如果要刪除某個函式庫,或者某個函式庫的路徑修改了,都要重新執行ldconfig來更新這個cache。通常的一些包管理器在安裝一個新的函式庫的時候就要執行ldconfig。

另外,FreeBSD使用cache的檔案不一樣。FreeBSD的ELF cache是/var/run/ld-elf.so.hints,而a.out的cache則是/var/run/ld.so.hints。它們同樣是透過ldconfig來更新。

3.3. 環境變數

各種各樣的環境變數控制著一些關鍵的過程。例如你可以臨時為你特定的程式的一次執行指定一個不同的函式庫。Linux系統中,通常變數LD_LIBRARY_PATH就是可以用來指定函式庫查詢路徑的,而且這個路徑通常是在查詢標準的路徑之前查詢。這個是很有用的,特別是在除錯一個新的函式庫的時候,或者在特殊的場合使用一個非標準的函式庫的時候。環境變數LD_PRELOAD列出了所有共享函式庫中需要優先載入的庫檔案,功能和/etc/ld.so.preload類似。這些都是有/lib/ld-linux.so這個loader來實現的。值得一提的是,LD_LIBRARY_PATH可以在大部分的UNIX-linke系統下正常起作用,但是並非所有的系統下都可以使用,例如HP-UX系統下,就是用SHLIB_PATH這個變數,而在AIX下則使用LIBPATH這個變數。

LD_LIBRARY_PATH在開發和除錯過程中經常大量使用,但是不應該被一個普通使用者在安裝過程中被安裝程式修改,大家可以去參考http://www.visi.com/~barr/ldpath.html,這裡有一個文件專門介紹為什麼不使用LD_LIBRARY_PATH這個變數。

事實上還有更多的環境變數影響著程式的調入過程,它們的名字通常就是以LD_或者RTLD_打頭。大部分這些環境變數的使用的文件都是不全,通常搞得人頭昏眼花的,如果要真正弄清楚它們的用法,最好去讀loader的原始碼(也就是gcc的一部分)。

允許使用者控制動態連結函式庫將涉及到setuid/setgid這個函式,如果特殊的功能需要的話。因此,GNU loader通常限制或者忽略使用者對這些變數使用setuid和setgid。如果loader透過判斷程式的相關環境變數判斷程式的是否使用了setuid或者setgid,如果uid和euid不同,或者gid和egid部一樣,那麼loader就假定程式已經使用了setuid或者setgid,然後就大大的限制器控制這個老連結的許可權。如果閱讀GNU glibc的庫函式原始碼,就可以清楚地看到這一點。特別的我們可以看elf/rtld.c和sysdeps/generic/dl-sysdep.c這兩個檔案。這就意味著如果你使得uid和gid與euid和egid分別相等,然後呼叫一個程式,那麼這些變數就可以完全起效。

3.4. 建立一個共享函式庫

現在我們開始學習如何建立一個共享函式庫。其實建立一個共享函式庫非常容易。首先建立object檔案,這個檔案將加入透過gcc –fPIC引數命令加入到共享函式庫裡面。PIC的意思是“位置無關程式碼”(Position Independent Code)。下面是一個標準的格式:

gcc -shared -Wl,-soname,your_soname -o library_name file_list library_list

下面再給一個例子,它建立兩個object檔案(a.o和b.o),然後建立一個包含a.o和b.o的共享函式庫。例子中”-g”和“-Wall”引數不是必須的。

gcc -fPIC -g -c -Wall a.c

gcc -fPIC -g -c -Wall b.c

gcc -shared -Wl,-soname,liblusterstuff.so.1 -o liblusterstuff.so.1.0.1 a.o b.o -lc

下面是一些需要注意的地方:

不用使用-fomit-frame-pointer這個編譯引數除非你不得不這樣。雖然使用了這個引數獲得的函式庫仍然可以使用,但是這使得除錯程式幾乎沒有用,無法跟蹤除錯。

使用-fPIC來產生程式碼,而不是-fpic。

某些情況下,使用gcc 來生成object檔案,需要使用“-Wl,-export-dynamic”這個選項引數。

通常,動態函式庫的符號表裡面包含了這些動態的物件的符號。這個選項在建立ELF格式的檔案時候,會將所有的符號加入到動態符號表中。可以參考ld的幫助獲得更詳細的說明。

3.5. 安裝和使用共享函式庫

一旦你定義了一個共享函式庫,你還需要安裝它。其實簡單的方法就是複製你的庫檔案到指定的標準的目錄(例如/usr/lib),然後執行ldconfig。

如果你沒有許可權去做這件事情,例如你不能修改/usr/lib目錄,那麼你就只好透過修改你的環境變數來實現這些函式庫的使用了。首先,你需要建立這些共享函式庫;然後,設定一些必須得符號連結,特別是從soname到真正的函式庫檔案的符號連結,簡單的方法就是執行ldconfig:

ldconfig -n directory_with_shared_libraries
然後你就可以設定你的LD_LIBRARY_PATH這個環境變數,它是一個以逗號分隔的路徑的集合,這個可以用來指明共享函式庫的搜尋路徑。例如,使用bash,就可以這樣來啟動一個程式my_program:

LD_LIBRARY_PATH=$LD_LIBRARY_PATH my_program

如果你需要的是過載部分函式,則你就需要建立一個包含需要過載的函式的object檔案,然後設定LD_PRELOAD環境變數。

通常你可以很方便的升級你的函式庫,如果某個API改變了,建立庫的程式會改變soname。然而,如果一個函式升級了某個函式庫而保持了原來的soname,你可以強行將老版本的函式庫複製到某個位置,然後重新命名這個檔案(例如使用原來的名字,然後後面加.orig字尾),然後建立一個小的“wrapper”指令碼來設定這個庫函式和相關的東西。例如下面的例子:

#!/bin/sh export LD_LIBRARY_PATH=/usr/local/my_lib,$LD_LIBRARY_PATH

exec /usr/bin/my_program.orig $*

我們可以透過執行ldd來看某個程式使用的共享函式庫。例如你可以看ls這個實用工具使用的函式庫:

ldd /bin/ls

libtermcap.so.2 => /lib/libtermcap.so.2 (0x4001c000)

libc.so.6 => /lib/libc.so.6 (0x40020000)

/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
通常我麼可以看到一個soname的列表,包括路徑。在所有的情況下,你都至少可以看到兩個庫:

· /lib/ld-linux.so.N(N是1或者更大,一般至少2)。這是這個用於載入其他所有的共享庫的庫。

· libc.so.N(N應該大於或者等於6)。這是C語言函式庫。

值得一提的是,不要在對你不信任的程式執行ldd命令。在ldd的manual裡面寫得很清楚,ldd是透過設定某些特殊的環境變數(例如,對於ELF物件,設定LD_TRACE_LOADED_OBJECTS),然後執行這個程式。這樣就有可能使得某地程式可能使得ldd來執行某些意想不到的程式碼,而產生不安全的隱患。

3.6. 不相容的函式庫

如果一個新版的函式庫要和老版本的二進位制的庫不相容,則soname需要改變。對於C語言,一共有4個基本的理由使得它們在二進位制程式碼上很難相容:

一個函式的行文改變了,這樣它就可能與最開始的定義不相符合。

· 輸出的資料項改變了。

· 某些輸出的函式刪除了。

· 某些輸出函式的介面改變了。
如果你能避免這些地方,你就可以保持你的函式庫在二進位制程式碼上的相容,或者說,你可以使得你的程式的應用二進位制介面(ABI:Application Binary Interface)上相容。

4. 動態載入的函式庫Dynamically Loaded (DL) Libraries

動態載入的函式庫Dynamically loaded (DL) libraries是一類函式庫,它可以在程式執行過程中的任何時間載入。它們特別適合在函式中載入一些模組和plugin擴充套件模組的場合,因為它可以在當程式需要某個plugin模組時才動態的載入。例如,Pluggable Authentication Modules(PAM)系統就是用動態載入函式庫來使得管理員可以配置和重新配置身份驗證資訊。

Linux系統下,DL函式庫與其他函式庫在格式上沒有特殊的區別,我們前面提到過,它們建立的時候是標準的object格式。主要的區別就是這些函式庫不是在程式連結的時候或者啟動的時候載入,而是透過一個API來開啟一個函式庫,尋找符號表,處理錯誤和關閉函式庫。通常C語言環境下,需要包含這個標頭檔案。
Linux中使用的函式和Solaris中一樣,都是dlpoen() API。當然不是所有的平臺都使用同樣的介面,例如HP-UX使用shl_load()機制,而Windows平臺用另外的其他的呼叫介面。如果你的目的是使得你的程式碼有很強的移植性,你應該使用一些wrapping函式庫,這樣的wrapping函式庫隱藏不同的平臺的介面區別。一種方法是使用glibc函式庫中的對動態載入模組的支援,它使用一些潛在的動態載入函式庫介面使得它們可以誇平臺使用。具體可以參考http://developer.gnome.org/doc/API/glib/glib-dynamic-loading-of-modules.html. 另外一個方法是使用libltdl,是GNU libtool的一部分,可以進一步參考CORBA相關資料。

4.1. dlopen()
dlopen函式開啟一個函式庫然後為後面的使用做準備。C語言原形是:

void * dlopen(const char *filename, int flag);

如果檔名filename是以“/”開頭,也就是使用絕對路徑,那麼dlopne就直接使用它,而不去查詢某些環境變數或者系統設定的函式庫所在的目錄了。否則dlopen()就會按照下面的次序查詢函式庫檔案:
1. 環境變數LD_LIBRARY指明的路徑。

2. /etc/ld.so.cache中的函式庫列表。

3. /lib目錄,然後/usr/lib。不過一些很老的a.out的loader則是採用相反的次序,也就是先查 /usr/lib,然後是/lib。
dlopen()函式中,引數flag的值必須是RTLD_LAZY或者RTLD_NOW,RTLD_LAZY的意思是resolve undefined symbols as code from the dynamic library is executed,而RTLD_NOW的含義是resolve all undefined symbols before dlopen() returns and fail if this cannot be done'。
如果有好幾個函式庫,它們之間有一些依賴關係的話,例如X依賴Y,那麼你就要先載入那些被依賴的函式。例如先載入Y,然後載入X。

dlopen()函式的返回值是一個控制代碼,然後後面的函式就透過使用這個控制代碼來做進一步的操作。如果開啟失敗dlopen()就返回一個NULL。如果一個函式庫被多次開啟,它會返回同樣的控制代碼。
如果一個函式庫裡面有一個輸出的函式名字為_init,那麼_init就會在dlopen()這個函式返回前被執行。我們可以利用這個函式在我的函式庫裡面做一些初始化的工作。我們後面會繼續討論這個問題的。
4.2. dlerror()

透過呼叫dlerror()函式,我們可以獲得最後一次呼叫dlopen(),dlsym(),或者dlclose()的錯誤資訊。
4.3. dlsym()

如果你載入了一個DL函式庫而不去使用當然是不可能的了,使用一個DL函式庫的最主要的一個函式就是dlsym(),這個函式在一個已經開啟的函式庫裡面查詢給定的符號。這個函式如下定義:

void * dlsym(void *handle, char *symbol);

函式中的引數handle就是由dlopen開啟後返回的控制代碼,symbol是一個以NIL結尾的字串。如果dlsym()函式沒有找到需要查詢的symbol,則返回NULL。如果你知道某個symbol的值不可能是NULL或者0,那麼就很好,你就可以根據這個返回結果判斷查詢的symbol是否存在了;不過,如果某個symbol的值就是NULL,那麼這個判斷就有問題了。標準的判斷方法是先呼叫dlerror(),清除以前可能存在的錯誤,然後呼叫dlsym()來訪問一個symbol,然後再呼叫dlerror()來判斷是否出現了錯誤。一個典型的過程如下:

dlerror();      /*clear error code */  
s = (actual_type)dlsym(handle, symbol_being_searched_for);  
if((error = dlerror()) != NULL){  
    /* handle error, the symbol wasn't found */  
} else {  
    /* symbol found, its value is in s */  
}  

4.4. dlclose()

dlopen()函式的反過程就是dlclose()函式,dlclose()函式用力關閉一個DL函式庫。Dl函式庫維持一個資源利用的計數器,當呼叫dlclose的時候,就把這個計數器的計數減一,如果計數器為0,則真正的釋放掉。真正釋放的時候,如果函式庫裡面有_fini()這個函式,則自動呼叫_fini()這個函式,做一些必要的處理。Dlclose()返回0表示成功,其他非0值表示錯誤。

4.5. DL Library Example

下面是一個例子。例子中調入math函式庫,然後列印2.0的餘弦函式值。例子中每次都檢查是否出錯。應該是個不錯的範例:

int main(int argc, char *argv){  
        void *handle;  
        char *error;  
          
        double (*cosine )(double);  
        handle = dlopen("/lib/libm.so.6", RTLD_LAZY);  
        if(!handle){  
            fputs(dlerror(), stderr);  
             exit(1);  
        }  
          
        cosine = dlsym(handle, "cos");  
        if((error = dlerror()) != NULL){  
            fputs(error, stderr);  
            exit(1);  
        }  
          
        printf("%f", (*cosine)(2, 0));  
          
        dlclose(handle);  
          
        return 0;  
}  

如果這個程式名字叫foo.c,那麼用下面的命令來編譯:

gcc -o foo foo.c –ldl

共享庫

共享庫是程式啟動時載入的庫。共享庫安裝正確後,所有啟動的程式將自動使用新的共享庫。它實際上比這更靈活和複雜,因為Linux使用的方法允許您:

  • 更新庫並且仍然支援希望使用這些庫的舊版,非後向相容版本的程式;

  • 在執行特定程式時,重寫特定庫或甚至庫中的特定函式。

  • 在程式使用現有庫執行時執行所有這些操作。

3.1。約定

對於共享庫來支援所有這些所需的屬性,必須遵循許多約定和準則。您需要了解圖書館名稱之間的區別,特別是“soname”和“實名”(以及它們的相互作用)。您還需要了解它們應該放在檔案系統中的位置。

3.1.1。共享庫名稱

每個共享庫都有一個名為“soname”的特殊名稱。soname具有字首``lib'',庫的名稱,短語“.so”,後跟一個句點和一個版本號,每當介面改變時都會遞增(作為一個特殊的例外,級別C庫不以“lib”開頭)。一個完全合格的soname包含作為字首的目錄; 在一個工作系統上,一個完全合格的soname只是一個與共享庫的“真實姓名”的符號連結。

每個共享庫還有一個“實名”,它是包含實際庫程式碼的檔名。真正的名字增加了一個時期,次要號碼,另一個時期和發行號碼。最後一個期間和發行號碼是可選的。次要號碼和發行號碼透過讓您準確知道安裝了哪些版本的庫,來支援配置控制。請注意,這些數字可能與用於在文件中描述庫的數字不同,儘管這樣做更容易。

另外,編譯器在請求庫時使用的名稱(我將其稱為“連結器名稱”),這只是沒有任何版本號的soname。

管理共享庫的關鍵是這些名稱的分離。程式在內部列出他們需要的共享庫時,應該只列出他們需要的soname。相反,建立共享庫時,只能建立具有特定檔名的庫(具有更詳細的版本資訊)。當您安裝新版本的庫時,將其安裝在幾個特殊目錄之一中,然後執行程式ldconfig(8)。ldconfig檢查現有檔案,並將聲名建立為真實名稱的符號連結,以及設定快取檔案/etc/ld.so.cache(稍後描述)。

ldconfig不設定連結器名稱; 通常這是在庫安裝期間完成的,連結器名稱簡單地建立為“最新”的soname或最新的真實名稱的符號連結。我建議將連結器名稱作為與soname的符號連結,因為在大多數情況下,如果您更新庫,那麼您希望在連結時自動使用它。我問HJ Lu為什麼ldconfig不會自動設定連結器名稱。他的解釋基本上是你可能想使用最新版本的庫來執行程式碼,但是可能需要 開發 連結到舊的(可能不相容的)庫。因此,ldconfig不會對您希望程式連結的任何假設,因此安裝程式必須特別修改符號連結以更新連結器將用於庫。

因此,/ usr /lib/libreadline.so.3是一個完全限定的soname,其中ldconfig將被設定為與/usr/lib/libreadline.so.3.0之類的一些真實名稱的符號連結 。還應該有一個連結器名稱 /usr/lib/libreadline.so ,它可以是引用/usr/lib/libreadline.so.3的符號連結 。

3.1.2。檔案系統放置

共享庫必須位於檔案系統的某個位置。大多數開源軟體往往遵循GNU標準; 有關更多資訊,請參閱info:standards#Directory_Variables上的資訊檔案文件 。GNU標準建議預設安裝/ usr / local / lib中的所有庫,當分發原始碼(所有命令都應該進入/ usr / local / bin)時。它們還定義了覆蓋這些預設值和呼叫安裝例程的約定。

檔案系統層次標準(FHS)討論了在分發中應該去哪裡(請參閱 http://www.pathname.com/fhs)。根據FHS,大多數庫應該安裝在/ usr / lib中,但啟動所需的庫應該在/ lib中,不屬於系統的庫應該在/ usr / local / lib中。

這兩個檔案之間沒有真正的衝突; GNU標準建議開發人員使用預設的原始碼,而FHS則建議分銷商使用預設值(通常透過系統的軟體包管理系統來選擇覆蓋原始碼預設值)。在實踐中,這很好地工作:您下載的“最新”(可能是buggy!)原始碼自動安裝在“本地”目錄(/ usr / local),一旦該程式碼已經成熟,軟體包管理器可以輕鬆地覆蓋預設值,以將程式碼放置在標準的發行版中。請注意,如果您的庫呼叫只能透過庫呼叫的程式,則應將這些程式放在/ usr / local / libexec(在/ usr / libexec中)。一個複雜的情況是,Red Hat派生的系統在搜尋庫時預設不包括/ usr / local / lib; 請參閱下面關於/etc/ld.so.conf的討論。其他標準庫位置包括用於X-windows的/ usr / X11R6 / lib。請注意,/ lib / security用於PAM模組,但通常會作為DL庫載入(下面也將討論)。

3.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。我認為這是一個錯誤,並在/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.hints,a.out快取為/var/run/ld.so.hints。這些仍然由ldconfig(8)更新,所以這個位置的差異只能在幾個異乎尋常的情況下重要。

3.3。環境變數

各種環境變數可以控制此過程,並且有一些環境變數允許您覆蓋此過程。

3.3.1。LD_LIBRARY_PATH

您可以臨時替換不同的庫進行此特定執行。在Linux中,環境變數LD_LIBRARY_PATH是一個冒號分隔的目錄庫,首先要在庫檔案的標準目錄集之前進行搜尋; 當除錯新庫或為特殊目的使用非標準庫時,這非常有用。環境變數LD_PRELOAD列出了覆蓋標準集的函式的共享庫,就像/etc/ld.so.preload一樣。這些由載入器/lib/ld-linux.so實現。我應該注意,雖然LD_LIBRARY_PATH適用於許多類Unix系統,但它並不適用; 例如,此功能在HP-UX上可用,但作為環境變數SHLIB_PATH,在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  - 檔案路徑路徑可執行

只需執行ld-linux.so而不使用引數即可提供更多的使用幫助,但是再一次不要使用它來進行正常使用 - 這些都是用於除錯的。

3.3.2。LD_DEBUG

GNU C載入器中的另一個有用的環境變數是LD_DEBUG。這會觸發dl *函式,以便他們提供關於他們正在做什麼的相當詳細的資訊。例如:

  匯出LD_DEBUG =檔案
  command_to_run

在處理庫時顯示檔案和庫的處理,告訴您哪些依賴關係被檢測到,哪些SO以什麼順序載入。將LD_DEBUG設定為“bindings”顯示有關符號繫結的資訊,將其設定為“libs”,顯示庫搜尋路徑,並將ti設定為“`versions”顯示版本依賴。

將LD_DEBUG設定為“幫助”,然後嘗試執行程式將列出可能的選項。再次,LD_DEBUG不適用於正常使用,但在除錯和測試時可以方便。

3.3.3。其他環境變數

實際上還有一些控制載入過程的其他環境變數; 他們的名字以LD_或RTLD_開頭。大多數其他的是用於低階別的載入程式除錯或用於實現專門的功能。他們大多沒有檔案證明; 如果您需要了解它們,瞭解它們的最佳方式是讀取裝載器的原始碼(gcc的一部分)。

如果不採取特殊措施,允許使用者控制動態連結的庫對於setuid / setgid程式將是災難性的。因此,在GNU載入程式(程式啟動時載入程式的其餘部分)中,如果程式為setuid或setgid,那麼這些變數(和其他類似的變數)將被忽略或受到很大的限制。載入程式透過檢查程式的憑據來確定程式是否被setuid或setgid; 如果uid和euid不同,或者gid和egid不同,那麼載入器會假定程式是setuid / setgid(或者從一個下降的),因此極大地限制了其控制連結的能力。如果您閱讀GNU glibc庫原始碼,可以看到這一點; 特別看到檔案elf / rtld.c和sysdeps / generic / dl-sysdep.c。這意味著如果你使uid和gid等於euid和egid,然後呼叫一個程式,這些變數就會有效果。其他類Unix系統處理不同的情況,但出於同樣的原因:setuid / setgid程式不應該受到環境變數集的不當影響。

3.4。建立共享庫

建立共享庫很容易。首先,使用gcc -fPIC或-fpic標誌建立將進入共享庫的物件檔案。-fPIC和-fpic選項可以實現“位置獨立程式碼”生成,這是共享庫的一個要求; 見下文的差異。您使用-Wl gcc選項傳遞soname。-Wl選項將選項傳遞給連結器(在這種情況下為-soname連結器選項) - -Wl之後的逗號不是打字錯誤,並且您不能在選項中包含未轉義的空格。然後使用以下格式建立共享庫:

gcc -shared -Wl,-soname,your_soname \
    -o library_name  file_list  library_list

這是一個例子,它建立兩個物件檔案(ao和bo),然後建立一個包含它們的共享庫。請注意,此編譯包括除錯資訊(-g),並將生成警告(-Wall),這些共享庫不是必需的,但建議使用。編譯生成物件檔案(使用-c),幷包含所需的-fPIC選項:

gcc -fPIC -g -c -Wall ac
gcc -fPIC -g -c -Wall bc
gcc -shared -Wl,-soname,libmystuff.so.1 \
    -o libmystuff.so.1.0.1 ao bo -lc

這裡有幾點值得注意:

  • 不要剝離生成的庫,並且不要使用編譯器選項-fomit-frame-pointer,除非你真的必須。生成的庫將工作,但這些操作使偵錯程式大多沒有用。

  • 使用-fPIC或-fpic生成程式碼。是否使用-fPIC或-fpic生成程式碼是依賴於目標的。-fPIC選項始終有效,但是可能產生比-fpic更大的程式碼(請記住,這是PIC在更大的情況下,因此可能產生更大量的程式碼)。使用-fpic選項通常會生成更小更快的程式碼,但會有平臺相關的限制,例如全域性可見符號的數量或程式碼的大小。連結器將告訴您,建立共享庫時是否適合。如果有疑問,我選擇-fPIC,因為它總是有效。

  • 在某些情況下,呼叫gcc來建立物件檔案也需要包含“-Wl,-export-dynamic”選項。通常,動態符號表僅包含動態物件使用的符號。此選項(建立ELF檔案時)將所有符號新增到動態符號表(有關詳細資訊,請參閱ld(1))。當有“反向相關性”時,您需要使用此選項,即,DL庫具有未解決的符號,按照慣例,必須在要載入這些庫的程式中定義它們。對於“反向相關性”工作,主程式必須使其符號動態可用。請注意,如果您只使用Linux系統,則可以使用“-rdynamic”而不是“-Wl,export-dynamic”,但根據ELF文件,“-rdynamic”

在開發過程中,修改也被許多其他程式使用的庫的潛在問題 - 您不希望其他程式使用“開發”庫,只是您正在測試的特定應用程式。您可能使用的一個連結選項是ld的“rpath”選項,它指定正在編譯的特定程式的執行時庫搜尋路徑。從gcc,您可以透過這樣指定來呼叫rpath選項:

 -Wl,-rpath,$(DEFAULT_LIB_INSTALL_PATH)

如果您在構建庫客戶機程式時使用此選項,則不需要再打擾LD_LIBRARY_PATH(下文將介紹),除了確保它不衝突,或者使用其他技術來隱藏庫。

3.5。安裝和使用共享庫

建立共享庫後,您需要安裝它。簡單的方法是將庫複製到標準目錄(例如/ usr / lib)中,並執行ldconfig(8)。

首先,您需要在某個地方建立共享庫。然後,您將需要設定必要的符號連結,特別是從soname到真實名稱的連結(以及從無版本的soname,即以“.so”結尾的soname)為使用者誰沒有指定版本)。最簡單的方法是執行:

ldconfig -n directory_with_shared_libraries

最後,當你編譯你的程式時,你需要告訴連結器你正在使用的任何靜態和共享庫。使用-l和-L選項。

如果您不能或不想在標準位置安裝庫(例如,您沒有許可權修改/ usr / lib),則需要更改方法。在這種情況下,您需要將其安裝在某個地方,然後為您的程式提供足夠的資訊,以便程式可以找到庫...並且有幾種方法可以做到這一點。您可以在簡單的情況下使用gcc的-L標誌。您可以使用“rpath”方法(如上所述),特別是如果您只有一個特定的程式將庫放置在“非標準”位置。您也可以使用環境變數來控制事物。特別是,您可以設定LD_LIBRARY_PATH,這是一個冒號分隔的目錄列表,用於在通常的位置之前搜尋共享庫。如果你使用bash,

LD_LIBRARY_PATH =。:$ LD_LIBRARY_PATH my_program

如果要僅覆蓋幾個選定的函式,可以透過建立一個覆蓋目標的檔案並設定LD_PRELOAD來實現; 此物件檔案中的函式將僅覆蓋這些函式(留下其他函式)。

通常你可以不需要更新庫; 如果有API更改,則庫建立者應該更改soname。這樣,多個庫可以在單個系統上,併為每個程式選擇正確的庫。但是,如果一個程式中斷更新到保持相同soname的庫,您可以強制它使用舊的庫版本透過將舊的庫複製到某個地方,重新命名該程式(比如說舊的名稱加上“.orig ''),然後建立一個小的“包裝器”指令碼,該指令碼重置庫以使用並呼叫真實(重新命名)程式。您可以將舊圖書館放在自己的特殊區域,如果您願意,儘管編號約定允許多個版本生活在同一目錄中。包裝指令碼可能看起來像這樣:

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

編寫自己的程式時請不要依賴這個; 嘗試確保您的庫向後相容,或者您​​每次進行不相容的更改時都會在soname中增加版本號。這只是處理最壞情況問題的“緊急”方法。

您可以使用ldd(1)檢視程式使用的共享庫列表。所以,例如,您可以透過鍵入以下方式檢視ls使用的共享庫:

  ldd / bin / ls

一般來說,您將看到依賴的聲名的列表,以及這些名稱解析的目錄。在幾乎所有情況下,您至少有兩個依賴關係:

  • /lib/ld-linux.so.N(其中N為1或更多,通常至少為2)。這是載入所有其他庫的庫。

  • libc.so.N(N為6以上)。這是C庫。即使是其他語言也傾向於使用C庫(至少要實現自己的庫),所以大多數程式至少包括這個庫。

請注意:千萬 不能 對你不信任的程式執行LDD。如ldd(1)手冊中明確指出的,ldd透過設定特殊環境變數(對於ELF物件,LD_TRACE_LOADED_OBJECTS),然後執行程式(在某些情況下)工作。不可信程式可能強制ldd使用者執行任意程式碼(而不是簡單地顯示ldd資訊)。所以,為了安全起見,不要在不信任的程式上使用ldd來執行。

3.6。不相容的庫

當新版本的庫與舊版本的二進位制不相容時,soname需要更改。在C中,圖書館將不再是二進位制相容的四個基本原因:

  1. 函式的行為發生變化,使其不再符合其原始規範,

  2. 匯出的資料項更改(例外:將可選項新增到結構的末尾是可以的,只要這些結構只在庫中分配)。

  3. 匯出的功能被刪除。

  4. 匯出功能的介面發生變化。

如果可以避免這些原因,可以使您的庫二進位制相容。換句話說,如果您避免此類更改,您可以保持您的應用程式二進位制介面(ABI)相容。例如,您可能需要新增新功能,但不要刪除舊功能。您可以向結構中新增專案,但只有透過將專案新增到結構的末尾才能確保舊程式不會對這些更改敏感,只允許庫(而不是應用程式)分配結構,使額外的專案可選(或將庫填充到其中),等等。注意 - 如果使用者在陣列中使用它們,您可能無法展開結構。

對於C ++(以及支援編譯模板和/或編譯排程方法的其他語言),情況更加棘手。所有上述問題都適用,還有更多問題。原因是一些資訊在編譯程式碼中被實現為“在封面下”,導致依賴關係,如果您不知道如何通常實現C ++,這可能並不明顯。嚴格來說,它們不是“新”的問題,只是編譯的C ++程式碼以可能令您驚訝的方式呼叫它們。以下是您不能在C ++中執行的(可能是不完整的)列表,並保留二進位制相容性,如 Troll Tech的技術常見問題報告

  1. 新增虛擬函式的重新實現(除非它對於舊的二進位制檔案呼叫原始實現是安全的),因為編譯器在編譯時評估SuperClass :: virtualFunction()呼叫(而不是連結時)。

  2. 新增或刪除虛擬成員函式,因為這會改變每個子類的vtbl的大小和佈局。

  3. 更改任何資料成員的型別或移動可透過內聯成員函式訪問的任何資料成員。

  4. 更改類層次結構,除了新增新的樹葉。

  5. 新增或刪除私有資料成員,因為這會改變每個子類的大小和佈局。

  6. 刪除公共或受保護的成員函式,除非它們是內聯的。

  7. 公開或保護成員函式內聯。

  8. 更改行內函數的作用,除非舊版本繼續工作。

  9. 在行動式程式中更改成員函式的訪問許可權(即公共,受保護或私有),因為一些編譯器將訪問許可權轉換為函式名稱。

給定這個冗長的列表,特別是C ++庫的開發人員必須計劃更多的偶爾更新破壞二進位制相容性。幸運的是,在類Unix系統(包括Linux)上,您可以同時載入多個版本的庫,所以當有一些磁碟空間損失時,使用者仍然可以執行需要舊庫的“舊”程式。

本文來自部落格園,作者:sunsky303,轉載請註明原文連結:https://www.cnblogs.com/sunsky303/p/7731911.html

相關文章