Linux 動態庫相關知識總結

左小田發表於2015-11-13

動態庫和靜態庫在C/C++開發中很常見,相比靜態庫直接被編譯到可執行程式,動態庫執行時載入使得可執行程式的體積更小,更新動態庫可以不用重新編譯可執行程式等諸多好處。作者是一個Linux後臺開發,這些知識經常用到,所以整理了一下這方面的知識。靜態庫相對簡單,本文只關心Linux平臺下的動態庫。

建立動態庫

這裡我把一個短小卻很有用的雜湊函式編譯成動態庫做為示例,ELFhash用於對字串做雜湊,返回一個無符號整數。

//elfhash.h
#include 
unsigned long  ELFhash(const char* key);

//elfhash.c
#include "elfhash.h"
unsigned long ELFhash(const char* key)  
{
  unsigned long h = 0, g;
  while( *key ) {
    h = ( h > 24;
    h &= ~g;
}
  return h;
}

接下來使用gcc編譯以上程式碼,並用ld將編譯的目標檔案連結成動態庫

gcc -fPIC -c -Wall elfhash.c  
ld  -shared elfhash.o -o libelfhash.so

其中-fPIC意思是生成位置無關的程式碼(Position Independent Code),適用於動態庫,在多個程式中共享動態庫的同一份程式碼。ld的-shared選項告訴連結器建立的是動態庫。gcc也可以間接呼叫ld生成動態庫

gcc -fPIC -shared -Wall -o libelfhash.so elfhash.c

使用動態庫

動態庫的使用方法有兩種一種是隱式使用,第二種是顯式使用。隱式使用的方法很簡單。

#include "elfhash.h"
int main()  
{
    printf("%ldn", ElfHash("key-for-test"));
    return 0;
}

顯式使用動態庫需要藉助以下幾個函式

#include 
void *dlopen(const char *filename, int flag); //flag可以是RTLD_LAZY,執行共享庫中的程式碼時解決未定義符號,RTLD_NOW則是dlopen返回前解決未定義符號。  
char *dlerror(void); //當發生錯誤時,返回錯誤資訊  
void *dlsym(void *handle, const char *symbol); //獲取符號  
int dlclose(void *handle); //關閉

應用上面幾個函式,呼叫ELFhash實現跟隱式呼叫一樣的功能

#include "elfhash.h"
#include 
#include 

int main() {  
    void *handle;
    unsigned long (*hash)(const char*);
    char *error;
    handle = dlopen ("./libelfhash.so", RTLD_LAZY);
    if (!handle) {
        fputs (dlerror(), stderr);
        exit(1);
    }
    hash = dlsym(handle, "ElfHash");
    if ((error = dlerror()) != NULL)  {
         fputs(error, stderr);
         exit(1);
    }
    printf ("%ldn", (*hash)("key-for-test"));
    dlclose(handle);
}

至此瞭解以上的知識就可以建立和使用動態庫了。 實際應用中我們可能還是會遇到一些問題。

動態庫的載入

動態庫建立那一節,我演示如何隱式使用動態庫,那麼編譯執行這段程式碼試一下。

gcc main.c -L./ -lelfhash  
./a.out //執行可執行程式
//以下是輸出結果
./a.out: error while loading shared libraries: libelfhash.so: cannot open shared object file: No such file or directory

結果執行時報錯,可執行程式找不到動態庫。 網上有一些說法是編譯時設定-L選項,但在Linux上面證明是不行的(SunOS上可行),這個選項只能在編譯連結時有效, 可以讓你使用-l如上面的-lelfhash。使用readelf -d a.out可以看到可執行檔案依賴的動態庫資訊。

 0x0000000000000001 (NEEDED)  Shared library: [libelfhash.so]

可以看到這裡面並沒有包含動態庫的路徑資訊。查閱一下動態連結器的文件man ld-linux.so可以發現這樣一句話(有的沒有,版本問題)

If a slash is found, then the dependency string is interpreted as a (relative or absolute) pathname, and the library is loaded using that pathname

這段話太長,我只擷取一部分,大致就是說,當依賴中有/符號,那麼會被解析成動態庫載入的路徑,隱式使用的例子換一種編譯方法。

gcc main.c ./libelfhash.so  
./a.out
23621492 //輸出正常

再用readelf -d a.out檢視會發現,依賴資訊中有了一個路徑。

0x0000000000000001 (NEEDED)  Shared library: [./libelfhash.so]

這種方法雖然解決了問題,但是依賴中的路徑是硬編碼,不是很靈活。 動態連結器是如何查詢的動態庫的需要進一步查閱文件。關於查詢的順序有點長,這裡就不直接引用了,大致是這樣:

  • (僅ELF檔案) 使用可執行檔案中DT_RPATH區域設定的屬性,如果DT_RUNPATH被設定,那麼忽略DT_RPATH(在我的Linux對應的是RPATH和RUNPATH)。
  • 使用環境變數LD_LIBRARY_PATH,如果可執行檔案中有set-user-id/set-group-id, 會被忽略。
  • (僅ELF檔案) 使用可執行檔案中DT_RUNPATH區域設定的屬性
  • /etc/ld.so.cache快取檔案中查詢
  • 從預設路徑/lib, /usr/lib檔案目錄中查詢

我們需要設定RPATH或者RUNPATH,可以這樣做

gcc main.c -Wl,-rpath,/home/xxx,--enable-new-dtags -L./  -lelfhash

這裡的-Wl選項告訴連結器ld如果如何處理,接下來傳遞的-rpath(或者使用-R)告訴ld動態庫的路徑資訊(注意-Wl,和後面選項之間不能有空格)。如果沒有--enable-new-dtags那麼只會設定RPATH,反之,RPATH和RUNPATH會同時被設定。使用readelf -d a.out檢視結果:

0x000000000000000f (RPATH)  Library rpath: [/home/xxx]  
0x000000000000001d (RUNPATH)  Library runpath: [/home/xxx]

如果使用環境變數LD_LIBRARY_PATH,那麼一般這樣用 export

export LD_LIBRARY_PATH=/home/xxx;$LD_LIBRARY_PATH

RPATH和RUNPATH指定動態庫的路徑,用起來簡單,但是也缺乏靈活性,LDLIBRARYPATH在臨時測試的也是很有用的,但是在正式環境中,直接使用它也不是好的實踐,因為環境變數跟使用者的環境關係比較大。動態庫不僅要考慮自己使用, 還有分發給別的使用者使用的情況。

更通用的方法是使用ldconfig,有幾種方法,先在/etc/ld.so.conf.d/目錄下建立一個檔案,然後把你的動態庫路徑寫進去。或者將你的動態庫放到/lib,/lib64(64位),/usr/lib,/usr/lib64(64位)然後執行sudo ldconfig重建/etc/ld.so.cache檔案。

動態庫版本

通常在使用第三方給的動態庫的時候,都是帶有版本(檔案命名),可以在/usr/lib64下看到很多這樣的動態庫。現在我重新編譯動態庫,這次加上版本資訊。

gcc -fPIC -shared -Wall -Wl,-soname,libelfhash.so.0  -o libelfhash.so.0.0.0 elfhash.c

每個動態庫都有一個名字,如這裡的libelfhash.so.0.0.0,叫real name,命名規則跟簡單,通常是libxxx.so.MAJOR.MINOR.VERSION(有的時候VERSION會被省略),如果動態庫在介面上的相容性,比如刪除了介面或者修改了介面引數,MAJOR增加,如果介面相容,只是做了更新或者bug修復那麼MINOR和VERSION增加。也就是說MAJOR相同的庫介面都是相容的,反之不相容,如果使用不相容的動態庫需要重新編譯可執行程式。

編譯動態庫時,通過給ld傳遞連線選項-soname可以指定一個soname, 如這裡的libelfhash.so.0 只保留MAJOR,可執行程式執行載入動態庫時,會載入這個指定名字的庫。

動態庫還有一個名字是link name,編譯可執行程式時,傳個連結器ld的動態庫名字,通常是沒有版本號以.so結尾的檔名。 一般作法是對soname建立軟鏈。

按照這個規則來命名的動態庫可以ldconfig識別,我們把libelfhash.so.0.0.0放到/usr/lib64資料夾中,執行以下指令

$sudo ldconfig -v | grep libelfhash.so
libelfhash.so.0 -> libelfhash.so.0.0.0

可以發現ldconfig根據libelfhash.so.0.0.0的資訊,建立了一個soname指向real name的軟鏈,當動態庫更新(MINOR,VERSION增加),拷貝新庫到相應的位置,再執行sudo ldconfig會自動更新軟鏈指向最新的動態庫,動態庫更新就完成了。

總結

OK,關於Linux動態庫知識整理就到這裡了,這些知識雖說都是些基礎,少有涉及動態庫內部的一些原理,但是卻很常用。整理過程中我帶著疑問去閱讀了ldld-linux.so的文件,收穫頗豐。同樣,希望本文能幫你解釋遇到的部分問題或疑惑。

相關文章