關(guān)于靜態(tài)庫,共享庫,動(dòng)態(tài)鏈接庫~~~
2011-02-23 11:39
動(dòng)態(tài)鏈接庫de創(chuàng )建
一個(gè)“程序函數庫”簡(jiǎn)單說(shuō)就是一個(gè)文件包含一些編譯好的代碼和數據,這些編譯好的代碼和數據可以在事后供其它的程序
使用。程序函數庫可以使整個(gè)程序更加模塊化,更容易重新編譯,而且更方便升級。
程序函數庫可分為三種類(lèi)型:靜態(tài)函數庫--static libraries,共享函數庫--shared libraries,和動(dòng)態(tài)函數庫--dynamic loaded libraries。靜態(tài)函數庫是在程序執行前就加入到目標程序中去了;而共享函數庫則是在程序啟動(dòng)的時(shí)候加載到程序中,它可以被不同的程序共享;動(dòng)態(tài)加載函數庫則可以在程序運行的任何時(shí)候動(dòng)態(tài)加載。實(shí)際上,動(dòng)態(tài)函數庫并非另外一種庫函數
模式,區別是動(dòng)態(tài)函數庫是如何被程序元使用的。
靜態(tài)函數庫:
:靜態(tài)函數庫實(shí)際上就是簡(jiǎn)單的一個(gè)普通的目標文件集合,一般來(lái)說(shuō)習慣使用".a"作為文件的后綴。
:可以用ar這個(gè)程序產(chǎn)生靜態(tài)函數庫文件。ar是archiver的縮寫(xiě)。靜態(tài)函數庫現在已經(jīng)不再像以前用得那么多了,主要是共享函數庫與之比較有很多的優(yōu)勢的原因。
:靜態(tài)庫函數允許程序員把程序link起來(lái)而不用重新編譯代碼,節省了重新編譯代碼的時(shí)間。
:靜態(tài)函數庫對開(kāi)發(fā)這還是很有用的,例如你想把自己提供的函數給別人使用,但是又想對函數的源代碼進(jìn)行保密,你就可以給別人提供一個(gè)靜態(tài)函數庫文件。理論上說(shuō),使用elf格式的靜態(tài)庫函數生成的代碼可以比使用共享函數庫(或者動(dòng)態(tài)函數庫)的程序運行速度上快一些,大概1-5%。
:創(chuàng )建一個(gè)靜態(tài)函數庫文件,或者往一個(gè)已經(jīng)存在的靜態(tài)函數庫文件添加新的目標代碼,可以用下面命令:
ar rcs my_library.a file1.o file2.o //more info, please man ar
:一旦你創(chuàng )建了一個(gè)靜態(tài)函數庫,你就可以使用它了。你可以把它作為你編譯和鏈接過(guò)程中的一部分用來(lái)生成你的可執行代碼。如果你用gcc來(lái)編譯產(chǎn)生可執行代碼的話(huà),你可以用"-l"參數來(lái)指定這個(gè)庫函數。你也可以用ld來(lái)做,它用它的"-l"和"-L"參數選項。具體做法可參考info:gcc。
共享函數庫:
:共享函數庫中的函數是在當一個(gè)可執行程序在啟動(dòng)的時(shí)候被加載。如果一個(gè)共享函數庫正常安裝,所有的程序在重新運行的時(shí)候都可以自動(dòng)加載最新的函數庫中的函數。對于Linux系統還有更多的可以實(shí)現的功能:(1)升級了函數庫但是仍然允許程序使用老版本的函數庫;(2)當執行某個(gè)特定程序的時(shí)候可以覆蓋某個(gè)特定的庫或者庫中指定的函數; (3)可以在庫函數被使用的過(guò)程中修改這些函數庫。
一些約定:如果你要編寫(xiě)的共享函數庫支持所有有用的特性,你在編寫(xiě)的過(guò)程中必須遵循一系列約定。你必須理解庫的不同名字間的區別,例如它的"soname"和"real name"之間的區別和它們是如何相互作用的。你同樣還要知道你應該把這些庫函數放在你文件系統的什么位置下等等。
:共享庫的命名--每個(gè)共享函數庫都有個(gè)特殊的名字,稱(chēng)作soname。soname名字命名必須以"lib"作為前綴,然后是函數庫的名字,然后是".so",最后是版本號信息。不過(guò)有個(gè)特例,就是非常底層的C庫函數都不是以lib開(kāi)頭這樣命名的。
每個(gè)共享函數庫都有一個(gè)真正的名字--real name,它是包含真正庫函數代碼的文件。真名有一個(gè)主版本號,和一個(gè)發(fā)行版本號。最后一個(gè)發(fā)行版本號是可選的,可以沒(méi)有。主版本號和發(fā)行版本號使你可以知道你到底是安裝了什么版本的庫函數。
另外,還有一個(gè)名字是編譯器編譯的時(shí)候需要的函數庫的名字,這個(gè)名字就是簡(jiǎn)單的soname名字,而不包含任何版本號信息。
管理共享函數庫的關(guān)鍵是區分好這些名字。當可執行程序需要在自己的程序中列出這些它們需要的共享庫函數的時(shí)候,它只要用soname就可以了;反過(guò)來(lái),當你要創(chuàng )建一個(gè)新的共享函數庫的時(shí)候,你要指定一個(gè)特定的文件名,其中包含很多很細節的版本信息。當你要安裝一個(gè)新版本的函數庫的時(shí)候,你只要先將這些函數庫文件拷貝到一些特定的目錄中,運行ldconfig就可以。ldconfig檢查已經(jīng)存在的庫函數,然后創(chuàng )建soname的符號連接到真正的函數庫,同時(shí)設置/etc/ld.so.cache這個(gè)緩沖文件。
ldconfig并不設置連接的名字,通常的做法是在安裝過(guò)程當中完成這個(gè)連接名字的建立,一般來(lái)說(shuō)這個(gè)符號連接就簡(jiǎn)單的指向最新的soname或者最新版本的函數文件。最好把這個(gè)符號連接指向soname,因為通常當你升級你的庫函數的時(shí)候,你就可以自動(dòng)使用新版本的函數庫了。
Example:
/usr/lib/libreadline.so.3是一個(gè)完全的完整的soname,ldconfig可以設置一個(gè)符號鏈接到其它某個(gè)真正的函數庫文件,
例如是/usr/lib/libreadline.so.3.0。同時(shí)還必須有一個(gè)連接名字,例如/usr/lib/libreadline.so就是一個(gè)符號鏈接
指向/usr/lib/libreadline.so.3。//???????
:文件系統中函數庫文件的位置--共享函數庫文件必須放在一些特定的目錄里,這樣通過(guò)系統的環(huán)境變量設置,應用程序才能正確的使用這些函數庫。大部分的源碼開(kāi)發(fā)的程序都遵循GNU的一些標準。GNU標準建議所有的函數庫文件都放在/usr/local/lib目錄下,而且建議命令可執行程序都放在/usr/local/bin目錄下。這都是一些習慣問(wèn)題,可以改變。而文件系統層次化標準FHS(Filesystem Hierarchy Standard)規定:在一個(gè)發(fā)行包中大部分的函數庫文件應該安裝到/usr/lib目錄下,但是如果某些庫是在系統啟動(dòng)的時(shí)候要加載的,則放到/lib目錄下,而那些不適系統本身一部分的庫則放到/usr/local/lib下。
GNU提出的標準主要對于開(kāi)發(fā)者開(kāi)放源碼的,而FHS的建議則是針對發(fā)行版本的路徑的。具體信息可以看/etc/ld.so.conf里面的配置信息。
:如何使用?
在基于GNU glibc的系統里,包括所有的linux系統,啟動(dòng)一個(gè)elf格式的二進(jìn)制可執行文件會(huì )自動(dòng)啟動(dòng)和運行一個(gè)program loader。對于linux系統,這個(gè)loader的名字是/lib/ld-linux.so.X(version)。這個(gè)loader啟動(dòng)后,反過(guò)來(lái)就會(huì )load所有的本程序要使用的共享函數庫。
到底在哪些目錄里查找共享函數呢?這些定義缺省的是放在/etc/ld.so.conf文件里面,我們可以修改這個(gè)文件,加入我們自己的一些特殊的路徑要求。
如果想覆蓋某個(gè)庫中的一些函數,用自己的函數替換它們,同時(shí)保留該庫中其它的函數的話(huà),你可以在/etc/ld.so.preload中加入你想要替換的庫(.o結尾的文件),這些preloading的庫函數將有優(yōu)先加載的權利。
當程序啟動(dòng)的時(shí)候搜索所有的目錄顯然會(huì )效率很低,于是linux系統實(shí)際上用的是一個(gè)高速緩沖的做法。ldconfig缺省情況下讀出/etc/ld.so.conf相關(guān)信息,然后設置適當當地符號鏈接,然后寫(xiě)cache到/etc/ld.so.cache這個(gè)文件中,而這個(gè)/etc/ld.so.cache則可以被其它程序有效使用。這樣的做法可以大大提高訪(fǎng)問(wèn)函數庫的速度。這就要求每次新增加一個(gè)動(dòng)態(tài)加載的函數庫的時(shí)候,就要運行ldconfig來(lái)更新這個(gè)cache,如果要刪除某個(gè)函數庫,或者某個(gè)函數庫的路徑修改了,都要重新運行ldconfig來(lái)更新這個(gè)cache。通常的一些包管理器在安裝一個(gè)新的函數庫的時(shí)候都要運行ldconfig。
:環(huán)境變量
各種各樣的環(huán)境變量控制著(zhù)一些關(guān)鍵的過(guò)程。例如你可以臨時(shí)為特定的程序的一次執行指定一個(gè)不同的函數庫。Linux系統中,通常變量LD_LIBRARY_PATH就是可以用來(lái)會(huì )自定函數庫查找路徑的,而且這個(gè)路徑通常是在查找標準路徑之前查找。這個(gè)是很有用的,特別是在調試一個(gè)新的函數庫的時(shí)候,或者在特殊的場(chǎng)合使用一個(gè)非標準的函數庫的時(shí)候。環(huán)境變量LD_PRELOAD列出了所有共享函數庫中需要優(yōu)先加載的庫文件,功能和/etc/ld.so.preload類(lèi)似。這些都是有/lib/ld-linux.so這個(gè)loader來(lái)實(shí)現的。值得一提的是LD_LIBRARY_PATH可以在大部分UNIX-LIKE系統下正常工作,但是并非所有的系統下都可以使用,例如HP-UX系統下,就是用SHLIB_PATH這個(gè)變量,而在A(yíng)IX下則使用LIBPATH這個(gè)變量。LD_LIBRARY_PATH在開(kāi)發(fā)和調試過(guò)程中經(jīng)常大量使用,但是不應該被一個(gè)普通用戶(hù)在安裝過(guò)程中被安裝程序修改...
事實(shí)上還有更多的環(huán)境變量影響著(zhù)程序的調入過(guò)程,它們的名字通常就是以L(fǎng)D_或者RTLD_打頭。大部分這些環(huán)境變量的使用的文檔都不完全。如果要真正弄清楚它們的用法,最好去讀loader的源碼(gcc的一部分)。
允許用戶(hù)控制動(dòng)態(tài)鏈接函數庫將涉及到setuid/setgid這個(gè)函數如果特殊的功能需要的話(huà)。因此,GNU loader通常限制或者忽略
用戶(hù)對這些變量使用的setuid和setgid,如果loader通過(guò)判斷程序的相關(guān)環(huán)境變量判斷程序是否使用了setuid或者setgid,然后就大大的限制控制這個(gè)老鏈接的權限。如果閱讀GNU glibc的庫函數源碼,就可以清查的看到這一點(diǎn),特別的我們可以看elf/rtld.c和/sysdeps/generic/dl-sysdep.c這兩個(gè)文件。這就意味著(zhù)如果你使用uid和gid與euid和egid分別相等,然后調用一個(gè)程序,那么這些變量就可以完全起效。
創(chuàng )建一個(gè)共享函數庫:
現在我們開(kāi)始學(xué)習創(chuàng )建一個(gè)共享函數庫。其實(shí)創(chuàng )建一個(gè)共享函數庫非常容易,首先創(chuàng )建object文件,這個(gè)文件將加入通過(guò)gcc -fPIC參數命令加入到共享函數庫里面。PIC的意思是位置無(wú)關(guān)代碼--Position Independent Code。下面是一個(gè)標準的格式:
gcc -shared -Wl, -soname,your_soname -o library_name file_list library_list
Example:
創(chuàng )建兩個(gè)object文件a.o和b.o,然后創(chuàng )建一個(gè)包含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
一些注意的地方:
不用-formit-frame-pointer這個(gè)編譯參數,除非你不得不這樣。雖然使用了這個(gè)參數后獲得的函數庫仍然可用,但是這使得調試程序幾乎沒(méi)有用,無(wú)法跟蹤調試。
使用-fPIC來(lái)乘勝代碼,而不是-fpic。
某些情況下,使用gcc來(lái)生成object文件,需要使用"-Wl, -export-dynamic"這個(gè)選項參數。通常動(dòng)態(tài)函數庫的符號表里面包含了這些動(dòng)態(tài)的對象的符號。這個(gè)選項在創(chuàng )建elf格式的文件的時(shí)候,會(huì )將所有的符號加入到動(dòng)態(tài)符號表中??梢詤⒖糽d的幫助獲得更多信息。
:安裝和使用共享函數庫
一旦你創(chuàng )建并使用一個(gè)共享函數庫,你還需要安裝它。其實(shí)簡(jiǎn)單得方法就是拷貝你的庫文件到指定的標準的目錄,然后運行ldconfig。
如果你沒(méi)有權限去做這些事情,那么你只好通過(guò)修改你的環(huán)境變量來(lái)實(shí)現這些函數庫的使用了。首先,你需要創(chuàng )建這些共享函數庫;然后,設置一些必須的符號鏈接,特別是從soname到真正的函數庫文件的符號鏈接,簡(jiǎn)單的方法就是運行ldconfig:
ldconfig -n directory_with_shared_libraries
然后你就可以設置你的LD_LIBRARY_PATH這個(gè)環(huán)境變量,它是一個(gè)以逗號分割的路徑的集合,這個(gè)可以用來(lái)指明共享函數庫的搜索路徑。例如,使用bash,就可以這樣來(lái)啟動(dòng)一個(gè)程序my_program:
LD_LIBRARY_PATH=. :$LD_LIBRARY_PATH my_program
如果你需要的是重載部分函數,則你就需要創(chuàng )建一個(gè)包含需要重載的函數的object文件,然后設置LD_PRELOAD環(huán)境變量。通常你可以很方便升級你的函數庫,如果某個(gè)API改變了,創(chuàng )建庫的程序會(huì )改變soname。然而,如果一個(gè)函數升級了某個(gè)函數庫而保持了原來(lái)的soname,你可以強行將老版本的函數庫拷貝到某個(gè)位置,然后重新命名這個(gè)文件(例如使用原來(lái)的名字,然后后面加.orig后綴),然后創(chuàng )建一個(gè)小的"wrapper"腳本來(lái)設置這個(gè)庫函數和相關(guān)的東西。
Example:
#!/bin/sh export
LD_LIBRARY_PATH=/usr/local/my_lib:$LD_LIBRARY_PATH exec /usr/bin/my_program.orig $*
我們可以通過(guò)運行ldd來(lái)查看某個(gè)程序使用的共享函數庫。
Example:利用ldd來(lái)看ls這個(gè)使用工具使用的函數庫:
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)
通常我們可以看到一個(gè)soname的列表,包括路徑。在所有的情況下,你都至少可以看到兩個(gè)庫:
/lib/ld-linux.so.N (N是1或者更大,一般至少2) //這個(gè)是用來(lái)加載其它所有的共享庫的庫
/libc.so.N (N應該大于或者等于6)。這是C語(yǔ)言庫函數。
值得一提的是,不要再對你不信任的程序運行ldd命令。在ldd的manual里面寫(xiě)的清楚,ldd是通過(guò)設置某些特殊的環(huán)境變量(例如,對于elf對象,設置LD_TRACE_LOADED_OBJECTS),然后運行這個(gè)程序。這樣就有可能執行某些意想不到的代碼,而產(chǎn)生不安全的隱患。
:不兼容的函數庫
如果一個(gè)新版本的函數庫要和老版本的二進(jìn)制的庫不兼容,則soname需要改變。對于C語(yǔ)言,一共有四個(gè)基本理由使得它們在
二進(jìn)制代碼上很難兼容:(1)一個(gè)函數的行文改變了,這樣它就可能與最開(kāi)始的定義不相符合;(2)輸出的數據項改變了;(3)某些輸出的函數刪除了;(4)某些輸出函數的接口改變了。如果你能避免這些地方,你就可以保持你的函數庫在二進(jìn)制代碼上的兼容,或者說(shuō),你可以使得你的程序應用二進(jìn)制接口(ABI:Application Binary Interface)上兼容。
動(dòng)態(tài)加載的函數庫:參見(jiàn)第一部分。
nm命令:
nm命令可以列出一個(gè)函數庫文件中的符號表。它對于靜態(tài)的函數庫和共享的函數庫都起作用。對于一個(gè)給定的函數庫,nm命令可以列出函數庫中定義的所有符號,包括每個(gè)符號的值和類(lèi)型。還可以給出在原程序中這個(gè)函數(符號)是在多少行定義的,不過(guò)這必須要求編譯該函數庫的時(shí)候加“-l”選項。
符號的類(lèi)型是以一個(gè)字母的形式顯示的,小寫(xiě)字母表示這個(gè)符號是本地(local)的,而大寫(xiě)字母則表示這個(gè)符號是全局的(global,externel)。一般來(lái)說(shuō),類(lèi)型有一下幾種:T、D、B、U、W。各自的含義如下:T表示在代碼段中定義的一般變量符號;D表示時(shí)初始化過(guò)的數據段;B表示初始化的數據段;U表示沒(méi)有定義的,在這個(gè)庫里面使用了,但是在其他庫中定義的符號;W,weak的縮寫(xiě),表示如果其他函數庫中也有對這個(gè)符號的定義,則其他符號的定義可以覆蓋這個(gè)定義。
如果你知道一個(gè)函數的名字,但是你不知道這個(gè)函數在什么苦衷定義的,那么可以用nm的"-o"選項和grep命令來(lái)查找庫的名字。
Example:
nm -o /lib/* /usr/lib/* /usr/lib/*/* /usr/local/lib/* 2 > /dev/null | grep 'cos$'