1. 程式人生 > >對靜態庫,共享庫,動態載入庫的理解

對靜態庫,共享庫,動態載入庫的理解

轉載來源:http://blog.sina.com.cn/s/blog_8f3985400100uw5k.html

在上面原文基礎上整理了一下,主要突出了重點內容。

        庫檔案一般就是編譯好的二進位制檔案,用於在連結階段同目的碼一起生成可執行檔案,或者執行可執行檔案的時候被載入,以便呼叫庫檔案中的某段程式碼。它與可執行檔案相同之處是:兩者都是編譯好的二進位制檔案(本文中我們討論的二進位制檔案,假設都是linux上面最常見的ELF格式);與可執行檔案不同的是:庫檔案無法直接執行(直觀上來看它的原始碼中沒有main函式,而只是一些函式模組的定義和實現,沒有執行的入口主函式,所以無法直接執行)。我們開發的

程式,無論是執行的時候,還是編譯、連結的時候,一般都需要藉助一些庫來實現它們的功能,而很少直接只通過程式原始碼生成完全獨立的可執行檔案。有許多著名的常用的用於開發或者執行程式所需的庫,例如Qt庫,gtk庫,甚至是標準C庫等等,通過使用它們可以充分體會到模組化程式設計和程式碼重用等的好處。本文對Linux庫的編譯,生成,使用進行了簡單的介紹,並且通過一個簡單例子(開發自己的庫)進行說明。

主要內容
原理
舉例
其它

[原理]
為便於理解,我們可以將庫分為三種類型:靜態庫,共享庫,動態載入庫,。下面分別介紹。 
一、 靜態庫
靜態庫實際就是一些目標檔案(一般以.o結尾)的集合,靜態庫一般以.a結尾,只用於連結生成可執行檔案階段。具體來說,以c程式為例,一般我們編譯程式原始碼的時候,過程大致是這樣的:以.c為字尾的原始檔經過編譯生成.o的目標檔案,以.o為字尾的目標檔案經過連結生成最終可執行的檔案。我們可以在連結的時候直接連結.o的目標檔案,也可以將這些.o目標檔案打包集中起來,統一連結,而這個打包生成的集中了所有.o檔案的檔案,就是靜態庫。靜態庫只在程式連結的時候使用,連結器會將程式中使用到函式的程式碼從庫檔案中拷貝到應用程式中,一旦連結完成生成可執行檔案之後,在執行程式的時候就不需要靜態庫了。由於每個使用靜態庫的應用程式都需要拷貝所用函式的程式碼,所以靜態連結的生成的可執行檔案會比較大,多個程式執行時佔用記憶體空間比較大(每個程式在記憶體中都有一份重複的靜態庫程式碼),但是由於執行的時候不用從外部動態載入額外的庫了,速度會比共享庫快一些。我們將在後面的例子中看到靜態庫的生成和應用的具體過程。

二、共享庫
共享庫以.so結尾. (so == share object) 在程式連結的時候並不像靜態庫那樣從庫中拷貝使用的函式程式碼到生成的可執行檔案中,而只是作些標記,然後在程式開始啟動執行的時候,動態地載入所需庫(模組)。所以,應用程式在執行的時候仍然需要共享庫的支援。共享庫連結出來的可執行檔案比靜態庫連結出來的要小得多,執行多個程式時佔用記憶體空間比也比靜態庫方式連結少(因為記憶體中只有一份共享庫程式碼的拷貝),但是由於有一個動態載入的過程所以速度稍慢。

三、 動態載入庫 
1. 概念 
動態載入庫(dynamically loaded (DL) libraries)是指在程式執行過程中可以載入的函式庫。而不是像共享庫一樣在程式啟動的時候載入。DL對於實現外掛和模組非常有用,因為他們可以讓程式在允許時等待外掛的載入。在Linux中,動態庫的檔案格式跟共享庫沒有區別,主要區別在於共享庫是程式啟動時載入,而動態載入庫是執行的過程中載入。

