插件和 DLL 通常是用來無須編寫整個新應用程序而添加功能的極好方法。
在 Linux 中,插件和 DLL 是以動態庫形式實現的。
電子商務顧問兼設計師 Allen Wilson 介紹了動態庫,並且向您演示了如何在某一個應用程序正在運行之后使用動態庫來更改該應用程序。
Internet 瀏覽器用戶非常熟悉插件的概念。從 Web 上下載插件,通常這些插件為瀏覽器的音頻、視頻以及特殊效果提供增強支持。一般來講,在不更改原有應用程序的情況下,插件為現有應用程序提供新功能。
DLL 是程序函數,它們在設計和構建應用程序時為該程序所知。設計應用程序的主程序時使用程序框架或底板,這些程序框架或底板在運行時選擇性地裝入所需的 dll,這些 dll 位於磁盤上同主程序分離的一些文件中。這一打包和動態裝入提供了靈活的升級、維護、以及許可策略。
隨 Linux 一起交付的還有幾千條命令和應用程序,它們至少都需要 libc 庫函數。如果 libc 函數與每一個應用程序都打包在一起,那么磁盤上將會出現幾千個相同函數的副本。Linux 構建這些應用程序,以使用通常所需的系統庫的單個系統級副本,而不浪費磁盤空間。Linux 甚至做得更好,每個需要公共系統庫函數的進程使用單個的系統級內的副本,一次性將該副本裝入到內存並為各進程所共享。
在 Linux 中,插件和 dll 以動態庫形式實現。本文的余下部分是在應用程序運行之后使用動態庫更改該應用程序的示例。
Linux 動態鏈接
Linux 中的應用程序以以下兩種方式之一鏈接到外部函數:要么在構建時與靜態庫( lib*.a
) 靜態地鏈接,並且將庫代碼包含在該應用程序的可執行文件里;要么在運行時與共享庫( lib*.so
) 動態地鏈接。通過動態鏈接裝入器,將動態庫映射進應用程序的可執行內存中。在啟動應用程序之前,動態鏈接裝入器將所需的共享目標庫映射到應用程序的內存,或者使用系統共享的目標並為應用程序解析所需的外部引用。現在應用程序就可以運行了。
作為示例,下面有一個演示 Linux 中對動態鏈接庫的缺省使用的小程序:
main() { printf("Hello world "); } |
當使用 gcc 編譯 hello.c 時,就創建了一個名為 a.out
的可執行文件。通過使用 Linux 命令 ldd a.out
(該命令打印出共享庫的相互依賴性),可以看出所需的共享庫是:
libc.so.6 => /lib/libc.so.6 (0x4001d000) /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000) |
使用相同的動態鏈接裝入器在應用程序運行之后將 dll 映射進應用程序的內存。通過使用 Linux 動態裝入器例程,應用程序控制裝入哪一個動態庫以及調用庫中的哪一個函數,以執行裝入和鏈接以及返回所需入口點的地址。
Linux dll 函數
Linux 提供 4 個庫函數( dlopen
, dlerror
, dlsym
和 dlclose
),一個 include 文件( dlfcn.h
)以及兩個共享庫(靜態庫 libdl.a
和動態庫 libdl.so
),以支持動態鏈接裝入器。這些庫函數是:
- dlopen 將共享目標文件打開並且映射到內存中,並且返回句柄
- dlsym返回一個指向被請求入口點的指針
- dlerror 返回 NULL 或者一個指向描述最近錯誤的 ASCII 字符串的指針
- dlclose關閉句柄並且取消共享目標文件的映射
動態鏈接裝入器例程 dlopen 需要在文件系統中查找共享目標文件以打開文件並創建句柄。有 4 種方式用以指定文件的位置:
dlopen call
中的絕對文件路徑
- 在 LD_LIBRARY_PATH 環境變量中指定的目錄中
- 在 /etc/ld.so.cache 中指定的庫列表之中
- 先在 /usr/lib 之中,然后在 /lib 之中
dll 示例:小的 C 程序和 dlTest
動態鏈接裝入器示例程序是一個小的 C 程序,該程序被設計用來練習 dl 例程。該程序基於每個人都編寫過的一個 C 程序,它將“Hello World”打印到控制台上。最初打印的消息是“HeLlO WoRlD”。該測試程序鏈接到再次打印該消息的兩個函數上:第一次都用大寫字符,第二次都用小寫字符。
以下是該程序的概要:
- 定義 dll include 文件
dlfcn.h
和所需的變量。至少需要這些變量:
- 到共享庫文件的句柄
- 指向被映射函數入口點的指針
- 指向錯誤字符串的指針
- 打印初始消息,“HeLlO WoRlD”。
- 使用絕對路徑“/home/dlTest/UPPERCASE.so”和選項 RTLD_LAZY,
dlopen
打開 UPPERCASE dll 的共享目標文件並返回句柄。
- 選項 RTLD_LAZY 推遲解析 dll 的外部引用,直到 dll 被執行。
- 選項 RTLD_NOW 在
dlopen
返回之前解析所有的外部引用。
dlsym
返回入口點 printUPPERCASE 的地址。
- 調用 printUPPERCASE 並且打印修改過的消息“HELLO WORLD”。
dlclose
關閉到 UPPERCASE.so 的句柄,並且從內存中取消 dll 映射。
dlopen
使用基於環境變量 LD_LIBRARY_PATH 的相對路徑查找共享目標路徑,來打開 lowercase dll 的共享目標文件 lowercase.so,並且返回句柄。
dlsym
返回入口點 printLowercase 的地址。
- 調用 printLowercase 並且打印修改過的信息“hello world”。
dlclose
關閉到 lowercase.so 的句柄,並且從內存中取消 dll 映射。
注意,每次調用 dlopen
、 dlsym
或 dlclose
之后,調用 dlerror
以獲取最后的錯誤信息,並且打印該錯誤信息字符串。以下是 dlTest 的測試運行:
dlTest 2-Original message HeLlO WoRlD dlTest 3-Open Library with absolute path return-(null)- dlTest 4-Find symbol printUPPERCASE return-(null)- HELLO WORLD dlTest 5-printUPPERCASE return-(null)- dlTest 6-Close handle return-(null)- dlTest 7-Open Library with relative path return-(null)- dlTest 8-Find symbol printLowercase return-(null)- hello world dlTest 9-printLowercase return-(null)- dlTest 10-Close handle return-(null)- |
完整的 dlTest.c、UPPERCASE.c 和 lowercase.c 源代碼清單在本文后面的 清單里。
構建 dlTest
啟用運行時動態鏈接需要三步:
- 將 dll 編譯為位置無關代碼
- 創建 dll 共享目標文件
- 編譯主程序並同 dl 庫相鏈接
編譯 UPPERCASE.c 和 lowercase.c 的 gcc 命令包含 -fpic 選項。選項 -fpic 和 -fPIC 導致生成的代碼是位置無關的,重建共享目標庫需要位置無關。-fPIC 選項產生位置無關的代碼,這類代碼支持大偏移。用於 UPPERCASE.o 和 lowercase.o 的第二個 gcc 命令,帶有 -shared 選項,該選項產生適合於動態鏈接的共享目標文件 a*.so。
用於編譯和執行 dltest 的 ksh 腳本如下:
#!/bin/ksh # Build shared library # #set -x clear # # Shared library for dlopen absolute path test # if [ -f UPPERCASE.o ]; then rm UPPERCASE.o fi gcc -c -fpic UPPERCASE.c if [ -f UPPERCASE.so ]; then rm UPPERCASE.so fi gcc -shared -lc -o UPPERCASE.so UPPERCASE.o # # Shared library for dlopen relative path test # export LD_LIBRARY_PATH=`pwd` if [ -f lowercase.o ]; then rm lowercase.o fi gcc -c -fpic lowercase.c if [ -f lowercase.so ]; then rm lowercase.so fi gcc -shared -lc -o lowercase.so lowercase.o # # Rebuild test program # if [ -f dlTest ]; then rm dlTest fi gcc -o dlTest dlTest.c -ldl echo Current LD_LIBRARY_PATH=$LD_LIBRARY_PATH dlTest |
結束語
創建能在運行時被動態鏈接到 Linux 系統上的應用程序的共享目標代碼是一項非常簡單的練習。應用程序通過使用對動態鏈接裝入器的 dlopen、dlsym 和 dlclose 函數調用來獲取對共享目標文件的訪問。dlerror 以字符串的形式返回任何錯誤,這些錯誤信息字符串描述 dl 函數碰到的最后一個錯誤。在運行時,主應用程序使用絕對路徑或相對於 LD_LIBRARY_PATH 的相對路徑找到共享目標庫,並且請求所需的 dll 入口點的地址。當需要時,也可對 dll 進行間接函數調用,最后,關閉到共享目標文件的句柄,並且從內存中取消該目標文件映射,使之不可用。
使用附加選項 -fpic 或 -fPIC 編譯共享目標代碼,以產生位置無關的代碼,使用 -shared 選項將目標代碼放進共享目標庫中。
Linux 中的共享目標代碼庫和動態鏈接裝入器向應用程序提供了額外的功能。減少了磁盤上和內存里的可執行文件的大小。可以在需要時,裝入可選的應用程序功能,可以在無須重新構建整個應用程序的情況下修正缺陷,並且應用程序可以包含第三方的插件。
清單(應用程序和 dll)
dlTest.c:
/*************************************************************/ /* Test Linux Dynamic Function Loading */ /* */ /* void *dlopen(const char *filename, int flag) */ /* Opens dynamic library and return handle */ /* */ /* const char *dlerror(void) */ /* Returns string describing the last error. */ /* */ /* void *dlsym(void *handle, char *symbol) */ /* Return pointer to symbol's load point. */ /* If symbol is undefined, NULL is returned. */ /* */ /* int dlclose (void *handle) */ /* Close the dynamic library handle. */ /* */ /* */ /* */ /*************************************************************/ #include #include /* */ /* 1-dll include file and variables */ /* */ #include void *FunctionLib; /* Handle to shared lib file */ int (*Function)(); /* Pointer to loaded routine */ const char *dlError; /* Pointer to error string */ main( argc, argv ) { int rc; /* return codes */ char HelloMessage[] = "HeLlO WoRlD\n"; /* */ /* 2-print the original message */ /* */ printf(" dlTest 2-Original message \n"); printf("%s", HelloMessage); /* */ /* 3-Open Dynamic Loadable Libary with absolute path */ /* */ FunctionLib = dlopen("/home/dlTest/UPPERCASE.so",RTLD_LAZY); dlError = dlerror(); printf(" dlTest 3-Open Library with absolute path return-%s- \n", dlError); if( dlError ) exit(1); /* */ /* 4-Find the first loaded function */ /* */ Function = dlsym( FunctionLib, "printUPPERCASE"); dlError = dlerror(); printf(" dlTest 4-Find symbol printUPPERCASE return-%s- \n", dlError); if( dlError ) exit(1); /* */ /* 5-Execute the first loaded function */ /* */ rc = (*Function)( HelloMessage ); printf(" dlTest 5-printUPPERCASE return-%s- \n", dlError); /* */ /* 6-Close the shared library handle */ /* Note: after the dlclose, "printUPPERCASE" is not loaded */ /* */ rc = dlclose(FunctionLib); dlError = dlerror(); printf(" dlTest 6-Close handle return-%s-\n",dlError); if( rc ) exit(1); /* */ /* 7-Open Dynamic Loadable Libary using LD_LIBRARY path */ /* */ FunctionLib = dlopen("lowercase.so",RTLD_LAZY); dlError = dlerror(); printf(" dlTest 7-Open Library with relative path return-%s- \n", dlError); if( dlError ) exit(1); /* */ /* 8-Find the second loaded function */ /* */ Function = dlsym( FunctionLib, "printLowercase"); dlError = dlerror(); printf(" dlTest 8-Find symbol printLowercase return-%s- \n", dlError); if( dlError ) exit(1); /* */ /* 8-execute the second loaded function */ /* */ rc = (*Function)( HelloMessage ); printf(" dlTest 9-printLowercase return-%s- \n", dlError); /* */ /* 10-Close the shared library handle */ /* */ rc = dlclose(FunctionLib); dlError = dlerror(); printf(" dlTest 10-Close handle return-%s-\n",dlError); if( rc ) exit(1); return(0); } |
UPPERCASE.c:
/************************************************/ /* Function to print input string as UPPER case. */ /* Returns 1. */ /*********************************************** */ int printUPPERCASE ( inLine ) char inLine[]; { char UPstring[256]; char *inptr, *outptr; inptr = inLine; outptr = UPstring; while ( *inptr != '\0' ) *outptr++ = toupper(*inptr++); *outptr++ = '\0'; printf(UPstring); return(1); } |
lowercase.c
/********************************************/ /* Function to print input string as lower case. */ /* Returns 2. */ /******************************************* */ int printLowercase( inLine ) char inLine[]; { char lowstring[256]; char *inptr, *outptr; inptr = inLine; outptr = lowstring; while ( *inptr != '' ) *outptr++ = tolower(*inptr++); *outptr++ = ''; printf(lowstring); return(2); } |