Linux下動態鏈接庫的創建和使用

 轉自:Linux下動態鏈接庫的創建和使用

http://blog.csdn.net/xlxxcc/article/details/51074150


1、鏈接庫的基本知識

    庫是一種軟件組件技術,庫裏面封裝了數據和函數。它的使用,可以是程序模塊化。在程序中使用,我們可以稱之爲程序函數庫。
    程序函數庫可分爲3種類型:靜態函數庫(static libraries)、共享函數庫(shared libraries)、動態函數庫(dynamically loaded libraries):
    1、靜態函數庫,是在程序執行前就加入到目標程序中去了;
    2、共享函數庫,則是在程序啓動的時候加載到程序中,它可以被不同的程序共享
    3、動態函數庫,並非另外一種庫函數格式,它只是使用動態加載方式加載共享函數庫。

    Windows系統包括靜態鏈接庫(.lib文件)和動態鏈接庫(.dll文件)。
    Linux通常把庫文件存放在/usr/lib或/lib目錄下
    Linux庫文件名由:前綴lib、庫名和後綴3部分組成,其中共享鏈接庫以.so.X最爲後綴, .X是版本號,靜態鏈接庫通常以.a作爲後綴。
    Linux下標準庫鏈接的三種方式(全靜態 , 半靜態 (libgcc,libstdc++), 全動態。 三種標準庫鏈接方式的選項及區別見下表。

三種標準庫鏈接方式的選項及區別:

標準庫鏈接方式示例連接選項優點缺點
全靜態-static -pthread -lrt -ldl不會發生不同Linux 版本下的標準庫不兼容問題生成的文件比較大,應用程序功能受限(不能調用動態庫等)
全動態-pthread -lrt -ldl生成的文件最小不同Linux版本下標準庫依賴不兼容問題
半靜態(libgcc,libstdc++)-static-libgcc -L. -pthread -lrt -ldl靈活度大,針對不同的標準庫採取不同的鏈接策略,從而避免不兼容問題發生難識別哪些庫容易發生不兼容問題,會因選擇的標準庫版本而喪失某些功能

上述三種標準庫鏈接方式中,比較特殊的是 半靜態鏈接方式,主要在於其還需要在鏈接前增加額外的一個步驟: 
ln -s ‘g++ -print-file-name=libstdc++.a’,作用是將 libstdc++.a(libstdc++ 的靜態庫)符號鏈接到本地工程鏈接目錄 
-print-file-name在gcc中的解釋如下: Display the full path to library

ldd 簡介:該命令用於打印出某個應用程序或者動態庫所依賴的動態庫 ,使用該命令我們可以觀察到Linux標準庫三種鏈接方式的區別。 
從實際應用當中發現,最理想的標準庫鏈接方式就是半靜態鏈接,通常會選擇將 libgcc 與 libstdc++ 這兩個標準庫靜態鏈接,從而避免應用程序在不同 Linux 版本間標準庫依賴不兼容的問題發生。

size 簡介:該命令用於顯示出可執行文件的大小

示例鏈接選項中所涉及命令(引用 GCC 原文):

  • -llibrary
  • -l library:指定所需要的額外庫
  • -Ldir:指定庫搜索路徑
  • -static:靜態鏈接所有庫
  • -static-libgcc:靜態鏈接 gcc 庫
  • -static-libstdc++:靜態鏈接 c++ 庫

2、靜態鏈接庫的創建和使用

   涉及命令:ar, ar是創建、修改、提取靜態庫的操作。
   ar -t 顯示靜態庫的內容
   ar -d 從庫中刪除成員文件
   ar -r 在庫中加入成員文件,若存在,則替換
   ar -c 創建一個庫
   ar -s 無論ar命令是否修改了庫內容,都強制重新生成庫符號表
   步驟如下:
   1、在一個頭文件種聲明靜態庫所導出的函數。
   2、在一個源文件種實現靜態庫所導出的函數。
   3、編譯源文件,生成可執行代碼。
   4、將可執行代碼所在的目標文件加入到某個靜態庫中,並將靜態庫拷貝到系統默認的存放庫文件的目錄下。

下面通過一個例子來說明:mylib.h種存放的是靜態庫提供給用戶使用的函數的聲明,mylib.c實現了mylib.h種聲明的函數。 
mylib.h

#ifndef _MYLIB_H_
#define _MYLIB_H_
void weclome(void);
void outString(const char *str);
#endif

mylib.cpp

#include "mylib.h"
void welcome(void){
    printf("welcome to libmylib\n");
}
void outString(const char *str){
    if(str != NULL)
        printf("%s\n", str);
}

test.cpp

#include "mylib.h"
#include 
int main(void){
    printf("create and use library:\n");
    welcome();
    outString("it's successful\n");
    return 0;
}
  1. 編譯mylib.c生成目標文件:gcc -o mylib.o -c mylib.cpp
  2. 將目標文件加入到靜態庫中:ar rcs libmylib.a mylib.o
  3. 將靜態庫copy到Linux的庫目錄(/usr/lib或者/lib)下:cp libmylib.a /usr/lib/libmylib.a
  4. 使用靜態庫編譯,編譯時無需帶上前綴和後綴:gcc -o test test.cpp -lmylib
  5. 運行可執行程序test: ./test

合併靜態鏈接庫的腳本代碼清單:

 echo CREATE demo.a > ar.mac 
 echo SAVE >> ar.mac 
 echo END >> ar.mac 
 ar -M < ar.mac 
 ar -q demo.a CdtLog.o 
 echo OPEN demo.a > ar.mac 
 echo ADDLIB xml.a >> ar.mac 
 echo SAVE >> ar.mac 
 echo END >> ar.mac 
 ar -M < ar.mac 
 rm ar.mac 

Linux makefile 中使用 ar 腳本方式進行靜態庫的創建,可以編寫如下代碼:

 define BUILD_LIBRARY 
 $(if $(wildcard $@),@$(RM) $@) 
 $(if $(wildcard ar.mac),@$(RM) ar.mac) 
 $(if $(filter %.a, $^), 
 @echo CREATE $@ > ar.mac 
 @echo SAVE >> ar.mac 
 @echo END >> ar.mac 
 @$(AR) -M < ar.mac 
 ) 
 $(if $(filter %.o,$^),@$(AR) -q $@ $(filter %.o, $^)) 
 $(if $(filter %.a, $^), 
 @echo OPEN $@ > ar.mac 
 $(foreach LIB, $(filter %.a, $^), 
 @echo ADDLIB $(LIB) >> ar.mac 
 ) 
 @echo SAVE >> ar.mac 
 @echo END >> ar.mac 
 @$(AR) -M < ar.mac 
 @$(RM) ar.mac 
 ) 
 endef 

 $(TargetDir)/$(TargetFileName):$(OBJS) 
    $(BUILD_LIBRARY) 
  • 1

Linux 靜態庫鏈接順序問題及解決方法: 
爲了解決這種庫鏈接順序問題,我們需要增加一些鏈接選項 : 
(CXX)(LINKFLAGS) (OBJS)Xlinker"("(LIBS) -Xlinker “-)” -o $@ 
通過將所有需要被鏈接的靜態庫放入 -Xlinker “-(” 與 -Xlinker “-)” 之間,可以是 g++ 鏈接過程中, 自動循環鏈接所有靜態庫,從而解決了原本的鏈接順序問題。

