Intel平臺下Linux中ELF文件動態連接的加載、解析及實例分析(一): 加載

王瑞川 ( jeppeterone@163.com), linux愛好者

簡介: 動態連接,一個常常被人提起的話題。但在這方面不多有文章來闡明這個重要的軟件運行機制,只有一些關於動態連接庫編程的文章。本系列文章就是要從源代碼的層次來探討這個問題。 html

發佈日期: 2003 年 10 月 01 日
級別: 初級
訪問狀況 : 11676 次瀏覽
評論: 0 (查看 | 添加評論 - 登陸) linux

平均分 5 星 共 28 個評分 平均分 (28個評分)
爲本文評分

固然從文章的題目就能夠看出,intel平臺下的linux ELF文件的動態連接。一則是由於這一方面的資料查找比較方便,二則也是這個討論的意思比其它的動態連接要更爲重要(畢竟如今是intel的天下)。當 然,有了這麼一個例子,其它的平臺下的ELF文件的動態連接也就大同小異。你能夠在閱讀完了本文以後"舉一隅,而反三隅"了。 程序員

因爲這是一個系列的文章,我計劃分三部分來寫,第一部分主要分析加載,涉及dl_open這個函數的內容,但因爲這個函數所包含的內容 實在太多。這裏主要是它的_dl_map_object與_dl_init這兩個部分,由於這裏是把動態連接文件經過在ELF文件中的獲得信息映射到內存 空間中,而_dl_init中是一個特殊的初始化。這是對面向對象的函數實現的。 編程

第二部分我將分析函數解析與卸載,這裏要講的內容會比較多,但每個內容都不會多。首先是在前一篇中沒有說完的dl_open中的涉及 的_dl_map_object_deps和_dl_relocate_object兩個函數內容,由於這些都與函數解析的內容直接相關,因此安排在這 裏。而下面的函數解析過程_dl_runtime_resolve是在程序運行中的動態解析過程。這裏從本質上來說沒有太多的代碼,但它的精巧程度倒是最 多的(正是我這三篇文章的核心之處)。最後是一個dl_close的實現。這裏是一個結尾的工做,順帶一下是_dl_signal_cerror,與 _dl_catch_error的錯誤例外處理。 數組

第三部將給出injectso實例分析與應用,會介紹一個應用了動態連接的實例,並能夠在往後的程序調試過程當中使用的injectso 實例,它不只可讓咱們對前面所說的動態連接原理有一個更感性的認識,並且就這個實例而言,還能夠在之後的代碼開發過程當中來做爲一種動態打補丁的工具,甚 至有可能,我會在之後的文章中會用這個工具來介紹新的技術。 bash

1、歷史問題 網絡

關於動態連接,能夠說由來已久。若是追溯,最先的思想就在五十年代就有了,那時就想把一些公用的代碼放在內存中的一個地方上,在別的地 址用call即是了。到後來又發展到了 loading overlays(就是把在程序運行生命期不一樣的代碼在不一樣的時間段被加入內存),這是在六十年代的事。但這隻能算是"濫觴"時期。接近於咱們如今所說的 動態連接是在unix操做系統以後,由於從unix的設計結構而言,自己就是分紅模塊來實現一個複雜的功能的操做系統。但這些還不是現代意義上的動態鏈 接,緣由是現代意義上的動態連接要符合兩個特色: 數據結構

一、 動態的加載,就是當這個運行的模塊在須要的時候才被映射入運行模塊的虛擬內存空間中,如一個模塊在運行中要用到mylib.so中的myget函數,而在 沒有調用mylib.so這個模塊中的其它函數以前,是不會把這個模塊加載到你的程序中(也就是內存映射),這些內容在內核中實現,用的是頁面異常機制 (我可能在另外一篇文章中提到這個問題)。 多線程

二、 動態的解析,就是當要調用的函數被調用的時候,纔會去把這個函數在虛擬內存空間的起始地址解析出來,再寫到專門在調用模塊中的儲存地址內,如前面所說的你 已經調用了myget,因此mylib.so模塊確定已經被映射到了程序虛擬內存之中,而若是你再調用mylib.so中的myput函數,那它的函數地 址就在調用的時候纔會被解析出來。 app

(注:這裏用的程序就是通常所說的進程process,而模塊既多是你的程序的二進制代碼,也多是被你的程序所依賴的別的共享連接文件-------一樣ELF格式。)

在這兩點中頗有點像如今的操做系統中對內存的操做,也就是隻有當要用到一個內存空間中的時候纔會進行虛擬空間映射,而不是過早的把全部 的空間映射好,而只有當要從這個內存空間讀的時候才分配物理空間。這有點像第一條。而只有當對這個內存空間進行寫的時候產生一個COW(copy on write)。這就有點像第二條。

這樣的好處就是充分避免沒必要要的開銷。由於任何一個程序在運行的時候,大部分狀況下,不可能用到全部的調用函數。

這樣的思想方法提出與實現都是在八十年代的sun公司的SunOS的系統上。

關於這一段歷史,請你參見資料[1]。

ELF二進制格式文件與現代的動態連接思想大體是在同一時段造成的,它的來源是AT&T公司的最先的unix中的a.out二 進行文件格式。Bell labs的工做人員爲了使這種在unix的早期主要的文件格式適應當時新的軟件與操做系統的要求(如aix,SunOS,HP-UX這樣的unix變種, 對更普遍的應用程序的擴展要求,對面向對象的支持等等),就發明了ELF文件格式。

我在這裏並不詳細討論ELF文件的具體細節,這原本就能夠寫一篇很長的文章,你能夠參看資料[2]來獲得關於它的 ABI(application binary interface的規範)。但在ELF文件所採用的那種分層的管理方式卻不只在動態連接中起着重要的做用,並且這一思想能夠說是咱們計算機中的最古老, 也是最經典的思想。

