Intel平臺下Linux中 ELF文件動態鏈接的加載、解析及實例分析(一): 加載
發表于:2007-07-04來源:作者:點擊數:
標簽:
轉載自:IBM developerWorks 中國網站 王瑞川 ( jeppeterone@163.com ) 2003 年 10 月 動態鏈接,一個經常被人提起的話題。但在這方面很少有文章來闡明這個重要的軟件運行機制,只有一些關于動態鏈接庫編程的文章。本系列文章就是要從動態鏈接庫源代碼的層次
轉載自:IBM developerWorks 中國網站
王瑞川 (jeppeterone@163.com)
2003 年 10 月
動態鏈接,一個經常被人提起的話題。但在這方面很少有文章來闡明這個重要的軟件運行機制,只有一些關于動態鏈接庫編程的文章。本系列文章就是要從動態鏈接庫源代碼的層次來探討這個問題。
當然從文章的題目就可以看出,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 實例,它不僅可以讓我們對前面所說的動態鏈接原理有一個更感性的認識,而且就這個實例而言,還可以在以后的代碼開發過程中來作為一種動態打補丁的工具,甚至有可能,我會在以后的文章中會用這個工具來介紹新的技術。
一、歷史問題
關于動態鏈接,可以說由來已久。如果追溯,最早的思想就在五十年代就有了,那時就想把一些公用的代碼放在內存中的一個地方上,在別的地址用call便是了。到后來又發展到了 loading overlays(就是把在程序運行生命期不同的代碼在不同的時間段被加入內存),這是在六十年代的事。但這只能算是"濫觴"時期。接近于我們現在所說的動態鏈接是在unix操作系統之后,因為從unix的設計結構而言,本身就是分成模塊來實現一個復雜的功能的操作系統。但這些還不是現代意義上的動態鏈接,原因是現代意義上的動態鏈接要符合兩個特點:
1、動態的加載,就是當這個運行的模塊在需要的時候才被映射入運行模塊的虛擬內存空間中,如一個模塊在運行中要用到mylib.so中的myget函數,而在沒有調用mylib.so這個模塊中的其它函數之前,是不會把這個模塊加載到你的程序中(也就是內存映射),這些內容在內核中實現,用的是頁面異常機制(我可能在另一篇文章中提到這個問題)。
2、動態的解析,就是當要調用的函數被調用的時候,才會去把這個函數在虛擬內存空間的起始地址解析出來,再寫到專門在調用模塊中的儲存地址內,如前面所說的你已經調用了myget,所以mylib.so模塊肯定已經被映射到了程序虛擬內存之中,而如果你再調用mylib.so中的myput函數,那它的函數地址就在調用的時候才會被解析出來。
(注:這里用的程序就是一般所說的進程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文件:
javascript:window.open(this.src);" style="CURSOR: pointer" onload="return imgzoom(this,550)">
舉個例子,如果要從一個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文件格式設計者當初的苦心了(現在這個真的有這么一個節了)。
二、代碼舉例
講了這么多,還沒有真正講到在intel 32平臺下linux動態鏈接庫的加載與調用。在一般的情況下,我們所編寫的程序是由編譯器與ld.so這個動態鏈接庫來完成的。而如果要顯式的調用某一個動態鏈接庫中的程序,則下面是一個例子。
#include #include
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, worldn"); 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 就是加載它的標志的)。
三、_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 但實際運行與下面的語句是等價的:
真是達到程序簡潔的特點。
但有一個特別要指出的是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, '', 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)到現在我們已經明白了以下幾點:
1、 動態鏈接庫的struct link_map* 的產生與組織過程(這個在_dl_new_object中實現)
2、 動態鏈接庫是如何被提取信息入struct link_map*中的,并被加載的(這個在open_verify 與dl_map_object_from_fd,elf_get_dynamic_info這三個函數中實現)
3、 動態鏈接庫本身的初始化過程(這個在_dl_init中實現)
總體上函數調用結構在下圖中一個示意圖。
但還有幾個問題沒有被提到
1、 可執行文件中的函數被如何定位到動態鏈接庫的函數體中的。
2、 一個動態鏈接庫與依賴的動態鏈接庫之間是什么關系,它們之間是如何聯系。
3、 一個函數是怎樣被動態解析,它又是使函數調用方與實現方成為一體的。
這些問題我會在《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(被加載的建議內存大?。?/td> |
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自身在內存中的映射地址與大小。 |
參考資料
[1] John Levine "Linkers and Loaders" (是對動態鏈接的一般性理論作了一個概觀介紹)可以在以下的網址上看到它的網絡版 http://www.iecc.com/linker/
[2] Executable and Linkable Format (ELF) (這專門介紹ELF文件格式的ABI的好文章,網絡版在 www.skyfree.org/linux/references/ELF_Format.pdf 可以得到)
[3]glibc2-3-2版本 本文的源代碼來源??梢栽?ftp://ftp.gnu.org 中下載而得。
原文轉自:http://www.anti-gravitydesign.com