2.使用 
有專門的一組API用於完成開啟動態庫,查詢符號,處理出錯,關閉動態庫等功能。下面對這些介面函式逐一介紹:
(1) dlopen   
函式原型:void *dlopen(const char *libname,int flag); 
功能描述:dlopen必須在dlerror,dlsym和dlclose之前呼叫,表示要將庫裝載到記憶體,準備使用。 
如果要裝載的庫依賴於其它庫,必須首先裝載依賴庫。如果dlopen操作失敗,返回NULL值;如果庫已經被裝載過,則dlopen會返回同樣的控制代碼。 
引數中的libname一般是庫的全路徑,這樣dlopen會直接裝載該檔案;如果只是指定了庫名稱,在dlopen會按照下面的機制去搜尋:

  • 根據環境變數LD_LIBRARY_PATH查詢
  • 根據/etc/ld.so.cache查詢
  • 查詢依次在/lib和/usr/lib目錄查詢。

flag引數表示處理未定義函式的方式,可以使用RTLD_LAZY或RTLD_NOW。RTLD_LAZY表示暫時不去處理未定義函式,先把庫裝載到記憶體,等用到沒定義的函式再說;RTLD_NOW表示馬上檢查是否存在未定義的函式,若存在,則dlopen以失敗告終。

(2) dlerror 
函式原型:char *dlerror(void); 
功能描述:dlerror可以獲得最近一次dlopen,dlsym或dlclose操作的錯誤資訊,返回NULL表示無錯誤。dlerror在返回錯誤資訊的同時,也會清除錯誤資訊。

(3) dlsym 
函式原型:void *dlsym(void *handle,const char *symbol); 
功能描述:在dlopen之後,庫被裝載到記憶體。dlsym可以獲得指定函式(symbol)在記憶體中的位置(指標)。 
如果找不到指定函式,則dlsym會返回NULL值。但判斷函式是否存在最好的方法是使用dlerror函式,

(4) dlclose 
函式原型:int dlclose(void *); 
功能描述:將已經裝載的庫控制代碼減一,如果控制代碼減至零,則該庫會被解除安裝。如果存在解構函式,則在dlclose之後,解構函式會被呼叫。 

在Linux上,使用動態連結的應用程式需要和庫libdl.so一起連結,也就是使用選項-ldl。但是,編譯時不需要和動態裝載的庫一起連結。使用上述四個函式時,在連結時需要增加選項:

    gcc/cc xxx.c  -ldl


[舉例] 
下面,通過一個具體的例項,對前面介紹的三種庫的開發,使用,以及部署進行演示。
首先給出我們所需要的程式的原始碼: 
[[email protected] test]$ ls 
main.cpp  myfile.cpp  myfile.h 

一、不使用庫 
只要有一點Linux程式設計知識的讀者都會知道,有了這三個檔案,我們就可以通過"g++ main.cpp myfile.cpp"命令,生成我們的可執行檔案並執行。具體過程如下: 
[[email protected] test]# ls 
main.cpp  myfile.cpp  myfile.h 
[[email protected] test]# g++ main.cpp myfile.cpp 
[[email protected] test]# ls 
a.out  main.cpp  myfile.cpp  myfile.h 
[[email protected] test]# ./a.out 
begin test 
hello 
在上面生成可執行檔案的過程中,我們並沒使用任何庫的概念,也就是說我們將所檔案都做為可執行檔案的一個部分,最終生成了一個無論是編譯、連結,還是執行的時候,都不依賴於任何庫的,獨立執行的可執行檔案(實質上嚴格來講這個可執行檔案還是依賴庫的,至少它依賴iostream庫,這裡不考慮這些)。用下載過許多軟體的朋友們的“術語”來說,我們現在生成的程式,就是一個免安裝的“綠色”軟體^_^。如果程式的原始碼結構很複雜的化,那麼這樣編譯的缺點是非常明顯的,那就是這個程式採用非模組化的方式編譯,至少不滿足可重用性的特點。後面我們將介紹使用庫來實現模組化編譯這個程式。