對每一個ELF文件,都有一個ELF header,在這裏的每一個header有兩個數據成員,就是

Elf32_Off  e_phoff;
Elf32_Off  e_shoff;

它們分別表明了program header 與section header 在ELF文件中的偏移量。Program header 是總綱,而section header 則是第一個小目。

Elf32_Addr  sh_addr;
Elf32_Off  sh_offset;

Sh_addr這個section 在內存中的映射地址(對動態連接庫而言,這是一個相對量,它與整個ELF文件被加載的l_addr造成絕對地址)。Sh_offset是這個section header在文件中的偏移量。

用一圖來表示就是這樣的,它就是用elf header 來管理了整個ELF文件:



舉個例子,若是要從一個ELF動態連接庫文件中,根據已知的函數名稱,找到相應的函數起始地址,那麼過程是這樣的。

先從前面的ELF 的ehdr中找到文件的偏移e_phoff處,在這其中找到爲PT_DYNAMIC 的d_tag的phdr,從這個地址開始處找到DT_DYNAMIC的節,最後從其中找到這樣一個Elf32_Sym結構,它的st_name所指的字符 串與給定的名稱相符,就用st_value即是了。

這種的管理模式,能夠說很複雜,有時會看起來是繁瑣。如找一個function 的起始地址就要從 elf header >>program header >>symbol section >>function address 這樣的四個步驟。但這裏的根本的緣由是咱們的計算機是線性尋址的,而且馮*諾依曼提出的計算機體系結構相關,因此在前面說這是一個古老的思想。但一樣也是 因爲這樣的一個ELF文件結構,頗有利於ELF文件的擴充。咱們能夠設想,若是有一天,咱們的ELF文件爲了某種緣由,對它進行加密。這時若是要在ELF 文件中保存密鑰,這時候能夠在ELF文件中開闢一個專門的section encrypt ,這個section 的type 就是ST_ENCRYPT,那不就是能夠了嗎?這一點就能夠看出ELF文件格式設計者當初的苦心了(如今這個真的有這麼一個節了)。

回頁首

2、代碼舉例

講了這麼多,尚未真正講到在intel 32平臺下linux動態連接庫的加載與調用。在通常的狀況下,咱們所編寫的程序是由編譯器與ld.so這個動態連接庫來完成的。而若是要顯式的調用某一個動態連接庫中的程序,則下面是一個例子。

#include <dlfcn.h> 
#include <stdio.h> 
main() 
{ 
void *libc; 
void (*printf_call)();
char* error_text; 
if(libc=dlopen("/lib/libc.so.5",RTLD_LAZY)) 
  { 
   printf_call=dlsym(libc,"printf"); 
   (*printf_call)("hello, world\n"); 
dlclose(libc);
return 0;
}
error_text= dlerror();
printf(error_test);
return -2;
}

在這裏先用dlopen來打開一個動態連接庫文件,而這個過程比咱們這裏看到的內容多的多,我會在下面用很大的篇幅來講明這一點,而它 返回的參數是一個指針,確切的說是struct link_map*,而dlsym就是在這個struct link_map* 與函數名稱一塊兒決定這個函數在這個進程中的地址,這個過程用術語來講就是函數解析(function resolution)。而最後的dlclose就是釋放剛纔在dlopen中獲得的資源,這個過程與咱們在加載的share object file module,內核中的程序是大概相同的,只不過這裏是在用戶態,而那個是在內核態。從函數的複雜性而言這裏還要複雜一些(最後有一點要說明,若是你想編 譯上面的文件-------文件名若是是test那就不能用通常的gcc -o test test.c ,而應該是gcc -c test test.c -ldl這樣才能編譯經過,由於不這樣編譯器會找不到dlopen 與dlsym dlclose這些特別函數的庫文件libdl.so.2, -ldl 就是加載它的標誌的)。

回頁首

3、_dl_open加載過程分析

本文以及之後的兩篇文章將都以上面的程序所展現的而講解。也就是以dlopen >> dlsym >> dlclose 的方式 來說解這個過程,但有幾點先要說明: 我在這裏所展現的源代碼來自glibc 2.3.2版本。但因爲原來的代碼,從代碼的移植與健壯的考慮,而有許多的防止出錯,與關於不一樣平臺的代碼,在這裏大部分是出錯處理代碼,我把這些的代碼 都刪除。而且只以intel 32平臺下的代碼爲準。還有,在這裏的還考慮到了多線程狀況下的動態連接庫加載,這裏也不予以包括在內(由於如今的linux內核中沒有對內核線程的支 持)。因此你所看到的代碼,在儘可能保證說明動態連接加載與函數解析的狀況做了多數的刪減,代碼量大概只有原來的四分之一左右,同時最大程度保持了原來代碼 的風格,突出核心功能。儘管如此,仍是有高達2000行以上的代碼,請你們耐心的解讀。我也會對其中可能的難解之處做出詳細的說明。讓你們真正體會到代碼 設計與動態解析的真諦。

第一個函數在dl-open.c中

2672  void* internal_function 
  2673  _dl_open (const char *file, int mode, const void *caller)
  2674  {
  2675     struct dl_open_args args;
  2676  
  2677       __rtld_lock_lock_recursive (GL(dl_load_lock));
  2678  
  2679     args.file = file;
  2680     args.mode = mode;
  2681     args.caller = caller;
  2682     args.map = NULL;
  2683  
  2684     dl_open_worker(&args);
  2685        __rtld_lock_unlock_recursive (GL(dl_load_lock));
  2686     
  2687  }

這裏的internal_function是代表這個函數從寄存器中傳遞參數,而它的定義在configure.in中獲得的。

