C語言的本質(35)——共享庫

尹成發表於2014-07-24


庫用於將相似函式打包在一個單元中。然後這些單元就可為其他開發人員所共享,並因此有了模組化程式設計這種說法— 即,從模組中構建程式。Linux支援兩種型別的庫,每一種庫都有各自的優缺點。靜態庫包含在編譯時靜態繫結到一個程式的函式。動態庫則不同,它是在載入應用程式時被載入的,而且它與應用程式是在執行時繫結的。

 使用共享庫的方法有兩種:您既可以在執行時動態連結庫,也可以動態載入庫並在程式控制之下使用它們。本文對這兩種方法都做了探討。

 靜態庫較適宜於較小的應用程式,因為它們只需要最小限度的函式。而對於需要多個庫的應用程式來說,則適合使用共享庫,因為它們可以減少應用程式對記憶體(包括執行時中的磁碟佔用和記憶體佔用)的佔用。這是因為多個應用程式可以同時使用一個共享庫;因此,每次只需要在記憶體上覆制一個庫。要是靜態庫的話,每一個執行的程式都要有一份庫的副本。

 GNU/Linux 提供兩種處理共享庫的方法(每種方法都源於Sun Solaris)。您可以動態地將程式和共享庫連結並讓 Linux 在執行時載入庫(如果它已經在記憶體中了,則無需再載入)。另外一種方法是使用一個稱為動態載入的過程,這樣程式可以有選擇地呼叫庫中的函式。使用動態載入過程,程式可以先載入一個特定的庫(已載入則不必),然後呼叫該庫中的某一特定函式(圖 2 展示了這兩種方法)。這是構建支援外掛的應用程式的一個普遍的方法。我稍候將在本文探討並示範該應用程式程式設計介面(API)。

 

Linux下的共享庫類似windows下的dll,共命令約定如下:

 

靜態庫一般由字母lib 開頭,並有 .a 的副檔名,而共享物件有兩個不同的名稱:soname 和 real name。

 

soname 包含字首 "lib",然後緊跟庫名,其次是".so"(後面緊跟另一個圓點),以及表明主版本號的數字。

 

soname 可以由字首的路徑資訊來限定。realname 是包含庫的已編譯程式碼的真正檔名。

 

real name 在 soname 後新增一個圓點、小的數字、另外一個圓點和釋出號。格式如下:

 

libxxxx.so.major.minor

 

其中,xxxx是庫的名字,major是主版本號,minor 是次版本號或叫釋出號,次版本號和其相應的圓點是可選的。

 

soname是記錄在共享庫中的,其它庫使用這個共享庫時,實際上只需要的提供soname,動態連結器會找到名稱是soname的動態庫給程式使用。

 

這種帶版本號的共享庫主要是為了你可以很方便的升級你的函式庫,如果某個API改變了,建立庫的程式會改變主版本號,然而,如果一個函式升級了某個函式庫,而功能沒有發生變化,這時只需要改變次版本號,由於只改變了次版本號,所以soname沒有發生改變,這樣就可以做到與舊的共享庫保持相容。

 

下面簡要說明動態庫的編寫過程:

 

/* file libhello.h - for example use! */
void printhello();


庫的程式碼很基本,在下一個清單中顯示。

 

/* file libprint.c */
#include "stdio.h"
void printhello()
{
   printf("hello opendba/n");
}


 編譯:

gcc -fPIC -c libhello.c
ld -shared -soname libhello.so.1 -olibhello.so.1.0 -lc libhello.o

-soname也可以用-h代替。

 

注意,gcc 命令列中的 -fPIC 選項。這是生成 Position-Independent Code 所必須要的。把這個命令翻譯出來就是:生成可以在程式的程式空間的任何地方載入的程式碼。這對於共享物件是非常重要的。使用這個選項,使得必須執行重定位的數量降低到最少。一旦載入可執行程式使用的共享物件,就必須給它分配一些空間。必須給文字和資料部分配一些位置。如果它們不是以“位置獨立”方式來構建,那麼載入共享物件時,程式要做大量的重定位,這會影響到效能。

 