二、使用庫 
我們將要用如下的方式組織我們的程式: 
1)對應我們應用程式,也就是可執行檔案的程式碼只有一個,那就是main.cpp檔案。 
2)我們的可執行檔案的生成或者執行,依賴外部的一些庫,例如iostream,以及myfile.h所對應的庫(名稱叫my),這裡我們重點關注my這個我們自己定義的庫。 
3)將myfile.h和myfile.cpp單獨進行編譯生成庫my,庫my的實現實際是通過myfile.cpp生成的;而myfile.h的作用就是讓使用這個庫的程式知道這個庫包含了什麼功能的函式,也就是說,庫的標頭檔案只是一個讓其它程式使用該庫的介面檔案。它存在的作用就是讓使用這個庫的程式通過#include "myfile.h"來包含,這樣就聲明瞭一些庫中必要的函式。 
這樣就實現了模組化的目的,將整個程式劃分為可執行檔案部分,以及可執行檔案所使用的庫這兩個相對獨立的部分。後面將講述如何真正在編譯執行的角度實現這種劃分的思想。

1、靜態庫方式 
採用如下方式進行: 
1.1生成靜態庫: 
1)只編譯myfile.cpp生成myfile.o 
[[email protected] test]$g++ -c myfile.cpp 
2)根據myfile.o生成庫libmy.a 
[[email protected] test]$ar r libmy.a myfile.o 
3)刪除myfile.o和myfile.cpp 
[[email protected] test]$rm myfile.cpp myfile.o 
這樣,我們第2步使用ar命令,將.o檔案打包新增到libmy.a中,打包生成的lib*.a檔案,就是靜態庫(相對的動態庫是lib*.so檔案)。關於ar命令,除了r選項還有c,s等選項: r表明將模組加入到靜態庫中,c表示建立靜態庫,s表示生產索引。具體參見ar命令的使用者手冊。另外這裡,為簡明起見,我們刪除了myfile.o和myfile.cpp檔案。

1.2使用靜態庫連結並執行: 
1)使用libmy.a進行編譯連線: 
[[email protected] test]$g++ main.cpp -L./ -lmy 
2)執行程式: 
[[email protected] test]$./a.out 
這裡,可以修改libmy.a的名字為libmy2.a,這樣就相應地用“g++ main.cpp -L./ -lmy2”進行連結。這樣只在編譯的時候需要對靜態庫進行連結,生成可執行檔案之後,靜態庫對這個程式而言就沒有用了(當然對別的程式的生成可能有用),至此生成的可執行檔案,也可以稱作是免安裝的“綠色軟體”^_^。
進一步的說明: 
這裡的-L選項,使用-L.表示將當前目錄加入到庫搜尋路徑。否則使用預設的庫搜尋路徑搜尋庫檔案,也就是/usr/lib目錄。 
另外個類似的容易混淆的引數-I, 它表示搜尋標頭檔案的路徑。使用它這樣gcc在查詢標頭檔案的時候會首先到-I指定的目錄查詢標頭檔案,然後才是系統預設目錄,也就是/usr/include。 
這裡的-l選項, -lname表示庫搜尋目錄下的libname.a 或者libname.so檔案 ,這也是為什麼庫檔案都以lib開頭的原因之一,如果你的庫檔案不是libmy,而是my. 那就不能用-l引數編譯了。 可以這樣: 
[[email protected] test]$g++ main.cpp -L. my.a -o test 
注意: $g++ -L. -lmy main.o -o test 會出錯!。 
原因是: -l是連結器選項,必須要放到被編譯檔案的後面。 所以上面的命令中-lmy一定要放到 main.o的後面。