# define internal_function __attribute__ ((regparm (3), stdcall))

這其中的regparm就是gcc的編譯選項是從寄存器傳遞3個參數,而stdcall代表這個函數是由調用函數來清棧,而通常的函數 是由調用者來負責清棧,用的是cdecl。 __rtld_lock_lock_recursive (GL(dl_load_lock));與__rtld_lock_unlock_recursive (GL(dl_load_lock));在如今尚未徹底定義,至少在linux中是沒有的,但能夠參考在linux/kmod.c 中的request_module中爲了防止過分嵌套而加的一個鎖。

而其它的內容就是一個封裝了。

dl_open_worker是真正作動態連接庫映射並構造一個struct link_map而這是一個絕對重要的數據結構它的定義因爲太長,我會放在第二篇文章結束的附錄中介紹,由於那時你能夠回頭再理解動態連接庫加載與解析的 過程,而在下面的具體函數中出現了做實用性的解釋,下面咱們分段來看:

_dl_open() >> dl_open_worker()
  2532  static void
  2533  dl_open_worker (void *a)
2534  {
……………………..
2547  args->map = new = _dl_map_object (NULL, file, 0, lt_loaded, 0, mode);

這裏就是調用_dl_map_object 來把文件映射到內存中。原來的函數要從不一樣的路徑搜索動態連接庫文件,還要與SONAME(這是動態連接庫文件在運行時的別名)比較,這些內容我在這裏都刪除了。

_dl_open() >> dl_open_worker() >> _dl_map_object()
  1693  struct link_map *
  1694  internal_function
  1695  _dl_map_object (struct link_map *loader, const char *name, int preloaded,
  1696      int type, int trace_mode, int mode)
  1697  {
  1698    int fd;
  1699    char *realname;
  1700    char *name_copy;
  1701    struct link_map *l;
  1702    struct filebuf fb;
  1703  
  1704  
  1705    /* Look for this name among those already loaded.  */
  1706    for (l = GL(dl_loaded); l; l = l->l_next)
  1707      {
  1708          if (!_dl_name_match_p (name, l))
…………….
  1721          return l;
  1722      } 
  1723  
  1724     fd = open_path (name, namelen, preloaded, &env_path_list,
  1725        &realname, &fb);
  1726  
  1727     l = _dl_new_object (name_copy, name, type, loader);
  1728  
  1729     return _dl_map_object_from_fd (name, fd, &fb, realname, loader, type, mode);
  1730  
  1731  
1732  }/*end of _dl_map_object*/

這裏先在已經被加載的一個動態連接庫的鏈中搜索,在1706與1721行中就是做這一件事。想起來也很簡單,由於可能在一個可執行文件依賴好幾個動態連接庫。而其中有幾個動態連接庫或許都依賴於同一個動態連接文件,可能早就加載了這樣一個動態連接庫,就是這樣的狀況了。

下面open_path是一個關鍵,這裏要指出的是env_path_list獲得的方式有幾種,一是在系統環境變量,二就是DT_RUNPATH所指的節中的字符串(參見下面的 附錄),還有更復雜的,是從其它要加載這個動態連接庫文件的動態連接庫中獲得的環境變量-------這些問題咱們都不說明了。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> open_path()
  1289  static int open_path (const char *name, size_t namelen, int preloaded,
  1290       struct r_search_path_struct *sps, char **realname,
  1291       struct filebuf *fbp)
  1292  
  1293  {
  1294    struct r_search_path_elem **dirs = sps->dirs;
  1295    char *buf;
  1296    int fd = -1;
  1297    const char *current_what = NULL;
  1298    int any = 0;
  1299  
  1300    buf = alloca (max_dirnamelen + max_capstrlen + namelen);
  1301  
  1302    do
  1303      {
  1304        struct r_search_path_elem *this_dir = *dirs;
  1305        size_t buflen = 0;
 ………………
  1310       struct stat64 st;
  1311       
  1312  
  1313        edp = (char *) __mempcpy (buf, this_dir->dirname, this_dir->dirnamelen);
  1314        for (cnt = 0; fd == -1 && cnt < ncapstr; ++cnt)
  1315    {
  1316      /* Skip this directory if we know it does not exist.  */
  1317      if (this_dir->status[cnt] == nonexisting)
  1318        continue;
  1319  
  1320      buflen = ((char *) __mempcpy (__mempcpy (edp, capstr[cnt].str,
  1321            capstr[cnt].len), name, namelen)- buf);
  1322  
  1323     
  1324      fd = open_verify (buf, fbp);
  1325          
  1326          
  1327      __xstat64 (_STAT_VER, buf, &st);
  1328      
  1329     
  1341    }
  1342  
…………….
    1358    }

在這上面的alloc是在棧上分配空間的函數,這樣就不用擔憂在函數結束的時候出現內存泄漏的狀況(好的程序員真的要對內存的分配熟諳 於心)。1313行就是把r_search_path_elem的dirname copy過來,而在1320至1321行的內容就是爲這個路徑加上最後的'/'路徑分隔號,而capstr就是根據不一樣的操做系統與體系獲得的路徑分隔 號。這實際上是一個很好的例子,由於__memcpy返回的參數是dest string所copy的最後的一個字節的地址,因此每copy以後就會獲得新的地址,若是用strncpy來寫的話,就要用這樣的方法

strncpy(edp, capstr[cnt].str, capstr[cnt].len);
edp+=capstr[cnt].len;
strncpy(edp,name, namelen);
edp+=namelen;
buflen=edp-buf;

這就要用四句,而這裏用了一句就能夠了。

下面的open_verify是打開這個buf所指的文件名,fbp是從這個文件獲得的文件開時1024字節的內容,並對文件的有效性 進行檢查,這裏最主要的是ELF_IMAGIC覈對。若是成功,就返回一個大於-1的文件描述符。整個open_path就這樣完成了打開文件的方法。

_dl_new_object是一個分配struct link_map* 數據結構並填充一些最基本的參數。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_new_object()
  2027  struct link_map *
  2028  internal_function
  2029  _dl_new_object (char *realname, const char *libname, int type,
  2030      struct link_map *loader)
  2031  
  2032  {  
  2033    struct link_map *l;
  2034    int idx;
  2035    size_t libname_len = strlen (libname) + 1;
  2036    struct link_map *new;
  2037    struct libname_list *newname;
  2038  
  2039     new = (struct link_map *) calloc (sizeof (*new) + sizeof (*newname)
  2040              + libname_len, 1);
  2041  
………………..
  2046  
  2047    new->l_name = realname;
  2048    new->l_type = type;
  2049    new->l_loader = loader;
  2050  
  2051    new->l_scope = new->l_scope_mem;
  2052    new->l_scope_max = sizeof (new->l_scope_mem) / sizeof (new->l_scope_mem[0]);
  2053  
  2054   if (GL(dl_loaded) != NULL)
  2055      {
  2056        l = GL(dl_loaded);
  2057        while (l->l_next != NULL)
  2058    l = l->l_next;
  2059        new->l_prev = l;
  2060        /* new->l_next = NULL;  Would be necessary but we use calloc.  */
  2061        l->l_next = new;
  2062  
  2063        /* Add the global scope.  */
  2064        new->l_scope[idx++] = &GL(dl_loaded)->l_searchlist;
  2065      }
  2066    else
  2067      GL(dl_loaded) = new;
  2068    ++GL(dl_nloaded);
 ………….
  2080  
  2081      return new;
  2082  
  2083  }

在2039行的內存分配是一個把libname 與name的數據結構也一同分配,是一種零用整取的策略。從2043-2053行都是爲struct link_map 的成員數據賦值。從2054-2067行則是把新的struct link_map* 加入到一個單鏈中,這是在之後是頗有用的,由於這樣在一個執行文件中若是要總體管理它相關的動態連接庫,就能夠以單鏈遍歷。

若是要加載的動態連接庫尚未被映射到進程的虛擬內存空間的話,那只是準備工做,真正的要點在_dl_map_object_from_fd()這個函數開始的。由於這以後,每一步都有關動態連接庫在進程中發揮它的做用而必須的條件。

這上段比較長,因此分段來看,

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd()
  1391  struct link_map *
  1392  _dl_map_object_from_fd (const char *name, int fd, struct filebuf *fbp,
  1393        char *realname, struct link_map *loader, int l_type,
  1394        int mode)
  1395  
  1396  {
  1397  
  1398    struct link_map *l = NULL;
  1399    const ElfW(Ehdr) *header;
  1400    const ElfW(Phdr) *phdr;
  1401    const ElfW(Phdr) *ph;
  1402    size_t maplength;
  1403    int type;
  1404    struct stat64 st;
  1405  
  1406    __fxstat64 (_STAT_VER, fd, &st);
…………
  1413    for (l = GL(dl_loaded); l; l = l->l_next)
  1414      if (l->l_ino == st.st_ino && l->l_dev == st.st_dev)
  1415        {
……….
  1418    __close (fd);
……………
  1422    free (realname);
  1423    add_name_to_object (l, name);
  1424  
  1425    return l;
1426  }

這裏先開始就要從再找一遍,若是找到了已經有的struct link_map* 要加載的libname(的而比較的依據是它的與st_ino,這是物理文件在內存中編號,且文件的設備號st_dev相同,這是從比較底層來比較文件, 具體的緣由,你能夠參看我將要發表的《從linux的內存管理看文件共享的實現》)。之因此採起這樣再查一遍,由於若是進程從要開始打開動態連接庫文件, 走到這裏可能要通過很長的時間(據我做的實驗來看,對第一次打開的文件大概也就在200毫秒左右---------主要的時間是硬盤的尋道與讀盤,但這對 於計算機的進程而言已是很長的時間了。)因此,有可能別的線程已經讀入了這個動態連接庫,這樣就沒有必要再作下去了。這與內核在文件的打開文件所用的思 想是一致的。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd()
  1427  
  1428    /* This is the ELF header.  We read it in `open_verify'.  */
  1429    header = (void *) fbp->buf;
  1430  
  1431    l->l_entry = header->e_entry;
  1432    type = header->e_type;
  1433    l->l_phnum = header->e_phnum;
  1434  
  1435    maplength = header->e_phnum * sizeof (ElfW(Phdr));
  1436

這一段所做的爲下面的ELF文件的分節映射入內存作一點準備(要讀寫phdr的數組)。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd()
  1438       /* Scan the program header table, collecting its load commands.  */
  1439      struct loadcmd
  1440        {
  1441    ElfW(Addr) mapstart, mapend, dataend, allocend;
  1442    off_t mapoff;
  1443    int prot;
  1444        } loadcmds[l->l_phnum], *c;
1445  size_t nloadcmds = 0;

這裏把數據結構定義在函數內部,能保證這是一個局部變量定義,與面向對象中的private的效果是同樣的。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd()
  1448      for (ph = phdr; ph < &phdr[l->l_phnum]; ++ph)
  1449        switch (ph->p_type)
  1450    {
………..
  1454    case PT_DYNAMIC:
  1455      l->l_ld = (void *) ph->p_vaddr;
  1456      l->l_ldnum = ph->p_memsz / sizeof (ElfW(Dyn));
  1457      break;
  1458  
  1459    case PT_PHDR:
  1460      l->l_phdr = (void *) ph->p_vaddr;
  1461      break;
  1462  
  1463    case PT_LOAD:
 …………..
  1467      c = &loadcmds[nloadcmds++];
  1468      c->mapstart = ph->p_vaddr & ~(ph->p_align - 1);
  1469      c->mapend = ((ph->p_vaddr + ph->p_filesz + GL(dl_pagesize) - 1)
  1470             & ~(GL(dl_pagesize) - 1));
  1471      c->dataend = ph->p_vaddr + ph->p_filesz;
  1472      c->allocend = ph->p_vaddr + ph->p_memsz;
  1473      c->mapoff = ph->p_offset & ~(ph->p_align - 1);
…………..
  1480      c->prot = 0;
  1481      if (ph->p_flags & PF_R)
  1482        c->prot |= PROT_READ;
  1483      if (ph->p_flags & PF_W)
  1484        c->prot |= PROT_WRITE;
  1485      if (ph->p_flags & PF_X)
  1486        c->prot |= PROT_EXEC;
    1488      break;
  …………
1493  }

在ELF文件的規範中,根據不一樣的program header 不一樣,要實現不一樣的功能,採用不一樣的處理策略,具體的內容請參看 附錄2中的說明。這裏沒有出現通常的default 但實際運行與下面的語句是等價的:

default: 
   continue;

真是達到程序簡潔的特色。

但有一個特別要指出的是PT_LOAD的那些,把全部的能夠加載的節都在加載的數據結構中loadcmds中構建完成,是一個好的想法。特別是指針的妙用,值得學習(1467 c = &loadcmds[nloadcmds++];)。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd()
  1498      c = loadcmds;
  …………
  1501      maplength = loadcmds[nloadcmds - 1].allocend - c->mapstart;
  1502  
  1503      if (__builtin_expect (type, ET_DYN) == ET_DYN)
  1504        {
…………….
  1521    l->l_map_start = (ElfW(Addr)) __mmap ((void *)0, maplength,
  1522                  c->prot, MAP_COPY | MAP_FILE,
  1523                  fd, c->mapoff);
  1524  
  1525          l->l_map_end = l->l_map_start + maplength;
  1526    l->l_addr = l->l_map_start - c->mapstart;
………..
  1535    __mprotect ((caddr_t) (l->l_addr + c->mapend),
  1536          loadcmds[nloadcmds - 1].allocend - c->mapend,
  1537          PROT_NONE);
  1538  
  1539    goto postmap;
1540  }

在1521-1526行之間就是把整個文件都進行了映射,妙處在1498行與1501行,是把頭與尾的兩個PT_LOAD program header 的內容都計算在內了。而1503行就是咱們這裏的情景,由於這是動態連接庫的加載。而1535行的修改虛擬內存的屬性,就是把映射在最高地址的空白失效。 這是一種保護。爲了防止有人利用這裏大作文章。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd()
  1546       while (c < &loadcmds[nloadcmds])
  1547        {
  1548    
  1549        postmap:
  1550    if (l->l_phdr == 0
  1551        && (ElfW(Off)) c->mapoff <= header->e_phoff
  1552        && ((size_t) (c->mapend - c->mapstart + c->mapoff)
  1553      >= header->e_phoff + header->e_phnum * sizeof (ElfW(Phdr))))
……
  1555      l->l_phdr = (void *) (c->mapstart + header->e_phoff - c->mapoff);
  1556  
  1557    if (c->allocend > c->dataend)
  1558      {
………..
  1561        ElfW(Addr) zero, zeroend, zeropage;
  1562  
  1563        zero = l->l_addr + c->dataend;
  1564        zeroend = l->l_addr + c->allocend;
  1565        zeropage = ((zero + GL(dl_pagesize) - 1)
  1566        & ~(GL(dl_pagesize) - 1));
  1567  
  1568        if (zeroend < zeropage)
……….
  1571          zeropage = zeroend;
  1572  
  1573        if (zeropage > zero)
  1574          {
…….
  1576      if ((c->prot & PROT_WRITE) == 0)
  1577        {
  1578          /* Dag nab it.  */
  1579        __mprotect ((caddr_t) (zero & ~(GL(dl_pagesize)
  1580             - 1)),  GL(dl_pagesize),
   1581                       c->prot|PROT_WRITE) < 0);
  1582            
  1583        }
  1584      memset ((void *) zero, '\0', zeropage - zero);
  1585      if ((c->prot & PROT_WRITE) == 0)
  1586        __mprotect ((caddr_t) (zero & ~(GL(dl_pagesize) - 1)),
  1587              GL(dl_pagesize), c->prot);
  1588          }
  1589  
  1590        if (zeroend > zeropage)
  1591          {
……..
  1593      caddr_t mapat;
  1594      mapat = __mmap ((caddr_t) zeropage, zeroend - zeropage,
  1595          c->prot, MAP_ANON|MAP_PRIVATE|MAP_FIXED,
  1596          ANONFD, 0);
  1597      
  1598          }
  1599      }
  1600  
  1601    ++c;
1602  }

這裏所做的與上面的相相似,根據在前面從PT_LOAD program header 獲得的文件映射的操做屬性進行修改,但在zeroend>zerorpage的時候不一樣,把它映射成爲進程獨享的數據空間。這也就是通常的初始化數 據區BSS的地方。由於zeroend是在文件中的映射的頁面對齊尾地址,而zeropage是文件中的內容映射的頁面對齊尾地址,這其中的差就是爲未初 始化數據準備的,這在1593-1597行之間體現,要把它的屬性改爲可寫的,且全爲0。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd()
  1606  if (l->l_phdr == NULL)
  1607        {
……..
  1611    ElfW(Phdr) *newp = (ElfW(Phdr) *) malloc (header->e_phnum
  1612                * sizeof (ElfW(Phdr)));
  1613    
  1614    l->l_phdr = memcpy (newp, phdr,
  1615            (header->e_phnum * sizeof (ElfW(Phdr))));
  1616    l->l_phdr_allocated = 1;
  1617        }
  1618      else
  1619        /* Adjust the PT_PHDR value by the runtime load address.  */
1620  (ElfW(Addr)) l->l_phdr += l->l_addr;

把phdr 就是program header 也歸入struct link_map的管理之中,通常的狀況是不會有的,因此要copy過來。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd()
  1625       elf_get_dynamic_info (l);

這裏調用的函數elf_get_dynamic_info是在加載過程當中最重要的一個之一,由於在這以後的幾乎全部的對動態連接管理的內容都要用要與這裏的l_info數據組相關。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> 
  _dl_map_from_fd() >> elf_get_dynamic_info()
  2826  static inline void __attribute__ ((unused, always_inline))
  2827  elf_get_dynamic_info (struct link_map *l)
  2828  { 
  2829    ElfW(Dyn) *dyn = l->l_ld;
  2830    ElfW(Dyn) **info;
  2831  
  2832  
  2833    info = l->l_info;
  2834  
  2835    while (dyn->d_tag != DT_NULL)
  2836      {
  2837        if (dyn->d_tag < DT_NUM)
  2838    info[dyn->d_tag] = dyn;
 ……………
  2853        ++dyn;
  2854      }
………….
  2858    if (l->l_addr != 0)
  2859      {
  2860        ElfW(Addr) l_addr = l->l_addr;
  2861  
  2862        if (info[DT_HASH] != NULL)
  2863    info[DT_HASH]->d_un.d_ptr += l_addr;
  2864        if (info[DT_PLTGOT] != NULL)
  2865    info[DT_PLTGOT]->d_un.d_ptr += l_addr;
  2866        if (info[DT_STRTAB] != NULL)
  2867    info[DT_STRTAB]->d_un.d_ptr += l_addr;
  2868        if (info[DT_SYMTAB] != NULL)
  2869    info[DT_SYMTAB]->d_un.d_ptr += l_addr;
……………….
  2874  
…………
  2876        if (info[DT_REL] != NULL)
  2877    info[DT_REL]->d_un.d_ptr += l_addr;
………….
  2879  
  2880        if (info[DT_JMPREL] != NULL)
  2881    info[DT_JMPREL]->d_un.d_ptr += l_addr;
  2882        if (info[VERSYMIDX (DT_VERSYM)] != NULL)
  2883    info[VERSYMIDX (DT_VERSYM)]->d_un.d_ptr += l_addr;
2884  }
………….
2889  }

上面的__attribute__ 中的unused 是爲了消除編譯器在-Wall 狀況下對於其中可能沒有用到在函數中的局部變量發出警告,而alwayse_inline,很好解釋,就是內聯函數的強制標誌。

2829行的l->l_ld是在前面的__dl_map_object_from_fd中的1455被給定的。也就是全部關於動態連接節的所在地址(參看 附錄B中的解釋)。

很明顯在2835至2854行之間的循環就是把l_info的內容都填充好。 這爲以後有很大的做用,由於這些節是能夠找到如函數名與定位信息的,這裏的的妙處是把數組的偏移量與d_tag相關聯,代碼簡潔。

2856至2885即是對動態連接庫的調整過程(這裏調整的每個節都是與函數解析有重要關係的,詳細內容可參看 附錄A),若是咱們考慮的更遠一點,在前面的函數中的1521行一開始把整個文件連續的映射入內存,在這裏就很好的獲得解釋,若是不是連續的,就沒有辦法在這裏做一個統一的調整了。

_dl_open() >> dl_open_worker() >> _dl_map_object() >> _dl_map_from_fd()
  1662    /* Finally the file information.  */
  1663    l->l_dev = st.st_dev;
  1664    l->l_ino = st.st_ino;
  1667    return l;
  1670  }

最後就是把設備號與節點號加入就完成了最後的dl_map_object就好了,回頭看1414行中對已經加載的文件的搜索,就能夠明白這裏的做用了。

再回到dl_open_worker中

_dl_open() >> dl_open_worker()
  2550  /* It was already open.  */
  2551    if (new->l_searchlist.r_list != NULL)
  2552      {
…….
  2556        if ((mode & RTLD_GLOBAL) && new->l_global == 0)
  2557    (void) add_to_global (new);
  2558  
  2559        /* Increment just the reference counter of the object.  */
  2560        ++new->l_opencount;
  2561  
  2562        return;
2563  }

這就是對已經被打開了的,就對l_opencount加一返回了。但爲何要在2551行以後做出這一判斷呢,那是在下面的代碼有關,_dl_map_object_deps會把l_searchlist加載入。

_dl_open() >> dl_open_worker()
  2565    /* Load that object's dependencies.  */
  2566    _dl_map_object_deps (new, NULL, 0, 0, mode & __RTLD_DLOPEN);
……………
  2573    l = new;
  2574    while (l->l_next)
  2575      l = l->l_next;
  2576    while (1)
  2577      {
  2578        if (! l->l_relocated)
  2579    {
  2580        _dl_relocate_object (l, l->l_scope, lazy, 0);
  2581    }
  2582  
  2583        if (l == new)
  2584    break;
  2585        l = l->l_prev;
  2586      }

在這裏的_dl_map_object_deps會填充l_searchlist.r_list,對於這個函數與下面的 _dl_relocate_object因爲與函數的解析關係比較大,因此我放在《Intel平臺下linux中ELF文件動態連接的加載、解析及實例分 析(中)-----------函數解析與卸載篇》講解。但能夠把這個看成這個新加載的動態連接庫的所依賴的動態連接庫的struct link_map* 放入這個指針的列表中(就是l_search_list中),_dl_relocate_object是對這個動態連接庫中的函數重定位,而這裏用的,這 裏之因此用的是while (1) 2576行,是由於在前面用的_dl_map_object_deps會把這個動態連接庫所依賴的動態連接庫也加載進來,這其中就會有沒有重定位的。

_dl_open() >> dl_open_worker()
  2592    for (i = 0; i < new->l_searchlist.r_nlist; ++i)
  2593      if (++new->l_searchlist.r_list[i]->l_opencount > 1
  2594    && new->l_searchlist.r_list[i]->l_type == lt_loaded)
  2595        {
  2596    struct link_map *imap = new->l_searchlist.r_list[i];
  2597    struct r_scope_elem **runp = imap->l_scope;
  2598    size_t cnt = 0;
  2599  
  2600    while (*runp != NULL)
  2601      {
 …………
  2605        if (*runp == &new->l_searchlist)
  2606          break;
  2607  
  2608        ++cnt;
  2609        ++runp;
  2610      }
  2611  
  2612    if (*runp != NULL)
  2613      /* Avoid duplicates.  */
  2614      continue;
…………
  2642    imap->l_scope[cnt++] = &new->l_searchlist;
  2643    imap->l_scope[cnt] = NULL;
2644  }

這段代碼若是從實現功能上來說是很簡單的,就是在咱們剛新加入的動態連接庫new中的l_searchlist中(這些都是在前面被 dl_object_deps加載入的被依賴的動態連接庫數組)imap->l_scope查找,若是裏面runp 有&new->l_searchlist,就不用對原來的imap->l_scope擴充了,但若是沒有就要完成2616到2644 行的擴充工做。

但在這以後的背景緣由,倒是&new->l_searchlist其實就是new自己。在通常狀況下,若是這個依賴的動 態連接庫在new被加載以前已經加載(具體的緣由會在下一篇文章關於動態連接庫函數解析中說明),那就會遇到這種狀況。而咱們又不能保證兩個動態連接庫之 間的互相依賴狀況的發生,以下圖,那這裏的解決辦法即是一個補救措施了。



_dl_open() >> dl_open_worker()
  2647    _dl_init (new, __libc_argc, __libc_argv, __environ);

這是要調用動態連接庫自備的初始函數。這有點相似與insmod時調用的init_module的內容。至於這其中所傳遞的 __libc_argc, __libc_argv, __environ三個參數是在你的可執行文件被運行的時候由bash引入的輸入參數與環境變量,通常的動態連接庫是沒有什麼用處了。

_dl_open() >> dl_open_worker()  >>  _dl_init()
  1118  void
  1119  internal_function
  1120  _dl_init (struct link_map *main_map, int argc, char **argv, char **env)
  1121  {
  1122  
  1123    ElfW(Dyn) *preinit_array = main_map->l_info[DT_PREINIT_ARRAY];
  1124    ElfW(Dyn) *preinit_array_size = main_map->l_info[DT_PREINIT_ARRAYSZ];
  1125    unsigned int i;
  1126  
  1127  
  1128    ElfW(Addr) *addrs;
  1129    unsigned int cnt;
  1130  
  1131      
  1132    addrs = (ElfW(Addr) *) (preinit_array->d_un.d_ptr + main_map->l_addr);
  1133    for (cnt = 0; cnt < i; ++cnt)
  1134      (init_t) addrs[cnt]) (argc, argv, env);
………….
  1146    i = main_map->l_searchlist.r_nlist;
  1147    while (i-- > 0)
  1148      call_init (main_map->l_initfini[i], argc, argv, env);
  1149  
  1150  
  1151    
  1152  
1153  }