現在我們分析一下傳給 ld 的選項。-shared 選項表明輸出的檔案被認為是共享的庫。通過 -soname name 選項,可以指定 soname 是什麼。-o name 指定了共享物件的real name,也就是實際生成的動態庫的檔名稱。

 

為了讓動態連結庫為系統所共享,還需執行動態連結庫的管理命令--ldconfig

ldconfig 命令的用途,主要是在預設搜尋目錄(/lib和/usr/lib)以及動態庫配置檔案/etc/ld.so.conf內所列的目錄下,搜尋出可共享的動態連結庫(格式如前介紹,lib*.so*),進而建立出動態裝入程式(ld.so)所需的連線和快取檔案.快取檔案預設為  /etc/ld.so.cache,此檔案儲存已排好序的動態連結庫名字列表.

 

ldconfig -p 輸出共享庫soname實際對應的共享庫的檔名稱。

 

  一個程式/shared庫一般都要依賴其他的一些庫,這可以用ldd來檢視,它列出了依賴的庫的soname,因為實際依賴是庫的介面,而 soname正是反映了庫的介面資訊。linux使用ELF作為可執行程式和庫的格式,這些依賴的庫的soname儲存在ELF的某個fileld裡。當一個可執行程式執行時,ld.so負責把它所依賴的shared庫載入到記憶體並連結,它按照以下順序尋找shared庫:

 

   1. 在LD_LIBRARY_PATH環境變數指定的目錄下

 2. ld.so.cache檔案該shared庫對應的檔案

 3. /usr/lib和/lib目錄下

 

環境變數:

 

LD_BIND_NOW --- 正常來講,函式在呼叫之前是不會讓程式尋找(looked up)的.設定這個旗號會使得程式庫一載入,所有的尋找(lookups)便會發生,同時也造成起始的時間(startup time)較慢.當你想測試程式,確定所有的連結都沒有問題時,這項旗號就變得很有用.

LD_PRELOAD 可以設定一個檔案,使其具有*覆蓋*(overriding)函式定義的能力.例如,如果你要測試記憶體分配的方略(strategies),而且還想置換*malloc*,那麼你可以寫好準備替換的副程式(routine),並把它編譯成mallolc. ,然後:

$LD_PRELOAD=malloc.o; export LD_PRELOAD
$ some_test_program
 

LD_ELF_PRELOAD  與LD_AOUT_PRELOAD  很類似,但是僅適用於正確的二進位型態.如果設定了 LD_ something _PRELOAD  與LD_PRELOAD  ,比較明確的那一個會被用到.

LD_LIBRARY_PATH  是一連串以分號隔離的目錄名稱,用來搜尋共享程式庫.對ld而言,並沒有 任何的影響;這項只有在執行期間才有影響.另外,對執行setuid與setgid的程式而言,這一項是無效的.而LD_ELF_LIBRARY_PATH與LD_AOUT_LIBRARY_PATH 這兩種旗號可根據各別的二進位型式分別導向不同的搜尋路徑.一般正常的運作下,不應該會用到LD_LIBRARY_PATH ;把需要搜尋的目錄加到/etc/ld.so.conf/ 裡;然後重新執行ldconfig.

LD_NOWARN 僅適用於a.out.一旦設定了這一項(LD_NOWARN=true; export LD_NOWARN ),它會告訴載入器必須處理fatal-warnings(像是次要版本不相容等)的警告訊息.

LD_WARN 僅適用於ELF.設定這一項時,它會將通常是致命訊息的"Can*t find library"轉換成警告訊息.對正常的操作而言,這並沒有多大的用處,可是對ldd就很重要了.

LD_TRACE_LOADED_OBJECTS  僅適用於ELF.而且會使得程式以為它們是由ldd所執行的:

 $LD_TRACE_LOADED_OBJECTS=true /usr/bin/lynx
       libncurses.so.1 => /usr/lib/libncurses.so.1.9.6
       libc.so.5 => /lib/libc.so.5.2.18


 

相關文章