載入動態連結庫——dlopen dlsym dlclose

Zhaoxi_Zhang發表於2019-03-04

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
", 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
", error);
	   exit(EXIT_FAILURE);
	}

	printf("%f
", (*cosine)(2.0));
	dlclose(handle);
	exit(EXIT_SUCCESS);
}
複製程式碼

相關文章