1. 介紹
html
使用GNU的工具咱們如何在Linux下建立本身的程序函數庫?一個「程序函數庫」簡單的說就是一個文件包含了一些編譯好的代碼和數據,這些編譯好的代碼和數據能夠在過後供其餘的程序使用。程序函數庫可使整個程序更加模塊化,更容易從新編譯,並且更方便升級。 linux
程序函數庫可分爲3種類型:靜態函數庫(static libraries)、共享函數庫(shared libraries)、動態加載函數庫(dynamically loaded libraries): 程序員
一、靜態函數庫,是在程序執行前就加入到目標程序中去了 ;windows
二、動態函數庫同共享函數庫是一個東西(在linux上叫共享對象庫, 文件後綴是.so ,windows上叫動態加載函數庫, 文件後綴是.dll)數組
2. 靜態函數庫
緩存
靜態函數庫實際上就是簡單的一個普通的目標文件的集合,通常來講習慣用「.a」做爲文件的後綴。能夠用ar這個程序來產生靜態函數庫文件。Ar是archiver的縮寫。靜態函數庫如今已經不在像之前用得那麼多了,主要是共享函數庫與之相比較有不少的優點的緣由。慢慢地,你們都喜歡使用共享函數庫了。不過,在一些場所靜態函數庫仍然在使用,一來是保持一些與之前某些程序的兼容,二來它描述起來也比較簡單。 安全
靜態庫函數容許程序員把程序link起來而不用從新編譯代碼,節省了從新編譯代碼的時間。不過,在今天這麼快速的計算機面前,通常的程序的從新編譯也花費不了多少時間,因此這個優點已經不是像它之前那麼明顯了。靜態函數庫對開發者來講仍是頗有用的,例如你想把本身提供的函數給別人使用,可是又想對函數的源代碼進行保密,你就能夠給別人提供一個靜態函數庫文件。理論上說,使用ELF格式的靜態庫函數生成的代碼能夠比使用共享函數庫(或者動態函數庫)的程序運行速度上快一些,大概1-5%。 bash
建立一個靜態函數庫文件,或者往一個已經存在地靜態函數庫文件添加新的目標代碼,能夠用下面的命令: app
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系統還有更多能夠實現的功能:
一、升級了函數庫可是仍然容許程序使用老版本的函數庫。
二、當執行某個特定程序的時候能夠覆蓋某個特定的庫或者庫中指定的函數。
三、能夠在庫函數被使用的過程當中修改這些函數庫。
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()來判斷是否出現了錯誤。一個典型的過程以下:
4.4. dlclose()
dlopen()函數的反過程就是dlclose()函數,dlclose()函數用力關閉一個DL函數庫。Dl函數庫維持一個資源利用的計數器,當調用dlclose的時候,就把這個計數器的計數減一,若是計數器爲0,則真正的釋放掉。真正釋放的時候,若是函數庫裏面有_fini()這個函數,則自動調用_fini()這個函數,作一些必要的處理。Dlclose()返回0表示成功,其餘非0值表示錯誤。
4.5. DL Library Example
下面是一個例子。例子中調入math函數庫,而後打印2.0的餘弦函數值。例子中每次都檢查是否出錯。應該是個不錯的範例:
若是這個程序名字叫foo.c,那麼用下面的命令來編譯:
gcc -o foo foo.c –ldl
共享庫是程序啓動時加載的庫。共享庫安裝正確後,全部啓動的程序將自動使用新的共享庫。它實際上比這更靈活和複雜,由於Linux使用的方法容許您:
更新庫而且仍然支持但願使用這些庫的舊版,非後向兼容版本的程序;
在執行特定程序時,重寫特定庫或甚至庫中的特定函數。
在程序使用現有庫運行時執行全部這些操做。
對於共享庫來支持全部這些所需的屬性,必須遵循許多約定和準則。您須要瞭解圖書館名稱之間的區別,特別是「soname」和「實名」(以及它們的相互做用)。您還須要瞭解它們應該放在文件系統中的位置。
每一個共享庫都有一個名爲「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的符號連接 。
共享庫必須位於文件系統的某個位置。大多數開源軟件每每遵循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庫加載(下面也將討論)。
在基於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)更新,因此這個位置的差別只能在幾個異乎尋常的狀況下重要。
各類環境變量能夠控制此過程,而且有一些環境變量容許您覆蓋此過程。
您能夠臨時替換不一樣的庫進行此特定執行。在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而不使用參數便可提供更多的使用幫助,可是再一次不要使用它來進行正常使用 - 這些都是用於調試的。
GNU C加載器中的另外一個有用的環境變量是LD_DEBUG。這會觸發dl *函數,以便他們提供關於他們正在作什麼的至關詳細的信息。例如:
導出LD_DEBUG =文件 command_to_run |
在處理庫時顯示文件和庫的處理,告訴您哪些依賴關係被檢測到,哪些SO以什麼順序加載。將LD_DEBUG設置爲「bindings」顯示有關符號綁定的信息,將其設置爲「libs」,顯示庫搜索路徑,並將ti設置爲「`versions」顯示版本依賴。
將LD_DEBUG設置爲「幫助」,而後嘗試運行程序將列出可能的選項。再次,LD_DEBUG不適用於正常使用,但在調試和測試時能夠方便。
實際上還有一些控制加載過程的其餘環境變量; 他們的名字以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程序不該該受到環境變量集的不當影響。
建立共享庫很容易。首先,使用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(下文將介紹),除了確保它不衝突,或者使用其餘技術來隱藏庫。
建立共享庫後,您須要安裝它。簡單的方法是將庫複製到標準目錄(例如/ 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庫(至少要實現本身的庫),因此大多數程序至少包括這個庫。
當新版本的庫與舊版本的二進制不兼容時,soname須要更改。在C中,圖書館將再也不是二進制兼容的四個基本緣由:
函數的行爲發生變化,使其再也不符合其原始規範,
導出的數據項更改(例外:將可選項添加到結構的末尾是能夠的,只要這些結構只在庫中分配)。
導出的功能被刪除。
導出功能的界面發生變化。
若是能夠避免這些緣由,可使您的庫二進制兼容。換句話說,若是您避免此類更改,您能夠保持您的應用程序二進制接口(ABI)兼容。例如,您可能須要添加新功能,但不要刪除舊功能。您能夠向結構中添加項目,但只有經過將項目添加到結構的末尾才能確保舊程序不會對這些更改敏感,只容許庫(而不是應用程序)分配結構,使額外的項目可選(或將庫填充到其中),等等。注意 - 若是用戶在數組中使用它們,您可能沒法展開結構。
對於C ++(以及支持編譯模板和/或編譯調度方法的其餘語言),狀況更加棘手。全部上述問題都適用,還有更多問題。緣由是一些信息在編譯代碼中被實現爲「在封面下」,致使依賴關係,若是您不知道如何一般實現C ++,這可能並不明顯。嚴格來講,它們不是「新」的問題,只是編譯的C ++代碼以可能令您驚訝的方式調用它們。如下是您不能在C ++中執行的(多是不完整的)列表,並保留二進制兼容性,如 Troll Tech的技術常見問題報告:
添加虛擬函數的從新實現(除非它對於舊的二進制文件調用原始實現是安全的),由於編譯器在編譯時評估SuperClass :: virtualFunction()調用(而不是連接時)。
添加或刪除虛擬成員函數,由於這會改變每一個子類的vtbl的大小和佈局。
更改任何數據成員的類型或移動可經過內聯成員函數訪問的任何數據成員。
更改類層次結構,除了添加新的樹葉。
添加或刪除私有數據成員,由於這會改變每一個子類的大小和佈局。
刪除公共或受保護的成員函數,除非它們是內聯的。
公開或保護成員函數內聯。
更改內聯函數的做用,除非舊版本繼續工做。
在便攜式程序中更改爲員函數的訪問權限(即公共,受保護或私有),由於一些編譯器將訪問權限轉換爲函數名稱。
給定這個冗長的列表,特別是C ++庫的開發人員必須計劃更多的偶爾更新破壞二進制兼容性。幸運的是,在類Unix系統(包括Linux)上,您能夠同時加載多個版本的庫,因此當有一些磁盤空間損失時,用戶仍然能夠運行須要舊庫的「舊」程序。