1. 程式人生 > >linux中動態載入動態庫的方法

linux中動態載入動態庫的方法

  dlopen()是一個強大的庫函式。該函式將開啟一個新庫,並把它裝入記憶體。該函式主要用來載入庫中的符號,這些符號在編譯的時候是不知道的。比如 Apache Web 伺服器利用這個函式在執行過程中載入模組,這為它提供了額外的能力。一個配置檔案控制了載入模組的過程。這種機制使得在系統中新增或者刪除一個模組時,都不需要重新編譯  可以在自己的程式中使用 dlopen()。dlopen() 在 dlfcn.h 中定義,並在 dl 庫中實現。它需要兩個引數:一個檔名和一個標誌。檔名可以是我們學習過的庫中的 soname。標誌指明是否立刻計算庫的依賴性。如果設定為 RTLD_NOW 的話,則立刻計算;如果設定的是 RTLD_LAZY,則在需要的時候才計算。另外,可以指定 RTLD_GLOBAL,它使得那些在以後才載入的庫可以獲得其中的符號。

  當庫被裝入後,可以把 dlopen() 返回的控制代碼作為給 dlsym() 的第一個引數,以獲得符號在庫中的地址。使用這個地址,就可以獲得庫中特定函式的指標,並且呼叫裝載庫中的相應函式。

dlsym()的函式原型是void* dlsym(void* handle,const char* symbol)該函式在<dlfcn.h>檔案中。handle是由dlopen開啟動態連結庫後返回的指標,symbol就是要求獲取的函式或全域性變數的名稱,函式返回值是void*,指向函式的地址,供呼叫使。

dlclose(void *handle))用於關閉指定控制代碼的動態連結庫,只有當此動態連結庫的使用計數為0時,才會真正被系統解除安裝。

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

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'。如果有好幾個函式庫,它們之間有一些依賴關係的話,例如X依賴Y,那麼你就要先載入那些被依賴的函式。例如先載入Y,然後載入X。dlopen()函式的返回值是一個控制代碼,然後後面的函式就通過使用這個控制代碼來做進一步的操作。如果開啟失敗dlopen()就返回一個NULL。如果一個函式庫被多次開啟,它會返回同樣的控制代碼。如果一個函式庫裡面有一個輸出的函式名字為_init,那麼_init就會在dlopen()這個函式返回前被執行。我們可以利用這個函式在我的函式庫裡面做一些初始化的工作。我們後面會繼續討論這個問題的。

dlerror() :通過呼叫dlerror()函式,我們可以獲得最後一次呼叫dlopen(),dlsym(),或者dlclose()的錯誤資訊。

/*************************************************************************************************************************************************************************************/ Linux提供了一套API來動態裝載庫。下面列出了這些API:

- dlopen,開啟一個庫,併為使用該庫做些準備。
- dlsym,在開啟的庫中查詢符號的值。
- dlclose,關閉庫。
- dlerror,返回一個描述最後一次呼叫dlopen、dlsym,或dlclose的錯誤資訊的字串。

C語言使用者需要包含標頭檔案dlfcn.h才能使用上述API。glibc還增加了兩個POSIX標準中沒有的API:
- dladdr,從函式指標解析符號名稱和所在的檔案。
- dlvsym,與dlsym類似,只是多了一個版本字串引數。

在Linux上,使用動態連結的應用程式需要和庫libdl.so一起連結,也就是使用選項-ldl。但是,編譯時不需要和動態裝載的庫一起連結。程式3-1是一個在Linux上使用dl*例程的簡單示例。



延遲重定位(Lazy Relocation)
延遲重定位/裝載是一個允許符號只在需要時才重定位的特性。這常在各UNIX系統上解析函式呼叫時用到。當一個和共享庫一起連結的應用程式幾乎不會用到該共享庫中的函式時,該特性被證明是非常有用的。這種情況下,只有庫中的函式被應用程式呼叫時,共享庫才會被裝載,否則不會裝載,因此會節約一些系統資源。但是如果把環境變數LD_BIND_NOW設定成一個非空值,所有的重定位操作都會在程式啟動時進行。也可以在連結器命令列通過使用-z now連結器選項使延遲繫結對某個特定的共享庫失效。需要注意的是,除非重新連結該共享庫,否則對該共享庫的這種設定會一直有效。


