Linux 動態庫剖析

From : http://www.ibm.com/developerworks/cn/linux/l-dynamic-libraries/index.html


Linux 動態庫剖析

進程與 API

M. Tim Jones, 顧問工程師, Emulex Corp.

簡介: 動態鏈接的共享庫是 GNU/Linux® 的一個重要方面。該種庫允許可執行文件在運行時動態訪問外部函數,從而(通過在需要時纔會引入函數的方式)減少它們對內存的總體佔用。本文研究了創建和使用靜態庫的過程,詳細描述了開發它們的各種工具,並揭祕了這些庫的工作方式。

發佈日期: 2008 年 9 月 08 日 
級別: 中級 其他語言版本: 英文 
訪問情況 : 14299 次瀏覽 
評論: 0 (查看 | 添加評論 - 登錄)

平均分 4 星 共 17 個評分 平均分 (17個評分)
爲本文評分

庫用於將相似函數打包在一個單元中。然後這些單元就可爲其他開發人員所共享,並因此有了模塊化編程這種說法 — 即,從模塊中構建程序。Linux 支持兩種類型的庫,每一種庫都有各自的優缺點。靜態庫包含在編譯時靜態綁定到一個程序的函數。動態庫則不同,它是在加載應用程序時被加載的,而且它與應用程序是在運行時綁定的。圖 1 展示了 Linux 中的庫的層次結構。


圖 1. Linux 中的庫層次結構
Linux 中的庫層次結構。 

使用共享庫的方法有兩種:您既可以在運行時動態鏈接庫,也可以動態加載庫並在程序控制之下使用它們。本文對這兩種方法都做了探討。

靜態庫較適宜於較小的應用程序,因爲它們只需要最小限度的函數。而對於需要多個庫的應用程序來說,則適合使用共享庫,因爲它們可以減少應用程序對內存(包括運行時中的磁盤佔用和內存佔用)的佔用。這是因爲多個應用程序可以同時使用一個共享庫;因此,每次只需要在內存上覆制一個庫。要是靜態庫的話,每一個運行的程序都要有一份庫的副本。

GNU/Linux 提供兩種處理共享庫的方法(每種方法都源於 Sun Solaris)。您可以動態地將程序和共享庫鏈接並讓 Linux 在執行時加載庫(如果它已經在內存中了,則無需再加載)。另外一種方法是使用一個稱爲動態加載的過程,這樣程序可以有選擇地調用庫中的函數。使用動態加載過程,程序可以先加載一個特定的庫(已加載則不必),然後調用該庫中的某一特定函數(圖 2 展示了這兩種方法)。這是構建支持插件的應用程序的一個普遍的方法。我稍候將在本文探討並示範該應用程序編程接口(API)。


圖 2. 靜態鏈接與動態鏈接
圖 2. 靜態鏈接與動態鏈接 

用 Linux 進行動態鏈接

現在,讓我們深入探討一下使用 Linux 中的動態鏈接的共享庫的過程。當用戶啓動一個應用程序時,它們正在調用一個可執行和鏈接格式(Executable and Linking Format,ELF)映像。內核首先將 ELF 映像加載到用戶空間虛擬內存中。然後內核會注意到一個稱爲.interp 的 ELF 部分,它指明瞭將要被使用的動態鏈接器(/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 共享庫,但它是靜態編譯的並且不具備共享庫依賴項。當需要動態鏈接時,內核會引導動態鏈接(ELF 解釋器),該鏈接首先會初始化自身,然後加載指定的共享對象(已加載則不必)。接着它會執行必要的再定位,包括目標共享對象所使用的共享對象。LD_LIBRARY_PATH 環境變量定義查找可用共享對象的位置。定義完成後,控制權會被傳回到初始程序以開始執行。

再定位是通過一個稱爲 Global Offset Table(GOT)和 Procedure Linkage Table(PLT)的間接機制來處理的。這些表格提供了 ld-linux.so 在再定位過程中加載的外部函數和數據的地址。這意味着無需改動需要間接機制(即,使用這些表格)的代碼:只需要調整這些表格。一旦進行加載,或者只要需要給定的函數,就可以發生再定位(稍候在 用 Linux 進行動態加載 小節中會看到更多的差別)。

再定位完成後,動態鏈接器就會允許任何加載的共享程序來執行可選的初始化代碼。該函數允許庫來初始化內部數據並備之待用。這個代碼是在上述 ELF 映像的 .init 部分中定義的。在卸載庫時,它還可以調用一個終止函數(定義爲映像的 .fini 部分)。當初始化函數被調用時,動態鏈接器會把控制權轉讓給加載的原始映像。

用 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:

char *dlerror();

最後,如果無需再調用共享對象的話,應用程序可以調用 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

再回到 清單 2main 函數僅充當解釋器,解析來自輸入行的三個參數(庫名、函數名和浮點參數)。如果出現 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 命令也如此)。切記,它僅僅是一個用戶空間程序,是由內核在需要時引導的。

結束語

本文只涉及到了動態鏈接器功能的皮毛而已。在下面的 參考資料 中,您可以找到對 ELF 映像格式和過程或符號再定位的更詳細的介紹。而且和 Linux 其他所有工具一樣,你也可以下載動態鏈接器的源代碼(參見 參考資料)來深入研究它的內部。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章