2、共享庫方式 
採用如下方式進行: 
1.1生成共享庫: 
1)只編譯myfile.cpp生成myfile.o 
[[email protected] test]$g++ -c myfile.cpp 
2)根據myfile.o生成動態庫libmy.so 
[[email protected] test]$g++ -shared -fPCI -o libmy.so myfile.o 
3)刪除myfile.o和myfile.cpp 
[[email protected] test]$rm myfile.cpp myfile.o 
這裡,我們第2步生成的libmy.so就是動態連線庫(共享庫)。實踐發現使用gcc也行,還可用"g++ -shared -o libmy.so myfile.o"。 
進一步的說明:  -fpic或者-fPIC表明建立position independent code,這通常是建立共享庫必須的。另外,-Wl 表明給連結器傳送引數,所以這裡-soname, library_name 為給連結器的引數。-shared 表明是使用共享庫。 
下面是使用a.c和b.c建立共享庫的示例: 
gcc -fPIC -g -c -Wall a.c 
gcc -fPIC -g -c -Wall b.c 
gcc -shared -Wl,-soname, libmyab.so.1 -o libmyab.so.1.0.1 a.o b.o -lc 
說明: lc == libc 
還幾個需要注意的地方: 
a.不推薦使用strip處理共享庫,最好不要使用-fomit-frame-pointer編譯選項, 
b.-fPIC和-fpic都可以產生目標獨立程式碼,具體應用取決於平臺,-fPIC是always work, 
儘管其產生的目標檔案可能會大些; -fpic產生的程式碼小,執行速度快,但可能有平臺依賴限制。 
c.一般情況下,-Wall,-soname,your_soname編譯選項是需要的。當然,-share選項更不能丟。

1.2使用共享庫libmy.so進行連結: 
1)編譯連結: 
[[email protected] test]$g++ main.cpp -L./ -lmy 
這裡,不要和libmy.a衝突了,如果同時存在libmy.a和libmy.so會優先選擇libmy.so。編譯選項類似前面請參照連結靜態庫,時候的選項。

1.3執行時載入共享庫libmy.so: 
1)將動態庫移動到/usr/lib等標準路徑: 
[[email protected] test]$sudo cp libmy.so /usr/lib 
2)執行程式: 
[[email protected] test]$./a.out 
這裡注意和靜態庫不同,還需要把共享庫移動到特定的位置,因為共享庫在執行之後還有用。 
實際有三種方法來讓執行的程式可以載入到你的共享庫檔案: 
a)拷貝共享庫檔案到/usr/lib 
b)或設定環境變數LD_LIBRARY_PATH加上你共享庫檔案的路徑 
c) 或修改配置檔案/etc/ld.so.conf加入你共享庫檔案的路徑,並重新整理快取ldconfig 
至此生成的程式,就不是免安裝的"綠色"軟體了,因為想要你的程式執行在一臺機器上,除了可執行檔案本身,你還要將這個可執行檔案所依賴的庫拷貝到系統的某個目錄中以便執行的時候載入,這個拷貝的過程“汙染”了系統,所以說它不是"綠色"的。

[其它] 
1. nm命令可以查可能一個庫中的符號 
nm列出的符號有很多,常見的有三種,一種是在庫中被呼叫,但並沒有在庫中定義(表明需要其他庫支援),用U表示;一種是庫中定義的函式,用T表示,這是最常見的;另外一種是所謂的“弱態”符號,它們雖然在庫中被定義,但是可能被其他庫中的同名符號覆蓋,用W表示。 
*檢視庫中所有的符號 
$nm libhello.so

*假設開發者希望知道上文提到的 hello庫中是否定義了 printf(): 
$nm libhello.so |grep printf 
U printf 
U表示符號printf被引用,但是並沒有在函式內定義,由此可以推斷,要正常使用hello庫,必須有其它庫支援,再使用ldd命令檢視hello依賴於哪些庫: 
$ldd hello 
libc.so.6=>/lib/libc.so.6(0x400la000) 
/lib/ld-linux.so.2=>/lib/ld-linux.so.2 (0x40000000) 
從上面的結果可以繼續檢視printf最終在哪裡被定義.

2. ldd命令可以查詢一個程式依賴哪些共享庫: 
*檢視一個程式依賴那些共享庫: 
[[email protected] test]# ldd a.out 
linux-gate.so.1 =>  (0x00322000) 
libdl.so.2 => /lib/libdl.so.2 (0x002f3000) 
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x025ab000) 
libm.so.6 => /lib/libm.so.6 (0x002c8000) 
libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x0013d000) 
libc.so.6 => /lib/libc.so.6 (0x0016d000) 
/lib/ld-linux.so.2 (0x0014e000)