初始化(initializing)和終止化(finalizing)函式
有時候,以前的程式碼可能用到了兩個特殊的函式:_init和_fini。_init和_fini函式用在裝載和解除安裝某個模組(註釋14)時分別控制該模組的構造器和析構器(或建構函式和解構函式)。他們的C語言原型如下:
void _init(void);
void _fini(void);
當一個庫通過dlopen()動態開啟或以共享庫的形式開啟時,如果_init在該庫中存在且被輸出出來,則_init函式會被呼叫。如果一個庫通過dlclose()動態關閉或因為沒有應用程式引用其符號而被解除安裝時,_fini函式會在庫解除安裝前被呼叫。當使用你自己的_init和_fini函式時,需要注意不要與系統啟動檔案一起連結。可以使用GCC選項 -nostartfiles 做到這一點。
但是,使用上面的函式或GCC的-nostartfiles選項並不是很好的習慣,因為這可能會產生一些意外的結果。相反,庫應該使用__attribute__((constructor))和__attribute__((destructor))函式屬性來輸出它的建構函式和解構函式。如下所示:
void __attribute__((constructor)) x_init(void)
void __attribute__((destructor)) x_fini(void)
建構函式會在dlopen()返回前或庫被裝載時呼叫。解構函式會在這樣幾種情況下被呼叫:dlclose()返回前,或main()返回後,或裝載庫過程中exit()被呼叫時。

我們通過一個例子來講解dlopen系列函式的使用和操作:

主程式:

  1. #include <stdlib.h>
  2. #include <dlfcn.h>
  3. #include <stdio.h>
  4. //申明結構體
  5. typedef struct __test {
  6.     int i;
  7.     void (* echo_fun)(struct __test *p);
  8. }Test;
  9. //供動態庫使用的註冊函式
  10. void __register(Test *p) {
  11.     p->= 1;
  12.     p->echo_fun(p);
  13. }
  14. int main(void) {
  15.     void *handle = NULL;
  16.     char *myso = "./mylib.so";
  17.     if((handle = dlopen(myso, RTLD_NOW)) == NULL) {
  18.         printf("dlopen - %sn", dlerror());
  19.         exit(-1);
  20.     }
  21.     return 0;
  22. }

動態庫:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. //申明結構體型別
  4. typedef struct __test {
  5.     int i;
  6.     void (*echo_fun)(struct __test *p);
  7. }Test;
  8. //申明註冊函式原型
  9. void __register(Test *p);
  10. static void __printf(Test *p) {
  11.     printf("i = %dn", p->i);
  12. }
  13. //動態庫申請一個全域性變數空間
  14. //這種 ".成員"的賦值方式為c99標準
  15. static Test config = {
  16.     .= 0,
  17.     .echo_fun = __printf,
  18. };
  19. //載入動態庫的自動初始化函式
  20. void _init(void) {
  21.     printf("initn");
  22.     //呼叫主程式的註冊函式
  23.     __register(&config);
  24. }

主程式編譯: gcc test.c -ldl -rdynamic

動態庫編譯: gcc -shared -fPIC -nostartfiles -o mylib.so mylib.c

主程式通過dlopen()載入一個.so的動態庫檔案, 然後動態庫會自動執行 _init() 初始化函式, 初始化函式列印一個提示資訊, 然後呼叫主程式的註冊函式給結構體重新賦值, 然後呼叫結構體的函式指標, 列印該結構體的值. 這樣就充分的達到了主程式和動態庫的函式相互呼叫和指標的相互傳遞.

gcc引數 -rdynamic 用來通知連結器將所有符號新增到動態符號表中(目的是能夠通過使用 dlopen 來實現向後跟蹤).

gcc引數 -fPIC 作用: 當使用.so等類的庫時,當遇到多個可執行檔案共用這一個庫時, 在記憶體中,這個庫就不會被複制多份,讓每個可執行檔案一對一的使用,而是讓多個可執行檔案指向一個庫檔案,達到共用. 宗旨:節省了記憶體空間,提高了空間利用率.