Intel平臺下linux中 ELF文件動態鏈接的加載、解析及實例分析(二): 函數解析與卸載
發表于:2007-07-04來源:作者:點擊數:
標簽:
轉載自:IBM developerWorks 中國網站 王瑞川 ( jeppeterone@163.com ) 從事 Linux 開發 工作 2003 年 12 月 相信讀者已經看過了 Intel平臺下Linux中 ELF文件動態鏈接的加載、解析及實例分析(一): 加載 的內容了,了解了ELF文件被加載的時候所經歷的一般過
轉載自:IBM developerWorks 中國網站
王瑞川(jeppeterone@163.com)
從事 Linux 開發工作
2003 年 12 月
相信讀者已經看過了Intel平臺下Linux中 ELF文件動態鏈接的加載、解析及實例分析(一): 加載的內容了,了解了ELF文件被加載的時候所經歷的一般過程。那我們現在就來解決在上一篇文章的最后所提到的那幾個問題,以及那些在dl_open_worker中沒有講解的代碼。
一、_dl_map_object_deps 函數分析
由于源代碼過分的冗長,并且由于效率的考慮,使原本很簡單的代碼變成了一件 TRAMPOLINE 的事情,所以我對它進行了大幅度的改變,不僅刪除了所有不必要的代碼,而且還用偽代碼來展現它最初的設計思想。
13 _dl_map_object_deps (struct link_map *lmap) 14 15 { 16 17 struct list_head* add_list; 18 char* load_dl_name; 19 struct link_map* curlmap; 20 Elf32_Dyn* needed_dyn; 21 struct link_map* new_lmap; 22 int lmap_count=1; 23 24 add_lmap_to_list(lmap,add_list); 25 26 for_each_in_list(add_list,curlmap) 27 { 28 for_every_DT_NEEDED_section(curlmap,needed_dyn) 29 { 30 load_dl_name= get_needed_name(curlmap,needed_dyn); 31 32 new_lmap=_dl_map_object(load_dl_name); 33 34 35 add_to_list_tail_uniq(add_list,new_lmap); 36 } 37 } 38 39 lmap_count=count_the_list(lmap); 40 41 lmap->l_initfini=(struct link_map**)malloc ((2*lmap_count+1)*(struct link_map*)); 42 43 lmap->l_searchlist.r_list=&lmap->l_initfini[lmap_count+1]; 44 lmap->l_searchlist.r_nlist=lmap_count; 45 46 47 copy_each_add_list_to_searchlist(lmap,add_list,lmap_count); 48 49 free_the_add_list(add_list); 50 51 }
|
先說明,其實加載一個動態鏈接庫的依賴動態鏈接庫不是一件簡單的事,因為所有的動態鏈接庫可能還有它自己所依賴的動態鏈接庫,如果采用遞歸簡單方法實現不僅是不可能的-----因為你可以參看第一篇的文章,那里提到了一個在加載動態鏈接庫中的加鎖問題,而且也是沒有必要的,你并不能保證這樣的動態鏈接庫依賴關系會不會形成一個依賴循環,就像下面的一張圖所顯示的那樣:
javascript:window.open(this.src);" style="CURSOR: pointer" onload="return imgzoom(this,550)">
這樣最簡單的想法就是我們不重復的加載所有的動態鏈接庫,這里就用一個單鏈實現-----在原來的程序中也是用這個方法,但那里用來分配的方法是在棧中直接實現,這樣可以加快程序的運行,但程序可讀性大大減弱了。
23 行就首先就把 lmap 自己加入這個 struct list 中去,在 26 行的 for_each_in_list(add_list,curlmap) 其實是就是把 curlmap=curlmap->next,并判斷它的 curlmap!=NULL,
28 行的 for_every_DT_NEEDED_section(curlmap,needed_dyn)
主要就是 needed_dyn=curlmap->l_info[DT_NEEDED]; 但這里要注意的是,在一個動態鏈接庫中可能有不只一個,就像在 readelf -a 的例子
Tag Type Name/Value 0x00000001 (NEEDED) Shared library: [libstdc++-libc6.2-2.so.3] 0x00000001 (NEEDED) Shared library: [libm.so.6] 0x00000001 (NEEDED) Shared library: [libc.so.6]
|
更確切的是要在 lmap-> l_ld 的 dynamic section 中查找它的 d_tag 為 DT_NEEDED 中
30 行的 get_needed_name 用的方法是這樣的
load_dl_name=curlmap->l_addr+need_dyn->d_un.d_ptr+curlmap->l_i nfo[DT_STRTAB];
|
很明顯這里就會把這個動態鏈接庫映射來完成它的加載,而 35 行是要把 add_list 擴充,這里只會對同一個動態鏈接庫加載一次,所以不會有前面的循環加載,再回過頭來看 26 行到 37 行之間的那個循環,如果在 35 行中加入了那個沒有重復的動態鏈接庫。那整個循環就可能繼續循環下去。
從 39 行到 51 行之中就把這個函數中已經得到的依賴動態鏈接庫 copy 入 l_searchlist 與 l_initfini 這兩個的重要數組中, 巧妙的是它們采用了一起分配的。最后前面的那個臨時單鏈表。
二、相對轉移,絕對轉移
在學習匯編語言的時候,我們對不同的尋址方式肯定有很深的印象。但對于在匯編語言中同樣重要的轉移指令,只是一筆帶過(用到了call 與 jxx ----------- 這里的 jxx 是指如 jmp jae jbe 這樣的有條件轉移指令和無條件轉移指令)。然而,如果講到動態鏈接庫的鏈接實現則一定要提到這一內容。
所謂相對轉移,就是這個二進制代碼的中的它是可以在重定位的環境中不經修改,就可以運行的。如下面的情況,
719: e9 e2 fe ff ff jmp 600
|
變成一般的地址是這樣的
movl %eip,%eax addl xfffffee2,%eax movl %eax,%eip
|
這里旁邊的 719 就是這個 ELF 文件與起始地址相比的偏移量,而在里面的 e9 e2 fe ff ff 如果寫成看的往后退 0x11e 因為這是 ff ff fe e2(intel 是 little endian 表示方法)所表示的 -0x11e 的數。如果把 719 加上 5 再減去 600 就是這個數了。這便是處理器的相對轉移。
還有另一種轉移方式,就是絕對轉移。
這個如果用最簡單的代碼來表示是
addl ,%eip pushl %eip movl (%eax),%eip
|
很明顯,就是把 eip 的內容變成了eax 中的內容,如果用 jmp 也是一樣的
上面的兩種轉移方式適應于不同的環境要求,如果是在一個ELF文件中的,采用相對轉移可帶來的好處有以下的幾點:
1、可以不用再訪問一次內存,在指令的執行時間上得到了大大的提高(這在PCI的總線結構中現在主流的最高主頻是133MHZ,而隨便一個INTEL CPU的主頻都能超過它)。
2、可以適應在動態加載與動態定位的內存環境,而不用再對原來的代碼修改便能實現(代碼段也不能在運行的時候修改),因為整個動態鏈接庫或可執行文件都是以連續的地址映射的。
但同樣帶來了幾個問題:
1、這樣的相對轉移沒有辦法在運行的時候準確的轉移到別的動態鏈接庫中的函數地址(因為雖然大部分的動態鏈接庫的加載地址是可以預計的,但從理論上來說是隨機的)。
2、這樣的代碼在平臺之間的移植性帶來很大的問題,因為不同的機器沒有辦法知道這樣的數字是代表一個地址,還是代表了一個二進制數。所以在對平臺移植有高要求的體系中用的是c++的虛函數指針------相對地址轉移的發展。如COM,corba體系中就是這樣的。
上面的這兩項缺點正好是絕對轉移的優勢。作一個對比,絕對轉移就相當于內存尋址時的立即尋址,而相對轉移相當于內存尋址的相對尋址。
在一般的動態鏈接庫中實際運用更是用了一個聰明的辦法。請看下一段的匯編語言片段:
2f7: e8 00 00 00 00 call 2fc 2fc: 5b pop %ebx 2fd: 81 c3 b0 10 00 00 add x10b0,%ebx
|
這里的2f7中的call 2fc 是什么意思呢,從我們上面的方法來看,這里是什么呢?就是把函數運行到了2fc處,根據是我上面所說的,因為是一個相對轉移。e8 00 00 00 00。如果用一般的觀點看這沒有什么用處。但妙處就在這里,2fc處的pop %ebx,是把什么送到%ebx中呢,如果每一次call 都會把下一條要執行的指令的地址壓入棧中,那%ebx中在這里的內容就是2d4這一條指令在內存中的地址了,回想動態鏈接庫的絕對地址是沒有辦法在編譯時得到,但這樣卻可以--------很巧妙,不對嗎?
那后面的add x10b0,%ebx又是什么用處?如果我們這里假定在內存中的地址是2fc,那加上10b0之后的值是0x13ac了,看在這里是什么呢?
Disassembly of section .got:
000013ac <.got>: 13ac: 34 13 xor x13,%al ...
|
這是一個got節, 它的全稱是global object table 就是全局對象表。它這里存儲著要轉移的地址。如果在動態鏈接庫中,或是要調用一個在它之外的函數是怎樣實現呢?我們往下看:
306: 8d 83 74 ef ff ff lea 0xffffef74(%ebx),%eax 30c: 50 push %eax 30d: e8 ce ff ff ff call 2e0
|
這里就要調用一個call 2e0 所在的函數。那在0x2e0處又是什么呢?
2e0: ff a3 0c 00 00 00 jmp *0xc(%ebx) 2e6: 68 00 00 00 00 push x0 2eb: e9 e0 ff ff ff jmp 2d0
|
很明顯,我們前面已經說了%ebx中所保存的就是.got節的起始地址,而這里就是轉移到在.got起始地址偏移0xc處所存儲的地址量。而0x2e0所在的地址是在.plt(procedure linkage table)的節中。正是plt got的互相配合,才達到了動態鏈接的效果。下面的_dl_relocate_object函數就是在把動態鏈接庫加載之后將got中的內容初始化的作用,作好了以后函數解析的準備。
三、_dl_relocate_object函數分析
舉個例子。同樣來自上面的動態鏈接庫文件中內容。如果我們在這里面調用了printf這個普通的函數,它的rel在文件中的位置是
Relocation section '.rel.plt' at offset 0x2c8 contains 1 entries: Offset Info Type Symbol's Value Symbol's Name 000013b8 00000e07 R_386_JUMP_SLOT 00000000 printf
|
這個值如果在文件中找到0x13b8(這是相對偏移量)的內容就是
由于intel 是little endian 所以這個數翻譯過來是0x02e6,那這里是什么呢?
2e0: ff a3 0c 00 00 00 jmp *0xc(%ebx) 2e6: 68 00 00 00 00 push x0 2eb: e9 e0 ff ff ff jmp 2d0
|
這下就會全部明白了吧。它就是壓入0x0(這其實就是我們前面的printf在rel節中的索引數0------它是第一項)。而下面跳到的就是2d0(這是一個相對轉移)處
2d0: ff b3 04 00 00 00 pushl 0x4(%ebx) 2d6: ff a3 08 00 00 00 jmp *0x8(%ebx)
|
前面已經說過%ebx得到的是got的起始地址,所以這就是壓got[1]入棧,再轉移到got[2]中所包含的地址去,你可以看前面在elf_machine_runtime_setup中的2162行與2167行,它就是這個動態鏈接庫自身的struct link_map*的指針,與_dl_runtime_resolve所在的地址。下面一張圖就可以形象的說明這一點。

