動態載入函式庫 (Dynamically Lodaded Libraries)
簡介
動態載入函式庫是一種在需要的時候才載入的函式庫。在實作外掛或者模組時特別有用,當程式需要用到的時候才載入外掛或模組。舉例來說,Pluggable Authenticaltion Modules (PAM) 使用動態載入函式庫來允許管理者設定或重新設定認證。
從 Linux 檔案格式的觀點來看,動態載入函式庫與共用函式庫並沒有什麼差別,它被編譯成標準的目的檔或標準的共用函式庫。主要的不同在於當程式連結或啟動時,函式庫並不會自動被載入,相反的,是由 API 來開啟,尋找符號,處理錯誤與關閉函式庫,C 語言需要加入<dlfcn.h>
這個標頭檔來使用這些 API。
Linux 使用的介面基本上跟 Solaris 一樣,都是 dlopen(),但是並不是所有的平臺都支援。HP-UX 使用 shl_load() 的機制而 Windows 使用完全不同的 DLLs 介面,如果考慮到平臺間的可攜性,可能就必須考慮使用隱藏不同平臺間差異的包裝函式庫,一種方法是使用 glib,glib 使用平臺基本的動態載入函式實作可攜性的介面,在 glib 的網站可以得到更多的相關資訊 Dynamic Loading of Modules,另一種方法是使用 libltdl,它是 GNU libtool 的一部分,想知道更多資訊的話可以去尋找 CORBA Object Request Broker (ORB) 的資料,這篇文章主要介紹 Linux 和 Solaris 提供的介面。
dlopen()
dlopen 函式開啟一個函式庫以供使用,在 C 的原型是:
void* dlopen(const char* filename, int flag);
第一個參數是函式庫的路徑,如果 filename 是由 “/“ 開始 (絕對路徑),dlopen() 會直接嘗試開啟,不會再去其它資料夾尋找。否則的話,dlopen() 會根據以下的順序尋找函式庫
使用者設定的 LD_LIBRARY_PATH 環境變數,每個目錄之間由冒號隔開
/etc/ld.so.cache 裡面所存的目錄 (由 /etc/ld.so.conf 產生)
/lib 然後是 /usr/lib,在這裡有一個値得注意的地方,有些舊的 a.out 載入器搜尋的順序是相反的,它會先搜尋 /usr/lib 再去 /lib 找,這在正常情況下應該是沒有問題的,因為一個函式庫應該只會出現在其中一個目錄下,如果不同的函式庫而有同樣的名稱,可能會造成一些無法預期的錯誤。
第二個參數 flag 的值一定要是 RTLD_LAZY (函式符號被用到才會重定位) 或 RTLD_NOW (在 dlopen() 返回前,重定位所有未定義的符號,失敗會回傳錯誤) 其中一種。RTLD_GLOBAL 可以與 flag 同時使用,代表在這個函式庫重定位的符號,之後可以給其他函式庫使用。在除錯的時候,傾向使用 RTLD_NOW,因為沒被定義的符號會馬上被發現,RTLD_LAZY 可能會造成無法預期的錯誤。使用 RTLD_NOW 會讓函式庫開起的時間稍微增加 (但是之後尋找的速度會加快)。
dlopen() 的回傳值是一個 handle 值,對其他函式庫來說應該是不可見的。一旦函式庫開啟失敗,dlopen() 會傳回 NULL,所以必須檢查傳回的值。如果同樣的函式庫被 dlopen() 打開不只一次,會傳回同樣的 handle。
開啟的函式庫如果有 init 這個符號,會在 dlopen() 回傳之前執行 init()。但是,函式庫不應該匯出 _init 這個符號,因為這些機制是過時的,而且可能造成無法預期的行為。相反的,函式庫應該使用 __attribute((constructor)) 這個函式屬性來匯出函式 (假設使用 gcc)。
如果函式庫依賴於其它的函式庫 (X 依賴 Y),必須先載入被依賴的函式庫 (先載入 Y,再載入 X)。
dlerror()
呼叫 dlerror() 可以取得相關的錯誤資訊,會回傳一個字串描述最近一次呼叫的 dlopen(),dlsym(),或者 dlclose()。一個比較特別的行為是當呼叫了 dlerror() 之後,之後呼叫 dlerror() 都會傳回 NULL 直到另一個錯誤發生。
dlsym()
載入一個函式庫而不去使用是沒有意義的,主要使用函式庫的函式是 dlsym,這個函式會在已經打開的函式庫內尋找符號,函式被定義成:
void* dlsym(void* handle, char* symbol);
handle 是從 dlopen 回傳的値,symbol 是一個由 ‘\0’ 結束的字串。如果可以避免的話,不要把 dlsym() 回傳的値存成 void* pointer,因為這樣會導致之後每次需要使用時都必須轉型一次 (對之後要維護的人來說可能會遺失一些資訊)。
如果符號沒有被找到,dlsym() 會回傳 NULL。如果知道符號不會是 NULL 或 0,那可能沒有問題,否則可能會有模稜兩可的情況,如果回傳值是 NULL,代表沒有找到符號,還是這個符號的值就是 NULL 呢,標準的做法是呼叫 dlerror() 函式 (需要先清空之前發生的錯誤),然後再呼叫 dlsym() 尋找符號,最後再呼叫 dlerror() 看是否有錯誤發生。程式片段看起來可能像這樣:
dlerror(); /* clear error code */
s = (actual_type) dlsym(handle, symbol_being_searched_for);
if ((err = dlerror()) != NULL) {
/* handle error, the symbol wasn't found */
} else {
/* symbol found, its value is in s */
}
dlclose()
dlopen() 的相反是 dlclose(),也就是關閉一個函式庫。如果一個函式庫被釋放,_fini() 函式會被呼叫 (如果存在的話),前面有提到這是一個過時的機制,因此不應該繼續使用。相反的,函式庫應該使用 __attribute((destructor)) 這個函式屬性來匯出函式。
Note:dlclose() 成功會回傳 0,失敗傳回非 0 値,一些 Linux 的 man page 沒有提到這個。
DL Library Example
這個範例開啟第一個參數指定的函式庫,尋找第二個參數指定的符號,並且在每個步驟中都確認是否有錯誤發生
- dltest.c
#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
int main(int argc, char const* argv[])
{
void* handle;
char* (*fp)();
char* error;
handle = dlopen(argv[1], RTLD_LAZY);
if (handle == NULL) {
fprintf(stderr, "Load error %s: %s\n", argv[1], dlerror());
exit(1);
}
dlerror(); /* clear error */
fp = (char* (*)()) dlsym(handle, argv[2]);
if ((error = dlerror()) != NULL) {
fprintf(stderr, "dlsym error %s: %s\n", argv[2], error);
exit(1);
}
printf("%s\n", (*fp)());
dlclose(handle);
}
建立執行檔
gcc -o dltest dltest.c -ldl
測試
./dltest libc.so.6 tmpnam