3、共享函數庫的創建和使用

   GNU標準建議所有的函數庫文件都放在/usr/local/lib目錄下,而且建議命令可執行程序都放在/usr/local/bin目錄下。
   文件系統層次化標準FHS(Filesystem Hierarchy Standard)規定了在一個發行包中大部分的函數庫文件應該安裝到/usr/lib目錄下,但是如果某些
庫是在系統啓動的時候要加載的,則放到/lib目錄下,而那些不是系統本身一部分的庫則放到/usr/local/lib下面。
   上面兩個路徑的不同並沒有本質的衝突。GNU提出的標準主要對於開發者開發源碼的,而FHS的建議則是針對發行版本的路徑的。具體的置信息可以看
/etc/ld.so.conf裏面的配置信息,通過對它的修改,可以增加自己的目錄。
   如果你想覆蓋某個庫中的一些函數,用自己的函數替換它們,同時保留該庫中其他的函數的話,你可以在 /etc/ld.so.preload中加入你想要替換的庫
(.o結尾的文件),這些preloading的庫函數將有優先加載的權ldconfig可以更新/etc/ld.so.cache。/etc/ld.so.cache可以大大提高訪問函數庫的速度。
   HP-UX系統下,就是用SHLIB_PATH這個變量,而在AIX下則使用LIBPATH這個變量。

共享函數庫創建的一個標準命令格式: 
gcc -shared -Wl,-soname,your_soname -o library_name file_list library_list

例子:

  • 1、創建Object文件: 
    gcc -fPIC -g -c -Wall a.c 
    gcc -fPIC -g -c -Wall b.c
  • 2、創建共享函數庫 
    gcc -shared -Wl,-soname,liblusterstuff.so.1 -o liblusterstuff.so.1.0.1 a.o b.o -lc

如果是C++項目,最簡單是使用Cmake來完成共享庫的創建,步驟如下:

  • 如果創建的是JNI鏈接庫,則需要將 jdk/include/jni.h 和 jdk/include/linux/jni_md.h 複製到 /usr/include 目錄下。反正執行make命令的時候將會報錯

  • 1、確保gcc-c++編譯環境, 安裝命令:: 
    yum install gcc-c++

  • 2、安裝Cmake 
    wget https://cmake.org/files/v3.5/cmake-3.5.1.tar.gz 
    tar -xvf cmake-3.5.1.tar.gz 
    cd cmake-3.5.1 
    ./bootstrap 
    make 
    make install

  • 3、如果您使用的windows系統,則將您的項目上傳到Linux,進入Linux下該項目的文件夾,創建CMakeLists.txt,內容格式如下:

CMAKE_MINIMUM_REQUIRED(VERSION 2.6)  
# cpp 文件  
SET(test_SRCS  
    source/test1.cpp  
    source/test2.cpp  
    ......
)  

# 頭文件
SET(test_HDRS  
    include/test1.h  
    include/test2.h  
    ..... 
)  

INCLUDE_DIRECTORIES(include)  

# test: 是生產的庫的名字, 這裏可以加上SHARED或者STATIC或者MODULE,分別表示動態庫、靜態庫、模塊。不加則默認是靜態庫
ADD_LIBRARY(test SHARED/STATIC/MODULE ${test_SRCS} ${test_HDRS}) 

# 生成可執行文件
# ADD_EXECUTABLE(test ${test_SRCS} ${test_HDRS})
  • 4、創建動態鏈接庫: 
    ccmake directory #用於配置編譯選項,如VTK_DIR目錄,一般這一步不需要配置 
    cmake directory #用於根據CMakeLists.txt生成Makefile文件 
    make #用於執行Makefile文件,編譯程序,生成可執行文件

共享函數庫的使用

   一旦你定義了一個共享函數庫,你還需要安裝它。其實簡單的方法就是拷貝你的庫文件到指定的標準的目錄(例如/usr/lib),然後運行ldconfig。
如果你沒有權限去做這件事情, 那麼最簡單的方法就是運行ldconfig:
    ldconfig -n directory_with_shared_libraries 
    然後設置LD_LIBRARY_PATH這個環境變量,它是一個以逗號分隔的路徑的集合:
    LD_LIBRARY_PATH=$LD_LIBRARY_PATH,my_program
    如果一個新版的函數庫要和老版本的二進制的庫不兼容,則soname需要改變。對於C語言,有四種情況會出現不兼容問題:
   · 一個函數的行文改變了,這樣它就可能與最開始的定義不相符合。
   · 輸出的數據項改變了。
   · 某些輸出的函數刪除了。
   · 某些輸出函數的接口改變了。**

4、共享函數庫的動態加載

   共享函數庫可以在程序運行過程中的任何時間加載,它們特別適合在函數中加載一些模塊和plugin擴展模塊的場合,因爲它可以在當程序需要某個
plugin模塊時才動態的加載。
   Linux系統下,DL函數庫與其他函數庫在格式上沒有特殊的區別。通常C語言環境下,需要包含這個頭文件。 Linux中使用的函數和Solaris中一樣,都是
dlpoen()API。當然不是所有的平臺都使用同樣的接口,例如HP-UX使用shl_load()機制,而Windows平臺用另外的其他的調用接口。

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'
   注意函數庫的加載順序。

dlerror()

    通過調用dlerror()函數,我們可以獲得最後一次調用dlopen(),dlsym(),或者dlclose()的錯誤信息。 

dlsym()

   void * dlsym(void *handle, char *symbol);
   函數中的參數handle就是由dlopen打開後返回的句柄,symbol是一個以NIL結尾的字符串。
   如果dlsym()函數沒有找到需要查找的symbol,則返回NULL。典型的調用過程如下:
dlerror();      /*clear error code */  
s = (actual_type)dlsym(handle,    symbol_being_searched_for);  
if((error = dlerror()) != NULL){  
    /* handle error, the symbol wasn't found */  
} else {  
    /* symbol found, its value is in s */  
}    

dlclose() 
dlopen()函數的反過程就是dlclose()數,dlclose()函數用力關閉一個DL函數庫。真正釋放的時候,如果函數庫裏面有_fini()這個函數,則自動調用_fini()這個函數,做一些必要的處理。Dlclose()返回0表示成功,其他非0值表示錯誤。

動態函數庫的創建: 
動態函數庫並非另外一種庫函數格式,可只是在程序運行的任何時候動態的加載的共享函數庫或。它的創建可以參考共享函數庫的創建。

動態函數庫的使用:

int main(int argc, char *argv){  
        void *handle;  
        char *error;  

        double (*cosine )(double);  
        handle = dlopen("/lib/libm.so.6", RTLD_LAZY);  
        if(!handle){  
            fputs(dlerror(), stderr);  
             exit(1);  
        }  

        cosine = dlsym(handle, "cos");  
        if((error = dlerror()) != NULL){  
            fputs(error, stderr);  
            exit(1);  
        }  

        printf("%f", (*cosine)(2, 0));  

        dlclose(handle);  

        return 0;  
}  

如果這個程序名字叫test.c,那麼用下面的命令來編譯: 
gcc -o test test.c –ldl


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章