先是調用 DT_PREINIT的內容,這是在init之的init方法。我想這個之因此要實現,不光是爲讓動態連接庫的開發者有更好的開發接口,並且仍是在以它所依賴的動態連接庫以前進行一些初始化工做,借鑑於面向對象的構造函數。

_dl_open() >> dl_open_worker()  >>  _dl_init()  >> call_init()
  1072  static void
  1073  call_init (struct link_map *l, int argc, char **argv, char **env)
  1074  {
  1075  
  1076     if (l->l_init_called)
  1078      return;
  1079  
  1082    l->l_init_called = 1;
………..
  1089    if (l->l_info[DT_INIT] != NULL)
  1090      {
  1091       init_t init = (init_t) DL_DT_INIT_ADDRESS(l, l->l_addr + 
                                   l->l_info[DT_INIT]->d_un.d_ptr);
  1092  
  1093        /* Call the function.  */
  1094        init (argc, argv, env);
  1095      }
  1098    ElfW(Dyn) *init_array = l->l_info[DT_INIT_ARRAY];
  1099    if (init_array != NULL)
  1100      {
  1101        unsigned int j;
  1102        unsigned int jm;
  1103        ElfW(Addr) *addrs;
  1104  
  1105        jm = l->l_info[DT_INIT_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr));
  1106  
  1107        addrs = (ElfW(Addr) *) (init_array->d_un.d_ptr + l->l_addr);
  1108        for (j = 0; j < jm; ++j)
  1109    ((init_t) addrs[j]) (argc, argv, env);
  1110      }
  1111  
  1112  
  1113  }

