DLOPEN DLMOPEN DLCLOSE
NAME
dlclose, dlopen, dlmopen - 打開/關閉共享對象
SYNOPSIS
#include <dlfcn.h>
void *dlopen(const char *filename, int flags);
int dlclose(void *handle);
#define _GNU_SOURCE
#include <dlfcn.h>
void *dlmopen (Lmid_t lmid, const char *filename, int flags);
DESCRIPTION
dlopen()
這個函數加載由以null結尾的字符串文件名命名的動態共享對象(共享庫)文件,並為加載的對象返回不透明的“句柄”。此句柄與 dlopen API 中的其他函數一起使用,例如dlsym()
,dladdr()
,dlinfo()
和dlclose()
。
如果 filename 為 NULL,則返回的句柄用於主程序。如果 filename 包含斜杠(“/”),則它被解釋為(相對或絕對)路徑名。否則,動態鏈接器將按如下方式搜索對象(有關詳細信息,請參閱ld.so(8)
):
- (僅限ELF)如果調用程序的可執行文件包含 DT_RPATH 標記,並且不包含 DT_RUNPATH 標記,則會搜索 DT_RPATH 標記中列出的目錄。
- 如果在程序啟動時,環境變量 LD_LIBRARY_PATH 被定義為包含以冒號分隔的目錄列表,則會搜索這些目錄。 (作為安全措施,set-user-ID 和 set-group-ID程序將忽略此變量。)
- (僅限ELF)如果調用程序的可執行文件包含 DT_RUNPATH 標記,則搜索該標記中列出的目錄。
- 檢查緩存文件/etc/ld.so.cache(由ldconfig(8)維護)以查看它是否包含filename的條目。
- 搜索目錄 /lib和 /usr/lib(按此順序)。
如果 filename 指定的對象依賴於其他共享對象,則動態鏈接器也會使用相同的規則自動加載這些對象。 (如果這些對象依次具有依賴性,則此過程可以遞歸地發生)
flags 參數必須包括以下兩個值中的一個:
- RTLD_LAZY
執行延遲綁定。僅在執行引用它們的代碼時解析符號。如果從未引用該符號,則永遠不會解析它(只對函數引用執行延遲綁定;在加載共享對象時,對變量的引用總是立即綁定)。自 glibc 2.1.1,此標志被LD_BIND_NOW環境變量的效果覆蓋。 - RTLD_NOW
如果指定了此值,或者環境變量LD_BIND_NOW設置為非空字符串,則在dlopen()
返回之前,將解析共享對象中的所有未定義符號。如果無法執行此操作,則會返回錯誤。
flags 也可以通過以下零或多個值進行或運算設置:
- RTLD_GLOBAL
此共享對象定義的符號將可用於后續加載的共享對象的符號解析。 - RTLD_LOCAL
這與RTLD_GLOBAL相反,如果未指定任何標志,則為默認值。此共享對象中定義的符號不可用於解析后續加載的共享對象中的引用。 - RTLD_NODELETE (since glibc 2.2)
在dlclose()
期間不要卸載共享對象。因此,如果稍后使用dlopen()
重新加載對象,則不會重新初始化對象的靜態變量。 - RTLD_NOLOAD (since glibc 2.2)
不要加載共享對象。這可用於測試對象是否已經駐留(如果不是,則dlopen()
返回 NULL,如果是駐留則返回對象的句柄)。此標志還可用於提升已加載的共享對象上的標志。例如,以前使用RTLD_LOCAL加載的共享對象可以使用RTLD_NOLOAD | RTLD_GLOBAL重新打開。 - RTLD_DEEPBIND (since glibc 2.3.4)
將符號的查找范圍放在此共享對象的全局范圍之前。這意味着自包含對象將優先使用自己的符號,而不是全局符號,這些符號包含在已加載的對象中。
dlmopen()
這個函數除了以下幾點與dlopen()
有所不同外,都執行同樣的任務。
dlmopen()
與dlopen()
的主要不同之處主要在於它接受另一個參數 lmid,它指定應該被加載的共享對象的鏈接映射列表(也稱為命名空間)。對於命名空間,Lmid_t 是個不透明的句柄。
lmid 參數要么是已經存在的命名空間的ID(這個命名空間可以通過dlinfo RTLD_DI_LMID
請求獲得)或者是以下幾個特殊值中的其中一個:
- LM_ID_BASE
在初始命名空間中加載共享對象(即應用程序的命名空間)。 - LM_ID_NEWLM
創建新的命名空間並在該命名空間中加載共享對象。該對象必須已正確鏈接到引用 所有其他需要的共享對象,因為新的命名空間最初為空。
如果 filename 是 NULL,那么 lmid 的值只能是LM_ID_BASE。
dlclose()
dlclose()
減少指定句柄 handle 引用的動態加載共享對象的引用計數。如果引用計數減少為0,那么這個動態加載共享對象將被真正卸載。所有在dlopen()
被調用的時候自動加載的共享對象將以相同的方式遞歸關閉。
dlclose()
成功返回並不保證與句柄相關的符號將從調用方的地址空間中刪除。除了顯式通過dlopen()
調用產生的引用之外,一些共享對象作為依賴項可能已被隱式加載(和引用計數)。只有當所有引用都已被釋放才可以從地址空間中刪除共享對象。
RETURN VALUE
執行成功時,dlopen()
和dlmopen()
返回一個非空句柄。
執行失敗時(文件找不到、不可讀、錯誤的格式或者在加載的時候出現錯誤),dlopen()
和dlmopen()
返回 NULL。
對於dlclose()
成功執行,將返回0值,失敗時,返回一個非0值。
以上這些函數產生的錯誤,其錯誤信息都可以通過dlerror()
獲知。
NOTES
dlmopen() 與 命名空間
鏈接映射列表定義了通過動態鏈接器解析的符號的孤立命名空間。在命名空間內,被依賴的共享對象根據通常的規則被隱式加載,符號引用同樣以通常的規則被解析。但是這種方案受限於已經被(顯式和隱式)加載進命名空間的對象的定義。
dlmopen()
函數允許對象隔離加載——在新的命名空間中加載共享對象而不暴露其余的應用於新對象提供的符號。注意使用RTLD_LOCAL標志不足以達到此目的,因為它防止一個共享對象的符號對任何其他共享對象可用。在某些情況下,我們可能想使得由一些動態加載共享對象提供的符號對於其他共享對象可用,而不將這些符號暴露給整個應用。這可以通過使用單獨的命名空間和RTLD_GLOBAL標志來實現。
dlmopen()
函數可以提供比RTLD_LOCAL標志更好的隔離效果。特別是,當共享對象是通過RTLD_LOCAL標志加載的,並且其依賴的共享對象是通過RTLD_GLOBAL加載的,那么有可能升級為RTLD_GLOBAL。因此,明確控制了所有共享對象的依賴的這種情況外,RTLD_LOCAL是不足以隔離加載的共享對象,。
dlmopen()
函數的一種用法是多次加載同樣的對象。不使用dlmopen()
函數來實現這個功能的話,需要創建共享對象的一個副本。而如果使用dlmopen()
函數來實現的話,可以通過將相同的共享對象文件加載到不同的命名空間來實現。
glibc實現最多支持16個命名空間。
初始化和終結功能
共享對象可以使用__attribute __((constructor))和__attribute __((destructor))函數屬性。構造函數在dlopen()
返回之前執行,而析構函數在dlclose()
返回之前執行。共享對象可以導出多個構造函數和析構函數並且優先順序可以和每個函數相關聯來決定它們的執行順序。
DLSYM
NAME
dlsym, dlvsym - 獲取共享對象或可執行文件中符號的地址
SYNOPSIS
#include <dlfcn.h>
void *dlsym(void *handle, const char *symbol);
#define _GNU_SOURCE
#include <dlfcn.h>
void *dlvsym(void *handle, char *symbol, char *version);
DESCRIPTION
dlsym()
接受由dlopen()
返回的動態加載的共享對象的“句柄”,並返回該符號加載到內存中的地址。如果未找到符號,則在加載該對象時,在指定對象或dlopen()
自動加載的任何共享對象中,dlsym()
將返回NULL。(dlsym()
通過這些共享對象的依賴關系樹進行寬度優先搜索。)
因為符號本身可能是 NULL(所以dlsym()
返回 NULL 並不意味着錯誤),因此判斷是否錯誤的正確做法是調用dlerror()
清除任何舊的錯誤條件,然后調用dlsym()
,並且再次調用dlerror()
,保存其返回值,判斷這個保存的值是否是 NULL。
可以在句柄中指定兩個特殊的偽句柄:
- RTLD_DEFAULT
使用默認共享對象搜索順序查找所需符號的第一個匹配項。搜索將包括可執行文件中的全局符號及其依賴項,以及使用RTLD_GLOBAL 標志動態加載的共享對象中的符號。 - RTLD_NEXT
在當前對象之后的搜索順序中查找下一個所需符號。這允許人們在另一個共享對象中提供一個函數的包裝器,因此,例如,預加載的共享對象中的函數定義(參見ld.so(8)中的LD_PRELOAD)可以找到並調用在另一個共享對象中提供的“真實”函數(或者就此而言,在存在多個預加載層的情況下,函數的“下一個”定義)。
dlvsym()
除了比dlsym()
多提供了一個額外的參數外,其余與dlsym()
相同。
RETURN VALUE
執行成功,這些函數將會返回 symbol 關聯的地址。執行失敗,它們將返回 NULL。錯誤的原因可以通過dlerror()
進行診斷。
EXAMPLE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <gnu/lib-names.h> /* Defines LIBM_SO (which will be a string such as "libm.so.6") */
int
main(void)
{
void *handle;
double (*cosine)(double);
char *error;
handle = dlopen(LIBM_SO, RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
exit(EXIT_FAILURE);
}
dlerror(); /* Clear any existing error */
cosine = (double (*)(double)) dlsym(handle, "cos");
/* According to the ISO C standard, casting between function
pointers and 'void *', as done above, produces undefined results.
POSIX.1-2003 and POSIX.1-2008 accepted this state of affairs and
proposed the following workaround:
*(void **) (&cosine) = dlsym(handle, "cos");
This (clumsy) cast conforms with the ISO C standard and will
avoid any compiler warnings.
The 2013 Technical Corrigendum to POSIX.1-2008 (a.k.a.
POSIX.1-2013) improved matters by requiring that conforming
implementations support casting 'void *' to a function pointer.
Nevertheless, some compilers (e.g., gcc with the '-pedantic'
option) may complain about the cast used in this program. */
error = dlerror();
if (error != NULL) {
fprintf(stderr, "%s\n", error);
exit(EXIT_FAILURE);
}
printf("%f\n", (*cosine)(2.0));
dlclose(handle);
exit(EXIT_SUCCESS);
}