UNIX進程環境和動態鏈接

 1. 環境表

    每個程序都會從父進程那裏接收一張環境表。和參數包一樣,環境表也是一個字符指針數組,其中每個指針包含你一個null結束的C字符串地址。全局變量environ則包含該指針數組地址,稱爲環境指針。環境指針指向環境表,保存每個環境字符串的地址。每個環境字符串都是name=value的形式。可以用getenv和putenv來訪問特定的環境標量。
extern char **environ;
環境指針 環境表
environ-----> 環境字符串地址-----> HOME=/home/luffy\0
環境字符串地址-----> PATH=:/bin:/usr/bin\0
環境字符串地址-----> SHELL=/bin/bash\0
環境字符串地址-----> USER=luffy\0
環境字符串地址-----> LOGNAME=luffy\0
NULL
 
2. C程序的存儲空間佈局
一般來說,從低地址和到高地址依次是:
(1). 正文段 這是CPU執行的機器指令部分。通常,正文段是可共享的,而且常常是隻讀的,以防止程序由於意外而修改其自身指令。
(2). 初始化數據段 通常稱之爲數據段,它包含了程序中需明確賦初值的變量。例如C程序出現在函數之外的聲明:int maxcount = 99;
使此便利那個帶有其初值存放在初始化數據段中。
(3). 非初始化數據段 通常稱之爲bss(起源與早期的彙編運算符,意思是block started by symbol由符號開始的段)段。在程序開始執行之前,內核將此段中的數據初始化爲0或空指針(通常exec時),這就是爲什麼靜態變量和全局變量會初始化爲0。鎖例如出現在函數之外的聲明:long sum[1000];
(4). 堆 由上面的弟低地址開始向高地址延展。通常在堆中進行動態存儲分配。位於BSS和堆之間。
(5). 棧 自動變量,函數調用時所需要保存的信息(返回地址,調用着寄存器環境信息)都保存在這個段中。然後被調用的函數會在棧上爲自動和臨時變量分配存儲空間。這就實現了遞歸,函數實例中的變量集不會影響該函數另一個實例中的變量。棧由高地址向低地址延展。高地址臨接環境參數和命令行參數的段。
(6). 環境參數和命令行參數 保存進程環境信息。
size命令報告程序的各個段長度。
$ size /bin/bash
   text   data    bss    dec    hex filename
 916355  35848  23304 975507  ee293 /bin/bash
 
3. 共享庫
    Linux 系統上有兩類根本不同的 Linux 可執行程序。第一類是靜態鏈接的可執行程序。靜態可執行程序包含執行所需的所有函數 — 換句話說,它們是“完整的”。因爲這一原因,靜態可執行程序不依賴任何外部庫就可以運行。第二類是動態鏈接的可執行程序。
我們可以用 ldd 命令來確定某一特定可執行程序是否爲靜態鏈接的:
# ldd /sbin/sln
not a dynamic executable
“not a dynamic executable”是 ldd 說明 sln 是靜態鏈接的一種方式。現在,讓我們比較 sln 與其非靜態同類 ln 的大小:
# ls -l /bin/ln /sbin/sln
-rwxr-xr-x    1 root     root        23000 Jan 14 00:36 /bin/ln
-rwxr-xr-x    1 root     root       381072 Jan 14 00:31 /sbin/sln
    如您所見,sln 的大小超過 ln 十倍。ln 比 sln 小這麼多是因爲它是動態可執行程序。動態可執行程序是不完整的程序,它依靠外部共享庫來提供運行所需的許多函數。