1076-1082行的內容一看便知,是防止兩次初始化。下面是對DT_INIT與DT_INIT_ARRAY的函數調用,值得注意的 是,前面調用call_init時是對l_initfine的數組進行的,這裏就包括了這個新的動態連接庫所依賴的。就這樣完成了 dl_open_worker()這個過程。

到此,咱們至少大體上已經把動態連接庫的過程說了一遍(固然,除了_dl_map_object_deps和_dl_relocate_object)到如今咱們已經明白瞭如下幾點:

一、 動態連接庫的struct link_map* 的產生與組織過程(這個在_dl_new_object中實現)

二、 動態連接庫是如何被提取信息入struct link_map*中的,並被加載的(這個在open_verify 與dl_map_object_from_fd,elf_get_dynamic_info這三個函數中實現)

三、 動態連接庫自己的初始化過程(這個在_dl_init中實現)

整體上函數調用結構在下圖中一個示意圖。



但還有幾個問題沒有被提到

一、 可執行文件中的函數被如何定位到動態連接庫的函數體中的。

二、 一個動態連接庫與依賴的動態連接庫之間是什麼關係,它們之間是如何聯繫。

三、 一個函數是怎樣被動態解析,它又是使函數調用方與實現方成爲一體的。

這些問題我會在《Intel平臺下linux中ELF文件動態連接的加載、解析及實例分析(中)-----------函數解析與卸載篇》進行闡明,敬請期待。

