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系列函數的使用和操作:
主程序:
- #include <stdlib.h>
- #include <dlfcn.h>
- #include <stdio.h>
- //申明結構體
- typedef struct __test {
- int i;
- void (* echo_fun)(struct __test *p);
- }Test;
- //供動態庫使用的注冊函數
- void __register(Test *p) {
- p->i = 1;
- p->echo_fun(p);
- }
- int main(void) {
- void *handle = NULL;
- char *myso = "./mylib.so";
- if((handle = dlopen(myso, RTLD_NOW)) == NULL) {
- printf("dlopen - %sn", dlerror());
- exit(-1);
- }
- return 0;
- }
動態庫:
- #include <stdio.h>
- #include <stdlib.h>
- //申明結構體類型
- typedef struct __test {
- int i;
- void (*echo_fun)(struct __test *p);
- }Test;
- //申明注冊函數原型
- void __register(Test *p);
- static void __printf(Test *p) {
- printf("i = %dn", p->i);
- }
- //動態庫申請一個全局變量空間
- //這種 ".成員"的賦值方式為c99標准
- static Test config = {
- .i = 0,
- .echo_fun = __printf,
- };
- //加載動態庫的自動初始化函數
- void _init(void) {
- printf("initn");
- //調用主程序的注冊函數
- __register(&config);
- }
主程序編譯: gcc test.c -ldl -rdynamic
動態庫編譯: gcc -shared -fPIC -nostartfiles -o mylib.so mylib.c
主程序通過dlopen()加載一個.so的動態庫文件, 然后動態庫會自動運行 _init() 初始化函數, 初始化函數打印一個提示信息, 然后調用主程序的注冊函數給結構體重新賦值, 然后調用結構體的函數指針, 打印該結構體的值. 這樣就充分的達到了主程序和動態庫的函數相互調用和指針的相互傳遞.
gcc參數 -rdynamic 用來通知鏈接器將所有符號添加到動態符號表中(目的是能夠通過使用 dlopen 來實現向后跟蹤).
gcc參數 -fPIC 作用: 當使用.so等類的庫時,當遇到多個可執行文件共用這一個庫時, 在內存中,這個庫就不會被復制多份,讓每個可執行文件一對一的使用,而是讓多個可執行文件指向一個庫文件,達到共用. 宗旨:節省了內存空間,提高了空間利用率.