如果是第一次的函數調用,它所走的路線就是我在上圖中用紅線標出的,而要是在第二次以后調用,那就是藍線所標明的。原因在前面的代碼中已經給出了。
82 int _dl_relocate_object (struct link_map* lmap,int lazy_mode) 83 { 84 elf_machine_runtime_setup(lmap,lazy_mode); 85 elf_machine_lazy_rel (lmap,lazy_mode); 86 87 }
|
這里要分兩步來完成,第一步的elf_machine_runtime_setup是把這個動態鏈接庫所代表的數據結構lmap的地址寫入一個在ELF文件中特別地方,而elf_machine_lazy_rel是對所有的要被調用的動態鏈接庫外部的函數重定位的實現。這兩步非常重要,因為如果沒有這兩步,那要實現動態鏈接庫的函數動態解析是不可能的,這個你可以在上面的 相對轉移,絕對轉移 中的論述得到詳細的了解。
54 void elf_machine_runtime_setup(struct link_map* lmap,int lazy_mode) 55 { 56 Elf32_Addr *got; 57 58 got = (Elf32_Addr *) lmap->l_info[DT_PLTGOT].d_un.d_ptr; 59 60 got[2]=&_dl_runtime_resolve 61 got[1]=lmap; 62 }
|
明顯的,那個被寫入的ELF文件中的地址就是它的DT_PLTGOT節中的第二個項目-----第60行的內容。而寫入第一項的內容就是要調動的處理函數的地址,這一點在后面所提到的動態解析中的入口地址。
64 void elf_machine_lazy_rel (struct link_map* lmap,int lazy_mode) 65 { 66 Elf32_Addr rel_addr=lmap->l_info[DT_REL].d_un.d_ptr; 67 int rel_num=lmap->l_info[DT_RELSZ].d_un.d_ptr; 68 int i; 69 Elf32_Addr l_addr=lmap->l_addr; 70 71 Elf32_Rel* rel; 72 for (i=0,rel=(Elf32_Rel*)rel_addr;i 73 { 74 Elf32_Addr *const reloc_addr = (void *) (l_addr + rel->r_offset); 75 *reloc_addr +=l_addr; 76 } 77 78 }
|
這里的elf_machine_lazy_rel我只列出了在intel平臺下的那種情況,其它的還要特別的內容,在這里很明顯,我們只是寫把原來的在ELF文件的內容加上一個文件加載的地址,這就是lazy mode,因為動態鏈接庫的函數很可能在整個程序運行中不會被調用--------這一點與虛擬內存管理的原理是一樣的。
四、動態鏈接庫函數的解析
前面的60行的代碼----設定了動態解析的入口地址與給出的在動態鏈接庫中的在達到調用一個外部函數時所有的函數路線,已經到了 _dl_runtime_resolve處
2087 # define ELF_MACHINE_RUNTIME_TRAMPOLINE asm (" 2088 .text
2089 .globl _dl_runtime_resolve
2090 .type _dl_runtime_resolve, @function
2091 .align 16
2092 _dl_runtime_resolve:
2093 pushl %eax
2094 pushl %ecx
2095 pushl %edx
2096 movl 16(%esp), %edx
2097 movl 12(%esp), %eax
2098 call fixup
2099 popl %edx
2100 popl %ecx
2101 xchgl %eax, (%esp)
2102 ret 2103 .size _dl_runtime_resolve, .-_dl_runtime_resolve
2104
2105 ");
|
從這里定義的名稱ELF_MACHINE_RUNTIME_TRAMPOLINE,我們就可以看出這個函數不簡單(TRAMPOLINE在英語中是蹦床的意思,就是要make your brain curving的那種怪怪的東西),后面的代碼也確實說明了這一點。
在前面的.text是下面的代碼是可執行,.globl _dl_runtime_resolve是表明這個函數是全局性的,如果沒有這一項,那我們前面看的got[2]=& _dl_runtime_resolve就不能編譯通過-----編譯器可能找不到它的定義。.type _dl_runtime_resolve, @function是函數說明。 .align 16處便是16字節對齊。
我們知道在前面的調用函數過程中已經壓入了兩個參數(第一個是動態鏈接庫的struct link_map* 指針,另一個是rel的索引值)這里先保存以前的寄存器值,而到這個時候16(%esp)就是第二個參數,12(%esp)第一個參數,這里作的原因是下面的fixup的函數以寄存器傳遞參數。
我先不管fixup具體內容是什么,單就看它結束的內容就很能說明代碼作者的優秀。先pop兩個寄存器的值,而又xchg %eax,(%esp)與棧頂的內容,這有兩個目的,一是恢復了eax的值,另一個作用是棧頂是函數返回的地址,而fixup返回的eax就是我們想找的函數有內存中的地址。這就自然跳到那個地方去了。但如果你認為這就好了,那也錯了,因為你不要忘記我們之前還壓入了兩個參數在棧中。所以用了ret ,這在intel的指令中表示
的組合。(很精彩?。。。。。。。?/p>
你還可以參看《程序的鏈接和裝入及Linux下動態鏈接的實現》 網址為 http://www-900.ibm.com/developerWorks/cn/linux/l-dynlink/index.shtml 里面的有一幅圖正好說明此的ELF_MACHINE_RUNTIME_TRAMPOLINE。
那直接看fixup函數的內容
124 Elf32_Addr fixup(struct link_map* lmap,Elf32_Word reloc_offset) 125 { 126 Elf32_Sym* symtab=lmap->l_info[DT_SYMTAB].d_un.d_ptr; 127 char* strtab=lmap->l_info[DT_STRTAB].d_un.d_ptr; 128 Elf32_Rel* reloc = (Elf32_Rel*) (lmap->l_info[DT_JMPREL].d_un.d_ptr+reloc_offset); 129 Elf32_Sym* sym=&symtab[Elf32_R_SYM(reloc->r_info)]; 130 char* symname=sym->st_name+strtab; 131 Elf32_Addr reloc_addr=lmap->l_addr+reloc->r_offset; 132 133 Elf32_Addr symaddr=0; 134 135 136 137 symaddr=do_lookup(lmap,symname); 138 139 140 141 if (symaddr>0) 142 { 143 *reloc_addr=symaddr; 144 return symaddr; 145 } 146 147 exit(-2); 148 149 150 151 152 153 }
|
這里是給出了從一個動態鏈接庫中可重定向的reloc_offset得到要解析函數的名稱,如果用圖示的方式表示就如下圖:

你可能會想:其實還可以用另一種方法,就是把這個reloc sym的st_value直接寫入前面的這個調用重定向函數相對應的got中。這樣解析時的速度會更快。但現實這樣卻可能對整個ELF文件結構體系帶來很大的麻煩。我將對每一點說明:
- 如果是這個reloc sym的地址,那對于一個動態鏈接庫而言,它的加載地址本身就是動態確定的。
- 如果用的是那個Elf32_Sym的st_value地址,那倒是可以與lmap->l_i nfo[DT_STRTAB]一起得到這個sym的name,但如果考慮到在編譯的時候有些函數是只對本模塊有效,可見的,如在一個文件中定義為 static的函數,則它就是局部可見的,那個時候就不可能是解析為這個函數,而且對c++函數還有更為復雜的情況,這樣就會要求一個字段來表示它的屬性,這就是要有了st_info這個數據成員變量。這也就要有了sym的參與了。
- 光有Elf32_Sym還是不行,因為就重定位而言它本身還有一點信息,就是這一個relocation symbol是在本地解析,還是在另外一個真正意義上的動態鏈接庫內被解析,這一情況主要是發生在幾個文件編寫的模塊中,它們編寫的一些函數就在鏈接的時候被確定了,而另一些則沒有,區分的就是relocation 中的r_info了。
從上面的分析來看,一種規范的設計有許多的考慮因素,如果只單一的考慮,那是不行的,特別是要對多個操作系統與平臺統一的規范,不能因為就是考慮效率一條就可以了。
在143行是對前面要重定位的函數實現真正的解析函數到位,這樣在這個函數被再次調用的時候就不用再來一次了,本來這時就對這個 relocation symbol r_info的判斷,現在都已經略去了。
真正的解析在do_lookup中實現了,我這里還是它的實現偽代碼:
90 Elf32_Addr do_lookup(struct link_map* lmap,char* symname) 91 { 92 struct link_map* search_lmap=NULL; 93 Elf32_Sym* symtab; 94 Elf32_Sym* sym; 95 char* strtab; 96 char* find_name; 97 int symindx; 98 99 Elf32_Word hash=elf_hash_name(symname); 100 for_each_search_lmap_in_search_list(lmap,search_lmap) 101 { 102 symtab=search_lmap->l_info[DT_SYMTAB].d_un.d_ptr; 103 strtab=search_lmap->l_info[DT_STRTAB].d_un.d_ptr; 104 for (symindx=search_lmap->l_buckets[hash % search_lmap->l_nbuckets]; 105 symindx!=0;symindx=search_lmap->l_chain[symindx]) 106 { 107 sym=&symtab[symindx]; 108 109 find_name=strtab+sym->st_name; 110 if (strcmp(find_name,symname)==0) 111 return sym->st_value+search_lmap->l_addr; 112 } 113 114 return 0; 115 116 117 118 } 119 }
|
100行for_each_search_lmap_in_search_list就是從前面在 _dl_map_object_deps中得到的l_searchlist中取下的它本身的依賴動態鏈接庫,中間查找的方法就如下面那張圖中所顯示的。

上面所表示的就是一個在hash表中symidx偏移處所存的就是下一個偏移所在。最后如果strcmp==0就可以得到了,否則就會返回一個0表示失敗了。
現在我們已經把函數的解析過程分析完畢,有必要作一個小結工作:
- 在調用函數的動態鏈接庫中,它所用的方法是從plt節的代碼執行絕對轉移,而轉移的地址存放在got節中。
- 在被調用函數的動態鏈接庫中(就是函數實現的動態鏈接庫),它的函數在以DT_HASH與DT_SYMTAB, DT_STRTAB組織起來。組織的方式如下面的一張圖,以symtab中的Elf32_Sym中的st_value表示這個可導出的標記在動態鏈接庫中的偏移量,st_name則是在動態鏈接庫strtab中的偏移量。
- 在調用動態鏈接庫與被調用動態鏈接庫的聯系能過的是Elf32_Rel(對MIPS等的體系結構中是Elf32_Rela),它的r_info體現了這個要導入標記(就是調用方中)的性質,而r_offset則是這個標記在動態鏈接庫中的偏移量。(這個可以看 elf_machine_lazy_rel中的實現)

五、動態鏈接庫的卸載
實際上卸載與加載只是反過程而已,但原來的代碼為了提高效率實現在棧內分配內存,不過這樣倒使原來簡單易懂的變的過于復雜,所以,我這里作了很大的修改,這里是偽代碼的實現。
245 void dl_close(struct link_map* lmap) 246 { 247 struct link_map** dep_lmaplist=NULL; 248 int i; 249 Elf32_Addr* fini_call_array; 250 void* fini_call; 251 struct link_map* curlmap; 252 struct list * has_removed_list=malloc(sizeof(struct list)); 253 254 has_removed_list->lmap=lmap; 255 has_removed_list->next=NULL; 256 257 if (lmap->l_opencount>1) 258 { 259 lmap->l_opencount--; 260 return; 261 } 262 263 lmap->l_opencount--; 264 265 266 dep_lmaplist=lmap->l_initfini; 267 268 269 270 for (i=0;dep_lmaplist[i]!=NULL;i++) 271 { 272 273 try_dl_close(dep_lmaplist[i],has_removed_list); 274 } 275 276 277 if (lmap->l_info[DT_FINI_ARRAY].d_un.d_ptr!=NULL && lmap->l_opencount ==0) 278 { 279 280 fini_call_array=lmap->l_info[DT_FINI_ARRAY].d_un.d_ptr 281 +lmap->l_addr; 282 unsigned int sz=lmap->l_info[DT_FINI_ARRAYSZ].d_un.d_ptr 283 +lmap->l_addr; 284 285 while(sz-->0) 286 { 287 /*call the fini function*/ 288 ((void*)fini_call_array[sz])(); 289 290 } 291 } 292 293 if (lmap->l_info[DT_FINI].d_un.d_ptr!=NULL && lmap->l_opencount ==0) 294 { 295 fini_call=lmap->l_info[DT_FINI].d_un.d_ptr 296 +lmap->l_addr; 297 298 ((void*)fini_call)(); 299 } 300 301 302 munmap(lmap->l_map_start,lmap->l_map_end-lmap->l_map_start); 303 304 305 free (lmap->l_initfini); 306 307 free (lmap->l_scope); 308 309 if (lmap->l_phdr_allocated) 310 free ((void *) lmap->l_phdr); 311 312 free_list(has_removed_list); 313 314 free (lmap); 315 316 317 return; 318 }
|
這里的has_removed_list就是記錄整個在這一次dl_close操作中已經被卸載了的動態鏈接庫,主要是為了防止再次卸載已經卸載的動態鏈接庫。其實先開始判斷這是否是已經沒有再依賴它本向的動態鏈接庫了。如果沒有了(減去1,等于0就是了),那才可以繼續去了,接下來不要先把它自己加入這個動態鏈接庫,試著去卸載它所依賴的動態鏈接庫,這些全做完之后就是它本身的各要點,一是它的DT_FINI_ARRAY中的卸載函數,還有就是DT_FINI中的函數,這之完了,便是加載到內存內容的去映射化,213行。再就是對struct link_map申請的內存就是了。
你可以看try_dl_close之后的代碼就能明白這種可能有的深度的遞歸過程。
233 void try_dl_close(struct link_map* lmap,struct list* 234 has_removed_lmap_list) 234 { 235 if(in_the_list(has_removed_lmap_list,lmap)) 236 return ; 237 dl_close_with_list(lmap,has_removed_lmap_list); 238 return ; 239 240 }
156 void dl_close_with_list(struct link_map* lmap,struct list* has_removed_lmap_list) 157 { 158 struct link_map** dep_lmaplist=NULL; 159 int i; 160 Elf32_Addr* fini_call_array; 161 void* fini_call; 162 163 164 165 166 167 if (lmap->l_opencount>1) 168 { 169 lmap->l_opencount--; 170 return; 171 } 172 add_to_list_tail_uniq(has_removed_lmap_list,lmap); 173 174 lmap->l_opencount--; 175 176 177 dep_lmaplist=lmap->l_initfini; 178 179 180 181 for (i=0;dep_lmaplist[i]!=NULL;i++) 182 { 183 184 try_dl_close(dep_lmaplist[i],has_removed_lmap_list); 185 } 186 187 188 if (lmap->l_info[DT_FINI_ARRAY].d_un.d_ptr!=NULL && lmap->l_opencount ==0) 189 { 190 191 fini_call_array=lmap->l_info[DT_FINI_ARRAY].d_un.d_ptr 192 +lmap->l_addr; 193 unsigned int sz=lmap->l_info[DT_FINI_ARRAYSZ].d_un.d_ptr 194 +lmap->l_addr; 195 196 while(sz-->0) 197 { 198 /*call the fini function*/ 199 ((void*)fini_call_array[sz])(); 200 201 } 202 } 203 204 if (lmap->l_info[DT_FINI].d_un.d_ptr!=NULL && lmap->l_opencount ==0) 205 { 206 fini_call=lmap->l_info[DT_FINI].d_un.d_ptr 207 +lmap->l_addr; 208 209 ((void*)fini_call)(); 210 } 211 212 213 munmap(lmap->l_map_start,lmap->l_map_end-lmap->l_map_start); 214 215 216 free (lmap->l_initfini); 217 218 free (lmap->l_scope); 219 220 if (lmap->l_phdr_allocated) 221 free ((void *) lmap->l_phdr); 222 223 free (lmap); 224 225 226 return; 227 228 }
|
綜合來看,dl_close這個函數如果是最終要卸載整個可執行文件的工作的話,那就要最高層的可執行文件開始,這里采用對可能有錯綜復雜的依賴關系的動態鏈接庫使用了一個mark_removed與dl_close相結合的方法,在不斷的遞歸調用中,把所有的動態鏈接庫 l_opencount減少到0。最后釋放所有的內存空間。這種情況如果你與linux內核中delet_module的調用相對比,也可以看的更清楚。
六、前景與展望
動態鏈接庫的實現發展到現今已經相當完善,它在理論與實踐方面對于我們學習操作系統和編譯語言提供了一個很好的范例。但是,動態鏈接庫的實現畢竟還是只能在一個操作系統,一個單機,一種編程語言(如果是c++編程語言,則這一點也滿足不了,因為不同的編譯器可能對function name mangling-----函數名稱混譯也不同),對于現在網絡化的信息產業是不夠的。所以,出現了以這個為目標的二進制實現規范,這就是OMG (object model group )所制定出來的 CORBA,和由 Microsoft 所制定出來的 COM,我可能以后的日子中詳細來探討這些最新發展。
參考資料
[1]glibc-2.3.2 sourcecode 這是我這里主要的代碼來源,可以在 ftp://ftp.gnu.org 中下載
[2]John R.Levine "Linkers and Loaders" 介紹動態鏈接庫技術的經典 http://linker.iecc.com/
[3] Hongjiu Lu "ELF: From The Programmer's Perspective" 好的ELF編程的參考。在 http://linux4u.jinr.ru/usoft/WWW/www_debian.org/Documentation/elf/elf.html 可以看到
原文轉自:http://www.anti-gravitydesign.com