回頁首

附錄A:動態連接section 類型及說明

類型 數值 d_un所指 EXEC可選性 DYN可選性 說明
DT_NULL 0 不用 必須 必須 這個表示動態連接section的結束標誌
DT_NEEDED 1 d_val 可選 可選 這個節d_val是包含了以null結尾的字符串,這些字符串是這個動態連接文件或可執行文件的依賴文件名稱與路徑的節的開始地址
DT_PLTRELSZ 2 d_val 可選 可選 這裏的d_val是過程連接表(procedure linkage table)的大小,它與DT_JMPREL結合使用
DT_PLTGOT 3 d_ptr 可選 可選 這裏的d_ptr是過程連接表或全局偏移量表的起始地址。
DT_HASH 4 d_ptr 必須 必須 這裏的d_val是符號哈希表的起始地址。
DT_STRTAB 5 d_ptr 必須 必須 這裏d_ptr所給出的是符號名稱字符串表的起始地址。
DT_SYMTAB 6 d_ptr 必須 必須 這裏的d_ptr是Elf32_sym數據結構在的節表中的起始地址。
DT_STRSZ 10 d_val 必須 必須 這d_val是上面的DT_STRTAB節的大小。
DT_SYMENT 11 d_val 必須 必須 這裏的d_val是DT_SYMTAB中的每一個Elf32_Sym數據結構的大小
DT_INIT 12 d_ptr 可選 可選 這裏的d_ptr是一個動態連接庫被加載時調用的初始函數所在節的起始地址。
DT_FINI 13 d_ptr 可選 可選 這裏的d_ptr是一個動態連接庫被卸載時,調用解構函數所在節的起始地址。
DT_REL 17 d_ptr 必須 可選 這裏的d_ptr與上面的DT_RELA類似,是Elf32_Rel數據結構所在節的起始地址,它在intel平臺下用。
DT_RELSZ 18 d_val 必須 可選 這d_val與上面的DT_REL上面的相對應,代表上面的那個節的大小。
DT_RELENT 19 d_val 必須 可選 這裏的d_val是DT_REL中的一個Elf32_Rel的數據結構的大小。
DT_PLTREL 20 d_val 可選 可選 這裏的d_val是與過程連接表(procedure linkage table)有關的,就是DT_REL 或DT_RELA的值,也就是這個ELF文件用的是DT_REL的話那d_val就是17,而若是是DT_RELA的話就是7
DT_JMPREL 23 d_ptr 可選 可選 這是咱們這裏最重要的Elf_Dyn,由於d_ptr所指的就是GOT(global object table)全局對象表,這實際上是一個導入函數與全局變量的地址表。
DT_INIT_ARRAY 25 d_ptr 可選 可選 這裏的d_ptr是要初始化函數跳轉表起始相對地址。
DT_FINI_ARRAY 26 d_ptr 可選 可選 這裏的d_ptr是要解構時調用的函數跳轉表起始相對地址。
DT_INIT_ARRAYSZ 27 d_val 可選 可選 這裏的d_val代表前面的DT_INIT_ARRAY的大小。
DT_FINI_ARRAYSZ 28 d_val 可選 可選 這裏的d_val是前面的DT_FINI_ARRAY的大小。
DT_ENCODING 32 d_val或d_ptr 沒有規定 沒有規定 如今這個節尚未規定,但很明顯就是爲之後的加密而準備的。
DT_PREINIT_ARRAY 32 d_ptr 可選 不用 這裏d_ptr是在調用main函數以前的調用初始函數跳轉表的起始地址。
DT_PREINIT_ARRAYSZ 33 d_val 可選 不用 這裏的d_val是前面的DT_PREINIT_ARRAY的大小

上面只列出了在咱們這裏要用到的項目,而ELF文件規範的設計者還爲它留下了能夠在不一樣的系統與平臺中獨自享用的項目,這裏不列出了。

回頁首

附錄B:動態連接庫program header 類型的說明

名稱 說明
PT_NULL 0 這是program header 數組的分界標誌符。
PT_LOAD 1 這個標誌說明它所指的文件內容要被加載到內存單元,加載的內容由p_offset(在ELF文件中的偏移量)p_filesz(被加載的內容在文件中的大小)。而加載的要求是p_vaddr(被建議的加載的開始地址)p_memsz(被加載的建議內存大小)
PT_DYNAMIC 2 表示它所對應的dynamic section 內容,也就是在 附錄A中全部的Elf32_Dyn數據結構所在的program heaer
PT_INTERP 3 這裏所指的是一個字符串,它指的是爲加載可執行文件而用的動態連接庫名稱,在linux下,這是/lib/ld-linux.so.2
PT_NOTE 4 爲軟件開發商加入標識而用的,代表軟件的開發說明。
PT_SHLIB 5 這是爲往後的擴充面預留。
PT_PHDR 6 表示program header array自身在內存中的映射地址與大小。

參考資料

相關文章
相關標籤/搜索