How does linux lib work?
用 Linux 進行動態加載
Linux 并不會自動為給定程序加載和鏈接庫,而是與應用程序本身共享該控制權。這個過程就稱為動態加載。使用動態加載,應用程序能夠先指定要加載的庫,然后將該庫作為一個可執行文件來使用(即調用其中的函數)。但是正如您在前面所了解到的,用于動態加載的共享庫與標準共享庫(ELF 共享對象)無異。事實上,ld-linux
動態鏈接器作為 ELF 加載器和解釋器,仍然會參與到這個過程中。
動態加載(Dynamic Loading,DL)API 就是為了動態加載而存在的,它允許共享庫對用戶空間程序可用。盡管非常小,但是這個 API 提供了所有需要的東西,而且很多困難的工作是在后臺完成的。表 1 展示了這個完整的 API。
表 1. Dl API
函數 |
描述 |
dlopen |
使對象文件可被程序訪問 |
dlsym |
獲取執行了 dlopen 函數的對象文件中的符號的地址 |
dlerror |
返回上一次出現錯誤的字符串錯誤 |
dlclose |
關閉目標文件 |
該過程首先是調用 dlopen
,提供要訪問的文件對象和模式。調用 dlopen
的結果是稍候要使用的對象的句柄。mode
參數通知動態鏈接器何時執行再定位。有兩個可能的值。第一個是 RTLD_NOW
,它表明動態鏈接器將會在調用 dlopen
時完成所有必要的再定位。第二個可選的模式是 RTLD_LAZY
,它只在需要時執行再定位。這是通過在內部使用動態鏈接器重定向所有尚未再定位的請求來完成的。這樣,動態鏈接器就能夠在請求時知曉何時發生了新的引用,而且再定位可以正常進行。后面的調用無需重復再定位過程。
還可以選擇另外兩種模式,它們可以按位 OR
到 mode
參數中。RTLD_LOCAL
表明其他任何對象都無法使加載的共享對象的符號用于再定位過程。如果這正是您想要的的話(例如,為了讓共享的對象能夠調用原始進程映像中的符號),那就使用 RTLD_GLOBAL
吧。
dlopen
函數還會自動解析共享庫中的依賴項。這樣,如果您打開了一個依賴于其他共享庫的對象,它就會自動加載它們。函數返回一個句柄,該句柄用于后續的 API 調用。dlopen
的原型為:
#include <dlfcn.h>
void *dlopen( const char *file, int mode );
|
有了 ELF 對象的句柄,就可以通過調用 dlsym
來識別這個對象內的符號的地址了。該函數采用一個符號名稱,如對象內的一個函數的名稱。返回值為對象符號的解析地址:
void *dlsym( void *restrict handle, const char *restrict name );
|
如果調用該 API 時發生了錯誤,可以使用 dlerror
函數返回一個表示此錯誤的人類可讀的字符串。該函數沒有參數,它會在發生前面的錯誤時返回一個字符串,在沒有錯誤發生時返回 NULL:
最后,如果無需再調用共享對象的話,應用程序可以調用 dlclose
來通知操作系統不再需要句柄和對象引用了。它完全是按引用來計數的,所以同一個共享對象的多個用戶相互間不會發生沖突(只要還有一個用戶在使用它,它就會待在內存中)。任何通過已關閉的對象的 dlsym
解析的符號都將不再可用。
char *dlclose( void *handle );
|
動態加載示例
了
解了 API 之后,下面讓我們來看一看 DL API 的例子。在這個應用程序中,您主要實現了一個
shell,它允許操作員來指定庫、函數和參數。換句話說,也就是用戶能夠指定一個庫并調用該庫(先前未鏈接于該應用程序的)內的任意一個函數。首先使用
DL API 來解析該庫中的函數,然后使用用戶定義的參數(用來發送結果)來調用它。清單 2 展示了完整的應用程序。
清單 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 );
}
}
|
要構建這個應用程序,需要通過 GNU Compiler Collection(GCC)使用如下的編譯行。選項 -rdynamic
用來通知鏈接器將所有符號添加到動態符號表中(目的是能夠通過使用 dlopen
來實現向后跟蹤)。-ldl
表明一定要將 dllib
鏈接于該程序。
gcc -rdynamic -o dl dl.c -ldl
|
再回到 清單 2,main
函數僅充當解釋器,解析來自輸入行的三個參數(庫名、函數名和浮點參數)。如果出現 bye
的話,應用程序就會退出。否則的話,這三個參數就會傳遞給使用 DL API 的 invoke_method
函數。
首先調用 dlopen
來訪問目標文件。如果返回 NULL 句柄,表示無法找到對象,過程結束。否則的話,將會得到對象的一個句柄,可以進一步詢問對象。然后使用 dlsym
API 函數,嘗試解析新打開的對象文件中的符號。您將會得到一個有效的指向該符號的指針,或者是得到一個 NULL 并返回一個錯誤。
在
ELF
對象中解析了符號后,下一步就只需要調用函數。要注意一下這個代碼和前面討論的動態鏈接的差別。在這個例子中,您強行將目標文件中的符號地址用作函數指
針,然后調用它。而在前面的例子是將對象名作為函數,由動態鏈接器來確保符號指向正確的位置。雖然動態鏈接器能夠為您做所有麻煩的工作,但這個方法會讓您
構建出極其動態的應用程序,它們可以再運行時被擴展。
調用 ELF 對象中的目標函數后,通過調用 dlclose
來關閉對它的訪問。
清
單 3 展示了一個如何使用這個測試程序的例子。在這個例子中,首先編譯程序而后執行它。接著調用了 math
庫(libm.so)中的幾個函數。完成演示后,程序現在能夠用動態加載來調用共享對象(庫)中的任意函數了。這是一個很強大的功能,通過它還能夠給程序
擴充新的功能。
清單 3. 使用簡單的程序來調用庫函數
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
命令,您可以使用它來發送共享庫依賴項。例如,在 dl
應用程序上使用 ldd
命令會顯示如下內容:
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(一個特殊的共享對象,它處理系統調用,它在文件系統中無關聯文件)、libdl.so(DL API)、GNU C
庫(libc.so)以及 Linux 動態加載器(因為它里面有共享庫依賴項)。
readelf
命令是一個有很多特性的實用程序,它讓您能夠解析和讀取 ELF 對象。readelf
有一個有趣的用途,就是用來識別對象內可再定位的項。對于我們這個簡單的程序來說(清單 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
庫調用,包括對 DL API(libdl.so)的調用。函數
__libc_start_main
是一個
C
庫函數,它優先于程序的
main
函數(一個提供必要初始化的 shell)而被調用。
其他操作對象文件的實用程序包括:objdump
,它展示了關于對象文件的信息;nm
,它列出來自對象文件(包括調試信息)的符號。還可以將 EFL 程序作為參數,直接調用 Linux 動態鏈接器,從而手動啟動映像:
mtj@camus:~/dl$ /lib/ld-linux.so.2 ./dl > libm.so expf 0.0 1.000000 >
|
另外,可以使用 ld-linux.so 的 --list
選項來羅列 ELF 映像的依賴項(ldd
命令也如此)。切記,它僅僅是一個用戶空間程序,是由內核在需要時引導的。