要查看 ln 依賴的所有共享庫的列表,可以使用 ldd 命令:
# ldd /bin/ln
libc.so.6 => /lib/libc.so.6 (0x40021000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
   如您所見,ln 依賴外部共享庫 libc.so.6 和 ld-linux.so.2。通常,動態鏈接的程序比其靜態鏈接的等價程序小得多。不過,靜態鏈接的程序可以在某些低級維護任務中發揮作用(動態共享庫減小了文件大小,以及運行時內存和磁盤空間的佔用,但是在動態鏈接dynamic loading時增加了時間開銷)。例如,sln 是修改位於 /lib 中的不同庫符號鏈接的極佳工具。但通常您會發現幾乎所有Linux系統上的可執行程序都是某種動態鏈接的變體。
動態裝入器:那麼,如果動態可執行程序不包含運行所需的所有函數,Linux 的哪部分負責將這些程序和所有必需的共享庫一起裝入,以使它們能正確執行呢?答案是動態裝入器(dynamic loader),它實際上是您在 ln 的 ldd 清單中看到的作爲共享庫相關性列出的 ld-linux.so.2 庫。動態裝入器負責裝入動態鏈接的可執行程序運行所需的共享庫。
    現在,讓我們迅速查看一下動態裝入器如何在系統上找到適當的共享庫。
    動態裝入器找到共享庫要依靠兩個文件 — /etc/ld.so.conf 和 /etc/ld.so.cache。如果您對 /etc/ld.so.conf 文件進行 cat 操作,您可能會看到一個與下面類似的清單:
$ cat /etc/ld.so.conf
/usr/X11R6/lib
/usr/lib/gcc-lib/i686-pc-linux-gnu/2.95.3
/usr/lib/mozilla
/usr/lib/qt-x11-2.3.1/lib
/usr/local/lib
ld.so.conf 文件包含一個所有目錄(/lib 和 /usr/lib 除外,它們會自動包含在其中)的清單,動態裝入器將在其中查找共享庫。
但是在動態裝入器能“看到”這一信息之前,必須將它轉換到 ld.so.cache 文件中。可以通過運行 ldconfig 命令做到這一點:
#ldconfig
當ldconfig 操作結束時,您會有一個最新的 /etc/ld.so.cache 文件,它反映您對 /etc/ld.so.conf 所做的更改。從這一刻起,動態裝入器在尋找共享庫時會查看您在 /etc/ld.so.conf 中指定的所有新目錄。
ldconfig 技巧
要查看 ldconfig 可以“看到”的所有共享庫,請輸入: # ldconfig -p | less
還有另一個方便的技巧可以用來配置共享庫路徑。有時候您希望告訴動態裝入器在嘗試任何 /etc/ld.so.conf 路徑以前先嚐試使用特定目錄中的共享庫。在您運行的較舊的應用程序不能與當前安裝的庫版本一起工作的情況下,這會比較方便。
要指示動態裝入器首先檢查某個目錄,請將LD_LIBRARY_PATH變量設置成您希望搜索的目錄。多個路徑之間用逗號分隔;例如:
#export LD_LIBRARY_PATH="/usr/lib/old:/opt/lib"
導出 LD_LIBRARY_PATH 後,如有可能,所有從當前 shell 啓動的可執行程序都將使用 /usr/lib/old 或 /opt/lib 中的庫,如果仍不能滿足一些共享庫相關性要求,則轉回到 /etc/ld.so.conf 中指定的庫。
    如果編譯時阻止使用共享庫,則編譯後文件的大小會大大增加。
例如:$cc -static hello1.c
然後查看大小$ls -l a.out
再$size a.out可以得到hello world的text正文段增加了300多KB。
 
4. 用Linux進行動態鏈接
    Linux中的動態鏈接的共享庫的過程。當用戶啓動一個應用程序時,它們正在調用一個可執行和鏈接格式(Executable and Linking Format,ELF)映像。內核首先將 ELF 映像加載到用戶空間虛擬內存中。然後內核會注意到一個稱爲 .interp 的 ELF 部分,它指明瞭將要被使用的動態鏈接器(/lib64/ld-linux-x86-64.so.2)。這與 UNIX® 中的腳本文件的解釋器定義(#!/bin/sh)很相似:只是用在了不同的上下文中。
luffy@luffy-laptop:/usr/bin$ readelf -l zip
Elf file type is EXEC (Executable file)
Entry point 0x402250
There are 9 program headers, starting at offset 64
 
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
......
補充:readelf的選項(一部分)
-a --all               Equivalent to: -h -l -S -s -r -d -V -A -I
-h --file-header       Display the ELF file header
-l --program-headers   Display the program headers
   --segments          An alias for --program-headers
-S --section-headers   Display the sections' header
   --sections          An alias for --section-headers
-g --section-groups    Display the section groups
-t --section-details   Display the section details
-e --headers           Equivalent to: -h -l -S
-s --syms              Display the symbol table
   --symbols           An alias for --syms
--dyn-syms             Display the dynamic symbol table
 
    由上面可以看出/lib64/ld-linux-x86-64.so.2本身就是一個 ELF 共享庫,但它是靜態編譯的並且不依賴其他共享庫。當需要動態鏈接時,內核會引導動態鏈接(ELF解釋器),該鏈接首先會初始化自身,然後加載指定的共享對象(已加載則不必)。接着它會執行必要的再定位,包括目標共享對象所使用的共享對象。在LD_LIBRARY_PATH 環境變量定義查找可用共享對象的位置。定義完成後,控制權會被傳回到初始程序以開始執行。
    再定位是通過一個稱爲 Global Offset Table(GOT)和 Procedure Linkage Table(PLT)的間接機制來處理的。這些表格提供了 ld-linux.so 或ld-linux-x86-64.so在再定位過程中加載的外部函數和數據的地址。這意味着無需改動需要間接機制(即,使用這些表格)的代碼:只需要調整這些表格。一旦進行加載,或者只要需要給定的函數,就可以發生再定位。
    再定位完成後,動態鏈接器就會允許任何加載的共享對象來執行可選的初始化代碼。該函數允許庫來初始化內部數據並備之待用。這個代碼是在上述 ELF 映像的.init 部分中定義的。在卸載庫時,它還可以調用一個終止函數(定義爲映像的.fini 部分)。當初始化函數被調用時,動態鏈接器會把控制權轉讓給加載的原始映像。
 
5. 用Linux進行動態加載
    動態加載是由應用程序自身管理共享庫的加載/鏈接(而Linux內核並不負責)。使用動態加載,應用程序能夠先指定要加載的庫,然後將該庫作爲一個可執行文件來使用(即調用其中的函數)。但是正如您在前面所瞭解到的,用於動態加載的共享庫與標準共享庫(ELF共享對象)過程一樣。ld-linux或ld-linux-x86-64.so動態鏈接器仍然參與到這個過程中。Dynamic Loading API就是爲動態加載而存在的,在頭文件dlfcn.h中聲明。
    首先是調用 dlopen,提供要訪問的文件對象和模式。調用 dlopen 的結果是稍候要使用的對象的句柄。mode 參數通知動態鏈接器何時執行再定位。有兩個可能的值。第一個是 RTLD_NOW,它表明動態鏈接器將會在調用 dlopen 時完成所有必要的再定位。第二個可選的模式是 RTLD_LAZY,它只在需要時執行再定位。這是通過在內部使用動態鏈接器重定向所有尚未再定位的請求來完成的。這樣,動態鏈接器就能夠在請求時知曉何時發生了新的引用,而且再定位可以正常進行。後面的調用無需重複再定位過程。
#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 );
 
6. dl的示例
用dl api實現shell,它允許操作員來指定庫、函數和參數。
 
  1. #include <stdio.h> 
  2. #include <dlfcn.h> 
  3. #include <string.h> 
  4.  
  5. #define MAX_STRING      80 
  6.  
  7. void invoke_method( char *lib, char *method, float argument ) 
  8.   void *dl_handle; 
  9.   float (*func)(float); 
  10.   char *error; 
  11.   /* Open the shared object */ 
  12.   dl_handle = dlopen( lib, RTLD_LAZY ); 
  13.   if (!dl_handle) { 
  14.     printf( "!!! %s\n", dlerror() ); 
  15.     return
  16.   } 
  17.   /* Resolve the symbol (method) from the object */ 
  18.   func = dlsym( dl_handle, method ); 
  19.   error = dlerror(); 
  20.   if (error != NULL) { 
  21.     printf( "!!! %s\n", error ); 
  22.     return
  23.   } 
  24.   /* Call the resolved method and print the result */ 
  25.   printf("  %f\n", (*func)(argument) ); 
  26.   /* Close the object */ 
  27.   dlclose( dl_handle ); 
  28.   return
  29.  
  30. int main( int argc, char *argv[] ) 
  31.   char line[MAX_STRING+1]; 
  32.   char lib[MAX_STRING+1]; 
  33.   char method[MAX_STRING+1]; 
  34.   float argument; 
  35.   while (1) { 
  36.     printf("> "); 
  37.     line[0]=0; 
  38.     fgets( line, MAX_STRING, stdin); 
  39.     if (!strncmp(line, "bye", 3)) break
  40.     sscanf( line, "%s %s %f", lib, method, &argument); 
  41.     invoke_method( lib, method, argument ); 
  42.   } 
編譯:指定-rdynamic用來通知鏈接器將所有符號添加到動態符號表中(目的是能夠通過使用 dlopen 來實現向後跟蹤)。-ldl表明一定要將dllib鏈接於該程序。
$ gcc -rdynamic -o dl dynamicloading.c -ldl
$ ./dl
> libm.so cosf 0.0
  1.000000
> bye
 
 
參考/轉載:
APUE
Linux programmer's manual
IBM文庫-Linux動態庫拋析 http://www.ibm.com/developerworks/cn/linux/l-dynamic-libraries/
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章