動態(tài)鏈接的共享庫是 GNU/Linux? 的一個重要方面。該種庫允許可執(zhí)行文件在運行時動態(tài)訪問外部函數(shù),從而(通過在需要時才會引入函數(shù)的方式)減少它們對內(nèi)存的總體占用。本文研究了創(chuàng)建和使用靜態(tài)庫的過程,詳細描述了開發(fā)它們的各種工具,并揭秘了這些庫的工作方式。 庫用于將相似函數(shù)打包在一個單元中。然后這些單元就可為其他開發(fā)人員所共享,并因此有了模塊化編程這種說法 — 即,從模塊中構(gòu)建程序。Linux 支持兩種類型的庫,每一種庫都有各自的優(yōu)缺點。靜態(tài)庫包含在編譯時靜態(tài)綁定到一個程序的函數(shù)。動態(tài)庫則不同,它是在加載應(yīng)用程序時被加載的,而且它與應(yīng)用程序是在運行時綁定的。圖 1 展示了 Linux 中的庫的層次結(jié)構(gòu)。 圖 1. Linux 中的庫層次結(jié)構(gòu) 使用共享庫的方法有兩種:您既可以在運行時動態(tài)鏈接庫,也可以動態(tài)加載庫并在程序控制之下使用它們。本文對這兩種方法都做了探討。 靜態(tài)庫較適宜于較小的應(yīng)用程序,因為它們只需要最小限度的函數(shù)。而對于需要多個庫的應(yīng)用程序來說,則適合使用共享庫,因為它們可以減少應(yīng)用程序?qū)?nèi)存(包括運行時中的磁盤占用和內(nèi)存占用)的占用。這是因為多個應(yīng)用程序可以同時使用一個共享庫;因此,每次只需要在內(nèi)存上復制一個庫。要是靜態(tài)庫的話,每一個運行的程序都要有一份庫的副本。 GNU/Linux 提供兩種處理共享庫的方法(每種方法都源于 Sun Solaris)。您可以動態(tài)地將程序和共享庫鏈接并讓 Linux 在執(zhí)行時加載庫(如果它已經(jīng)在內(nèi)存中了,則無需再加載)。另外一種方法是使用一個稱為動態(tài)加載的過程,這樣程序可以有選擇地調(diào)用庫中的函數(shù)。使用動態(tài)加載過程,程序可以先加載一個特定的庫(已加載則不必),然后調(diào)用該庫中的某一特定函數(shù)(圖 2 展示了這兩種方法)。這是構(gòu)建支持插件的應(yīng)用程序的一個普遍的方法。我稍候?qū)⒃诒疚奶接懖⑹痉对搼?yīng)用程序編程接口(API)。 圖 2. 靜態(tài)鏈接與動態(tài)鏈接 用 Linux 進行動態(tài)鏈接 現(xiàn)在,讓我們深入探討一下使用 Linux 中的動態(tài)鏈接的共享庫的過程。當用戶啟動一個應(yīng)用程序時,它們正在調(diào)用一個可執(zhí)行和鏈接格式(Executable and Linking Format,ELF)映像。內(nèi)核首先將 ELF 映像加載到用戶空間虛擬內(nèi)存中。然后內(nèi)核會注意到一個稱為 .interp 的 ELF 部分,它指明了將要被使用的動態(tài)鏈接器(/lib/ld-linux.so),如清單 1 所示。這與 UNIX? 中的腳本文件的解釋器定義(#!/bin/sh)很相似:只是用在了不同的上下文中。 清單 1. 使用 readelf 來顯示程序標題 mtj@camus:~/dl$ readelf -l dl Elf file type is EXEC (Executable file) Entry point 0x8048618 There are 7 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4 INTERP 0x000114 0x08048114 0x08048114 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x00958 0x00958 R E 0x1000 LOAD 0x000958 0x08049958 0x08049958 0x00120 0x00128 RW 0x1000 DYNAMIC 0x00096c 0x0804996c 0x0804996c 0x000d0 0x000d0 RW 0x4 NOTE 0x000128 0x08048128 0x08048128 0x00020 0x00020 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4 ... mtj@camus:~dl$ | 注意,ld-linux.so 本身就是一個 ELF 共享庫,但它是靜態(tài)編譯的并且不具備共享庫依賴項。當需要動態(tài)鏈接時,內(nèi)核會引導動態(tài)鏈接(ELF 解釋器),該鏈接首先會初始化自身,然后加載指定的共享對象(已加載則不必)。接著它會執(zhí)行必要的再定位,包括目標共享對象所使用的共享對象。LD_LIBRARY_PATH 環(huán)境變量定義查找可用共享對象的位置。定義完成后,控制權(quán)會被傳回到初始程序以開始執(zhí)行。 再定位是通過一個稱為 Global Offset Table(GOT)和 Procedure Linkage Table(PLT)的間接機制來處理的。這些表格提供了 ld-linux.so 在再定位過程中加載的外部函數(shù)和數(shù)據(jù)的地址。這意味著無需改動需要間接機制(即,使用這些表格)的代碼:只需要調(diào)整這些表格。一旦進行加載,或者只要需要給定的函數(shù),就可以發(fā)生再定位(稍候在 用 Linux 進行動態(tài)加載 小節(jié)中會看到更多的差別)。 再定位完成后,動態(tài)鏈接器就會允許任何加載的共享程序來執(zhí)行可選的初始化代碼。該函數(shù)允許庫來初始化內(nèi)部數(shù)據(jù)并備之待用。這個代碼是在上述 ELF 映像的 .init 部分中定義的。在卸載庫時,它還可以調(diào)用一個終止函數(shù)(定義為映像的 .fini 部分)。當初始化函數(shù)被調(diào)用時,動態(tài)鏈接器會把控制權(quán)轉(zhuǎn)讓給加載的原始映像。
用 Linux 進行動態(tài)加載 Linux 并不會自動為給定程序加載和鏈接庫,而是與應(yīng)用程序本身共享該控制權(quán)。這個過程就稱為動態(tài)加載。使用動態(tài)加載,應(yīng)用程序能夠先指定要加載的庫,然后將該庫作為一個可執(zhí)行文件來使用(即調(diào)用其中的函數(shù))。但是正如您在前面所了解到的,用于動態(tài)加載的共享庫與標準共享庫(ELF 共享對象)無異。事實上,ld-linux 動態(tài)鏈接器作為 ELF 加載器和解釋器,仍然會參與到這個過程中。 動態(tài)加載(Dynamic Loading,DL)API 就是為了動態(tài)加載而存在的,它允許共享庫對用戶空間程序可用。盡管非常小,但是這個 API 提供了所有需要的東西,而且很多困難的工作是在后臺完成的。表 1 展示了這個完整的 API。 表 1. Dl API 函數(shù) | 描述 | dlopen | 使對象文件可被程序訪問 | dlsym | 獲取執(zhí)行了 dlopen 函數(shù)的對象文件中的符號的地址 | dlerror | 返回上一次出現(xiàn)錯誤的字符串錯誤 | dlclose | 關(guān)閉目標文件 | 該過程首先是調(diào)用 dlopen ,提供要訪問的文件對象和模式。調(diào)用 dlopen 的結(jié)果是稍候要使用的對象的句柄。mode 參數(shù)通知動態(tài)鏈接器何時執(zhí)行再定位。有兩個可能的值。第一個是 RTLD_NOW ,它表明動態(tài)鏈接器將會在調(diào)用 dlopen 時完成所有必要的再定位。第二個可選的模式是 RTLD_LAZY ,它只在需要時執(zhí)行再定位。這是通過在內(nèi)部使用動態(tài)鏈接器重定向所有尚未再定位的請求來完成的。這樣,動態(tài)鏈接器就能夠在請求時知曉何時發(fā)生了新的引用,而且再定位可以正常進行。后面的調(diào)用無需重復再定位過程。 還可以選擇另外兩種模式,它們可以按位 OR 到 mode 參數(shù)中。RTLD_LOCAL 表明其他任何對象都無法使加載的共享對象的符號用于再定位過程。如果這正是您想要的的話(例如,為了讓共享的對象能夠調(diào)用原始進程映像中的符號),那就使用 RTLD_GLOBAL 吧。 dlopen 函數(shù)還會自動解析共享庫中的依賴項。這樣,如果您打開了一個依賴于其他共享庫的對象,它就會自動加載它們。函數(shù)返回一個句柄,該句柄用于后續(xù)的 API 調(diào)用。dlopen 的原型為: #include <dlfcn.h> void *dlopen( const char *file, int mode ); | 有了 ELF 對象的句柄,就可以通過調(diào)用 dlsym 來識別這個對象內(nèi)的符號的地址了。該函數(shù)采用一個符號名稱,如對象內(nèi)的一個函數(shù)的名稱。返回值為對象符號的解析地址: void *dlsym( void *restrict handle, const char *restrict name ); | 如果調(diào)用該 API 時發(fā)生了錯誤,可以使用 dlerror 函數(shù)返回一個表示此錯誤的人類可讀的字符串。該函數(shù)沒有參數(shù),它會在發(fā)生前面的錯誤時返回一個字符串,在沒有錯誤發(fā)生時返回 NULL: 最后,如果無需再調(diào)用共享對象的話,應(yīng)用程序可以調(diào)用 dlclose 來通知操作系統(tǒng)不再需要句柄和對象引用了。它完全是按引用來計數(shù)的,所以同一個共享對象的多個用戶相互間不會發(fā)生沖突(只要還有一個用戶在使用它,它就會待在內(nèi)存中)。任何通過已關(guān)閉的對象的 dlsym 解析的符號都將不再可用。 char *dlclose( void *handle ); |
動態(tài)加載示例 了解了 API 之后,下面讓我們來看一看 DL API 的例子。在這個應(yīng)用程序中,您主要實現(xiàn)了一個 shell,它允許操作員來指定庫、函數(shù)和參數(shù)。換句話說,也就是用戶能夠指定一個庫并調(diào)用該庫(先前未鏈接于該應(yīng)用程序的)內(nèi)的任意一個函數(shù)。首先使用 DL API 來解析該庫中的函數(shù),然后使用用戶定義的參數(shù)(用來發(fā)送結(jié)果)來調(diào)用它。清單 2 展示了完整的應(yīng)用程序。 清單 2. 使用 DL API 的 Shell #include <stdio.h> #include <dlfcn.h> #include <string.h> #define MAX_STRING 80 void invoke_method( char *lib, char *method, float argument ) { void *dl_handle; float (*func)(float); char *error; /* Open the shared object */ dl_handle = dlopen( lib, RTLD_LAZY ); if (!dl_handle) { printf( "!!! %s\n", dlerror() ); return; } /* Resolve the symbol (method) from the object */ func = dlsym( dl_handle, method ); error = dlerror(); if (error != NULL) { printf( "!!! %s\n", error ); return; } /* Call the resolved method and print the result */ printf(" %f\n", (*func)(argument) ); /* Close the object */ dlclose( dl_handle ); return; } int main( int argc, char *argv[] ) { char line[MAX_STRING+1]; char lib[MAX_STRING+1]; char method[MAX_STRING+1]; float argument; while (1) { printf("> "); line[0]=0; fgets( line, MAX_STRING, stdin); if (!strncmp(line, "bye", 3)) break; sscanf( line, "%s %s %f", lib, method, &argument); invoke_method( lib, method, argument ); } } | 要構(gòu)建這個應(yīng)用程序,需要通過 GNU Compiler Collection(GCC)使用如下的編譯行。選項 -rdynamic 用來通知鏈接器將所有符號添加到動態(tài)符號表中(目的是能夠通過使用 dlopen 來實現(xiàn)向后跟蹤)。-ldl 表明一定要將 dllib 鏈接于該程序。 gcc -rdynamic -o dl dl.c -ldl | 再回到 清單 2,main 函數(shù)僅充當解釋器,解析來自輸入行的三個參數(shù)(庫名、函數(shù)名和浮點參數(shù))。如果出現(xiàn) bye 的話,應(yīng)用程序就會退出。否則的話,這三個參數(shù)就會傳遞給使用 DL API 的 invoke_method 函數(shù)。 首先調(diào)用 dlopen 來訪問目標文件。如果返回 NULL 句柄,表示無法找到對象,過程結(jié)束。否則的話,將會得到對象的一個句柄,可以進一步詢問對象。然后使用 dlsym API 函數(shù),嘗試解析新打開的對象文件中的符號。您將會得到一個有效的指向該符號的指針,或者是得到一個 NULL 并返回一個錯誤。 在 ELF 對象中解析了符號后,下一步就只需要調(diào)用函數(shù)。要注意一下這個代碼和前面討論的動態(tài)鏈接的差別。在這個例子中,您強行將目標文件中的符號地址用作函數(shù)指針,然后調(diào)用它。而在前面的例子是將對象名作為函數(shù),由動態(tài)鏈接器來確保符號指向正確的位置。雖然動態(tài)鏈接器能夠為您做所有麻煩的工作,但這個方法會讓您構(gòu)建出極其動態(tài)的應(yīng)用程序,它們可以再運行時被擴展。 調(diào)用 ELF 對象中的目標函數(shù)后,通過調(diào)用 dlclose 來關(guān)閉對它的訪問。 清單 3 展示了一個如何使用這個測試程序的例子。在這個例子中,首先編譯程序而后執(zhí)行它。接著調(diào)用了 math 庫(libm.so)中的幾個函數(shù)。完成演示后,程序現(xiàn)在能夠用動態(tài)加載來調(diào)用共享對象(庫)中的任意函數(shù)了。這是一個很強大的功能,通過它還能夠給程序擴充新的功能。 清單 3. 使用簡單的程序來調(diào)用庫函數(shù) mtj@camus:~/dl$ gcc -rdynamic -o dl dl.c -ldl mtj@camus:~/dl$ ./dl > libm.so cosf 0.0 1.000000 > libm.so sinf 0.0 0.000000 > libm.so tanf 1.0 1.557408 > bye mtj@camus:~/dl$ |
工具 Linux 提供了很多種查看和解析 ELF 對象(包括共享庫)的工具。其中最有用的一個當屬 ldd 命令,您可以使用它來發(fā)送共享庫依賴項。例如,在 dl 應(yīng)用程序上使用 ldd 命令會顯示如下內(nèi)容: mtj@camus:~/dl$ ldd dl linux-gate.so.1 => (0xffffe000) libdl.so.2 => /lib/tls/i686/cmov/libdl.so.2 (0xb7fdb000) libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7eac000) /lib/ld-linux.so.2 (0xb7fe7000) mtj@camus:~/dl$ | ldd 所告訴您的是:該 ELF 映像依賴于 linux-gate.so(一個特殊的共享對象,它處理系統(tǒng)調(diào)用,它在文件系統(tǒng)中無關(guān)聯(lián)文件)、libdl.so(DL API)、GNU C 庫(libc.so)以及 Linux 動態(tài)加載器(因為它里面有共享庫依賴項)。 readelf 命令是一個有很多特性的實用程序,它讓您能夠解析和讀取 ELF 對象。readelf 有一個有趣的用途,就是用來識別對象內(nèi)可再定位的項。對于我們這個簡單的程序來說(清單 2 展示的程序),您可以看到需要再定位的符號為: mtj@camus:~/dl$ readelf -r dl Relocation section '.rel.dyn' at offset 0x520 contains 2 entries: Offset Info Type Sym.Value Sym. Name 08049a3c 00001806 R_386_GLOB_DAT 00000000 __gmon_start__ 08049a78 00001405 R_386_COPY 08049a78 stdin Relocation section '.rel.plt' at offset 0x530 contains 8 entries: Offset Info Type Sym.Value Sym. Name 08049a4c 00000207 R_386_JUMP_SLOT 00000000 dlsym 08049a50 00000607 R_386_JUMP_SLOT 00000000 fgets 08049a54 00000b07 R_386_JUMP_SLOT 00000000 dlerror 08049a58 00000c07 R_386_JUMP_SLOT 00000000 __libc_start_main 08049a5c 00000e07 R_386_JUMP_SLOT 00000000 printf 08049a60 00001007 R_386_JUMP_SLOT 00000000 dlclose 08049a64 00001107 R_386_JUMP_SLOT 00000000 sscanf 08049a68 00001907 R_386_JUMP_SLOT 00000000 dlopen mtj@camus:~/dl$ | 從這個列表中,您可以看到各種各樣的需要再定位(到 libc.so)的 C 庫調(diào)用,包括對 DL API(libdl.so)的調(diào)用。函數(shù) __libc_start_main 是一個 C 庫函數(shù),它優(yōu)先于程序的 main 函數(shù)(一個提供必要初始化的 shell)而被調(diào)用。 其他操作對象文件的實用程序包括:objdump ,它展示了關(guān)于對象文件的信息;nm ,它列出來自對象文件(包括調(diào)試信息)的符號。還可以將 EFL 程序作為參數(shù),直接調(diào)用 Linux 動態(tài)鏈接器,從而手動啟動映像: mtj@camus:~/dl$ /lib/ld-linux.so.2 ./dl > libm.so expf 0.0 1.000000 > | 另外,可以使用 ld-linux.so 的 --list 選項來羅列 ELF 映像的依賴項(ldd 命令也如此)。切記,它僅僅是一個用戶空間程序,是由內(nèi)核在需要時引導的。
結(jié)束語 本文只涉及到了動態(tài)鏈接器功能的皮毛而已。在下面的 參考資料 中,您可以找到對 ELF 映像格式和過程或符號再定位的更詳細的介紹。而且和 Linux 其他所有工具一樣,你也可以下載動態(tài)鏈接器的源代碼(參見 參考資料)來深入研究它的內(nèi)部。
參考資料 學習 獲得產(chǎn)品和技術(shù) - 從 Debian 下載 Linux 動態(tài)鏈接器的源代碼。這是關(guān)于動態(tài)鏈接和動態(tài)加載信息的最基本的信息來源。
- 定購 SEK for Linux,共包含兩張 DVD,其中有用于 Linux 的最新 IBM 試用軟件,包括 DB2?、Lotus?、Rational?、Tivoli? 和 WebSphere?。
- 使用可直接從 developerWorks 下載的 IBM 試用軟件 構(gòu)建您的下一個 Linux 項目。
|