天天看點

ELF檔案的加載和動态連結過程

近段時間在研究Erlang核心特性的實作,也許過段時間會有個系列的總結,期待...

今天看到有人寫一個深入Hello World的文章,想起來讀研的時候做的一個關于程式加載和連結的課程設計,也是以Hello World為例說明的,随發出來共享。文後有下載下傳連結。

======================================================

本文的目的:大家對于Hello World程式應該非常熟悉,随便使用哪一種語言,即使還不熟悉的語言,寫出一個Hello World程式應該毫不費力,但是如果讓大家詳細的說明這個程式加載和連結的過程,以及後續的符号動态解析過程,可能還會有點困難。本文就是以一個最基本的C語言版本Hello World程式為基礎,了解Linux下ELF檔案的格式,分析并驗證ELF檔案和加載和動态連結的具有實作。

C代碼 

ELF檔案的加載和動态連結過程
  1. #include <stdio.h>  
  2. int main()  
  3. {  
  4.     printf(“hello world!\n”);  
  5.     return 0;  
  6. }  
  7. $ gcc –o hello hello.c  

 本文的實驗平台:

<!--[if !supportLists]-->Ø  <!--[endif]-->Ubuntu 7.04

<!--[if !supportLists]-->Ø  <!--[endif]-->Linux kernel 2.6.20

<!--[if !supportLists]-->Ø  <!--[endif]-->gcc 4.1.2

<!--[if !supportLists]-->Ø  <!--[endif]-->glibc 2.5

<!--[if !supportLists]-->Ø  <!--[endif]-->gdb 6.6

<!--[if !supportLists]-->Ø  <!--[endif]-->objdump/readelf 2.17.50

本文的組織:

       第一部分大緻描述ELF檔案的格式;

       第二部分分析ELF檔案在核心空間的加載過程;

       第三部分分析ELF檔案在運作過程中符号的動态解析過程;

       (以上各部分都是以Hello World程式為例說明)

       第四部分簡要總結;

       第五部分闡明需要深入了解的東西。

 ELF檔案格式

概述

       Executable and Linking Format(ELF)檔案是x86 Linux系統下的一種常用目标檔案(object file)格式,有三種主要類型:

<!--[if !supportLists]-->1)        <!--[endif]-->适于連接配接的可重定位檔案(relocatable file),可與其它目标檔案一起建立可執行檔案和共享目标檔案。

<!--[if !supportLists]-->2)        <!--[endif]-->适于執行的可執行檔案(executable file),用于提供程式的程序映像,加載的記憶體執行。

<!--[if !supportLists]-->3)        <!--[endif]-->共享目标檔案(shared object file),連接配接器可将它與其它可重定位檔案和共享目标檔案連接配接成其它的目标檔案,動态連接配接器又可将它與可執行檔案和其它共享目标檔案結合起來建立一個程序映像。

       ELF檔案格式比較複雜,本文隻是簡要介紹它的結構,希望能給想了解ELF檔案結構的讀者以幫助。具體詳盡的資料請參閱專門的ELF文檔。

   檔案格式

       為了友善和高效,ELF檔案内容有兩個平行的視角:一個是程式連接配接角度,另一個是程式運作角度,如圖所示。

ELF檔案的加載和動态連結過程

<!--[endif]-->

       ELF header在檔案開始處描述了整個檔案的組織,Section提供了目标檔案的各項資訊(如指令、資料、符号表、重定位資訊等),Program header table指出怎樣建立程序映像,含有每個program header的入口,section header table包含每一個section的入口,給出名字、大小等資訊。

    資料表示

       ELF資料編碼順序與機器相關,資料類型有六種,見下表:

ELF檔案的加載和動态連結過程

      ELF檔案頭

       象bmp、exe等檔案一樣,ELF的檔案頭包含整個檔案的控制結構。它的定義如下:

C代碼 

ELF檔案的加載和動态連結過程
  1. 190 #define EI_NIDENT       16  
  2. 191   
  3. 192 typedef struct elf32_hdr{  
  4. 193   unsigned char e_ident[EI_NIDENT];   
  5. 194   Elf32_Half    e_type;       
  6. 195   Elf32_Half    e_machine;    
  7. 196   Elf32_Word e_version;  
  8. 197   Elf32_Addr    e_entry;      
  9. 198   Elf32_Off e_phoff;          
  10. 199   Elf32_Off e_shoff;          
  11. 200   Elf32_Word    e_flags;  
  12. 201   Elf32_Half    e_ehsize;         
  13. 202   Elf32_Half    e_phentsize;      
  14. 203   Elf32_Half    e_phnum;          
  15. 204   Elf32_Half    e_shentsize;      
  16. 205   Elf32_Half    e_shnum;          
  17. 206   Elf32_Half    e_shstrndx;   
  18. 207 } Elf32_Ehdr;  

 其中E_ident的16個位元組标明是個ELF檔案(7F+'E'+'L'+'F')。e_type表示檔案類型,2表示可執行檔案。e_machine說明機器類别,3表示386機器,8表示MIPS機器。e_entry給出程序開始的虛位址,即系統将控制轉移的位置。e_phoff指出program header table的檔案偏移,e_phentsize表示一個program header表中的入口的長度(位元組數表示),e_phnum給出program header表中的入口數目。類似的,e_shoff,e_shentsize,e_shnum 分别表示section header表的檔案偏移,表中每個入口的的位元組數和入口數目。e_flags給出與處理器相關的标志,e_ehsize給出ELF檔案頭的長度(位元組數表示)。e_shstrndx表示section名表的位置,指出在section header表中的索引。

Section Header

       目标檔案的section header table可以定位所有的section,它是一個Elf32_Shdr結構的數組,Section頭表的索引是這個數組的下标。有些索引号是保留的,目标檔案不能使用這些特殊的索引。

       Section包含目标檔案除了ELF檔案頭、程式頭表、section頭表的所有資訊,而且目标檔案section滿足幾個條件:

<!--[if !supportLists]-->1)        <!--[endif]-->目标檔案中的每個section都隻有一個section頭項描述,可以存在不訓示任何section的section頭項。

<!--[if !supportLists]-->2)        <!--[endif]-->每個section在檔案中占據一塊連續的空間。

<!--[if !supportLists]-->3)        <!--[endif]-->Section之間不可重疊。

<!--[if !supportLists]-->4)        <!--[endif]-->目标檔案可以有非活動空間,各種headers和sections沒有覆寫目标檔案的每一個位元組,這些非活動空間是沒有定義的。

       Section header結構定義如下:

C代碼 

ELF檔案的加載和動态連結過程
  1. 288 typedef struct {  
  2. 289   Elf32_Word    sh_name;      
  3. 290   Elf32_Word    sh_type;      
  4. 291   Elf32_Word    sh_flags;  
  5. 292   Elf32_Addr     sh_addr;         
  6. 293   Elf32_Off      sh_offset;  
  7. 294   Elf32_Word    sh_size;          
  8. 295   Elf32_Word    sh_link;  
  9. 296   Elf32_Word    sh_info;  
  10. 297   Elf32_Word    sh_addralign;  
  11. 298   Elf32_Word    sh_entsize;       
  12. 299 } Elf32_Shdr;  

 其中sh_name指出section的名字,它的值是後面将會講到的section header string table中的索引,指出一個以null結尾的字元串。sh_type是類别,sh_flags訓示該section在程序執行時的特性。sh_addr指出若此section在程序的記憶體映像中出現,則給出開始的虛位址。sh_offset給出此section在檔案中的偏移。其它字段的意義不太常用,在此不細述。

       檔案的section含有程式和控制資訊,系統使用一些特定的section,并有其固定的類型和屬性(由sh_type和sh_info指出)。下面介紹幾個常用到的section:“.bss”段含有占據程式記憶體映像的未初始化資料,當程式開始運作時系統對這段資料初始為零,但這個section并不占檔案空間。“.data.”和“.data1”段包含占據記憶體映像的初始化資料。“.rodata”和“.rodata1”段含程式映像中的隻讀資料。“.shstrtab”段含有每個section的名字,由section入口結構中的sh_name索引。“.strtab”段含有表示符号表(symbol table)名字的字元串。“.symtab”段含有檔案的符号表,在後文專門介紹。“.text”段包含程式的可執行指令。

       當然一個實際的ELF檔案中,會包含很多的section,如.got,.plt等等,我們這裡就不一一細述了,需要時再詳細的說明。

Program Header

       目标檔案或者共享檔案的program header table描述了系統執行一個程式所需要的段或者其它資訊。目标檔案的一個段(segment)包含一個或者多個section。Program header隻對可執行檔案和共享目标檔案有意義,對于程式的連結沒有任何意義。結構定義如下:

C代碼 

ELF檔案的加載和動态連結過程
  1. 232 typedef struct elf32_phdr{  
  2. 233   Elf32_Word    p_type;   
  3. 234   Elf32_Off      p_offset;  
  4. 235   Elf32_Addr    p_vaddr;          
  5. 236   Elf32_Addr    p_paddr;          
  6. 237   Elf32_Word    p_filesz;         
  7. 238   Elf32_Word    p_memsz;          
  8. 239   Elf32_Word    p_flags;  
  9. 240   Elf32_Word    p_align;       
  10. 241 } Elf32_Phdr;  

 其中p_type描述段的類型;p_offset給出該段相對于檔案開關的偏移量;p_vaddr給出該段所在的虛拟位址;p_paddr給出該段的實體位址,在Linux x86核心中,這項并沒有被使用;p_filesz給出該段的大小,在位元組為單元,可能為0;p_memsz給出該段在記憶體中所占的大小,可能為0;p_filesze與p_memsz的值可能會不相等。

Symbol Table

       目标檔案的符号表包含定位或重定位程式符号定義和引用時所需要的資訊。符号表入口結構定義如下:

C代碼 

ELF檔案的加載和動态連結過程
  1. 171 typedef struct elf32_sym{  
  2. 172   Elf32_Word    st_name;  
  3. 173   Elf32_Addr    st_value;  
  4. 174   Elf32_Word    st_size;  
  5. 175   unsigned char     st_info;  
  6. 176   unsigned char st_other;  
  7. 177   Elf32_Half     st_shndx;  
  8. 178 } Elf32_Sym;  

 其中st_name包含指向符号表字元串表(strtab)中的索引,進而可以獲得符号名。st_value指出符号的值,可能是一個絕對值、位址等。st_size指出符号相關的記憶體大小,比如一個資料結構包含的位元組數等。st_info規定了符号的類型和綁定屬性,指出這個符号是一個資料名、函數名、section名還是源檔案名;并且指出該符号的綁定屬性是local、global還是weak。

Section和Segment的差別和聯系

       可執行檔案中,一個program header描述的内容稱為一個段(segment)。Segment包含一個或者多個section,我們以Hello World程式為例,看一下section與segment的映射關系:

ELF檔案的加載和動态連結過程
ELF檔案的加載和動态連結過程

 如上圖紅色區域所示,就是我們經常提到的文本段和資料段,由圖中綠色部分的映射關系可知,文本段并不僅僅包含.text節,資料段也不僅僅包含.data節,而是都包含了多個section。

ELF檔案的加載過程

加載和動态連結的簡要介紹

       從編譯/連結和運作的角度看,應用程式和庫程式的連接配接有兩種方式。一種是固定的、靜态的連接配接,就是把需要用到的庫函數的目标代碼(二進制)代碼從程式庫中抽取出來,連結進應用軟體的目标映像中;另一種是動态連結,是指庫函數的代碼并不進入應用軟體的目标映像,應用軟體在編譯/連結階段并不完成跟庫函數的連結,而是把函數庫的映像也交給使用者,到啟動應用軟體目标映像運作時才把程式庫的映像也裝入使用者空間(并加以定位),再完成應用軟體與庫函數的連接配接。

       這樣,就有了兩種不同的ELF格式映像。一種是靜态連結的,在裝入/啟動其運作時無需裝入函數庫映像、也無需進行動态連接配接。另一種是動态連接配接,需要在裝入/啟動其運作時同時裝入函數庫映像并進行動态連結。Linux核心既支援靜态連結的ELF映像,也支援動态連結的ELF映像,而且裝入/啟動ELF映像必需由核心完成,而動态連接配接的實作則既可以在核心中完成,也可在使用者空間完成。是以,GNU把對于動态連結ELF映像的支援作了分工:把ELF映像的裝入/啟動入在Linux核心中;而把動态連結的實作放在使用者空間(glibc),并為此提供一個稱為“解釋器”(ld-linux.so.2)的工具軟體,而解釋器的裝入/啟動也由核心負責,這在後面我們分析ELF檔案的加載時就可以看到。

       這部分主要說明ELF檔案在核心空間的加載過程,下一部分對使用者空間符号的動态解析過程進行說明。

Linux可執行檔案類型的注冊機制

       在說明ELF檔案的加載過程以前,我們先回答一個問題,就是:為什麼Linux可以運作ELF檔案?

       回答:核心對所支援的每種可執行的程式類型都有個struct linux_binfmt的資料結構,定義如下:

C代碼 

ELF檔案的加載和動态連結過程
  1. 53   
  2. 57 struct linux_binfmt {  
  3. 58         struct linux_binfmt * next;  
  4. 59         struct module *module;  
  5. 60         int (*load_binary)(struct linux_binprm *, struct  pt_regs * regs);  
  6. 61         int (*load_shlib)(struct file *)  
  7. 62         int (*core_dump)(long signr, struct pt_regs * regs, struct file * file);  
  8. 63         unsigned long min_coredump;       
  9. 64         int hasvdso;  
  10. 65 };  

 其中的load_binary函數指針指向的就是一個可執行程式的處理函數。而我們研究的ELF檔案格式的定義如下:

C代碼 

ELF檔案的加載和動态連結過程
  1. 74 static struct linux_binfmt elf_format = {  
  2. 75                 .module      = THIS_MODULE,  
  3. 76                 .load_binary = load_elf_binary,  
  4. 77                 .load_shlib      = load_elf_library,  
  5. 78                 .core_dump       = elf_core_dump,  
  6. 79                 .min_coredump    = ELF_EXEC_PAGESIZE,  
  7. 80                 .hasvdso     = 1  
  8. 81 };  

 要支援ELF檔案的運作,則必須向核心登記這個資料結構,加入到核心支援的可執行程式的隊列中。核心提供兩個函數來完成這個功能,一個注冊,一個登出,即:

C代碼 

ELF檔案的加載和動态連結過程
  1. 72 int register_binfmt(struct linux_binfmt * fmt)  
  2. 96 int unregister_binfmt(struct linux_binfmt * fmt)  

 當需要運作一個程式時,則掃描這個隊列,讓各個資料結構所提供的處理程式,ELF中即為load_elf_binary,逐一前來認領,如果某個格式的處理程式發現相符後,便執行該格式映像的裝入和啟動。

核心空間的加載過程

       核心中實際執行execv()或execve()系統調用的程式是do_execve(),這個函數先打開目标映像檔案,并從目标檔案的頭部(第一個位元組開始)讀入若幹(目前Linux核心中是128)位元組(實際上就是填充ELF檔案頭,下面的分析可以看到),然後調用另一個函數search_binary_handler(),在此函數裡面,它會搜尋我們上面提到的Linux支援的可執行檔案類型隊列,讓各種可執行程式的處理程式前來認領和處理。如果類型比對,則調用load_binary函數指針所指向的處理函數來處理目标映像檔案。在ELF檔案格式中,處理函數是load_elf_binary函數,下面主要就是分析load_elf_binary函數的執行過程(說明:因為核心中實際的加載需要涉及到很多東西,這裡隻關注跟ELF檔案的處理相關的代碼):

C代碼 

ELF檔案的加載和動态連結過程
  1. 550         struct {  
  2. 551                 struct elfhdr elf_ex;  
  3. 552                 struct elfhdr interp_elf_ex;  
  4. 553                 struct exec interp_ex;  
  5. 554         } *loc;  
  6. 556         loc = kmalloc(sizeof(*loc), GFP_KERNEL);  
  7. 562           
  8. 563         loc->elf_ex = *((struct elfhdr *)bprm->buf);  
  9.                ……  
  10. 566           
  11. 567         if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)  
  12. 568                 goto out;  
  13. 570         if (loc->elf_ex.e_type != ET_EXEC && loc->elf_ex.e_type != ET_DYN)  
  14. 571                 goto out;  

 在load_elf_binary之前,核心已經使用映像檔案的前128個位元組對bprm->buf進行了填充,563行就是使用這此資訊填充映像的檔案頭(具體資料結構定義見第一部分,ELF檔案頭節),然後567行就是比較檔案頭的前四個位元組,檢視是否是ELF檔案類型定義的“\177ELF”。除這4個字元以外,還要看映像的類型是否ET_EXEC和ET_DYN之一;前者表示可執行映像,後者表示共享庫。

C代碼 

ELF檔案的加載和動态連結過程
  1. 577           
  2. 580         if (loc->elf_ex.e_phnum < 1 ||  
  3. 581                 loc->elf_ex.e_phnum > 65536U / sizeof(struct elf_phdr))  
  4. 582                 goto out;  
  5. 583         size = loc->elf_ex.e_phnum * sizeof(struct elf_phdr);  
  6.                ……  
  7. 585         elf_phdata = kmalloc(size, GFP_KERNEL);  
  8.                ……  
  9. 589         retval = kernel_read(bprm->file, loc->elf_ex.e_phoff,  
  10. 590                              (char *)elf_phdata, size);  

 這塊就是通過kernel_read讀入整個program header table。從代碼中可以看到,一個可執行程式必須至少有一個段(segment),而所有段的大小之和不能超過64K。

C代碼 

ELF檔案的加載和動态連結過程
  1. 614 elf_ppnt = elf_phdata;  
  2.                 ……  
  3. 623 for (i = 0; i < loc->elf_ex.e_phnum; i++) {  
  4. 624     if (elf_ppnt->p_type == PT_INTERP) {  
  5.             ……  
  6. 635         elf_interpreter = kmalloc(elf_ppnt->p_filesz, GFP_KERNEL);  
  7.             ……  
  8. 640         retval = kernel_read(bprm->file, elf_ppnt->p_offset,  
  9. 641                          elf_interpreter,  
  10. 642                          elf_ppnt->p_filesz);  
  11.             ……  
  12. 682         interpreter = open_exec(elf_interpreter);  
  13.             ……  
  14. 695         retval = kernel_read(interpreter, 0, bprm->buf,  
  15. 696                          BINPRM_BUF_SIZE);  
  16.             ……  
  17. 703           
  18.             ……  
  19. 705         loc->interp_elf_ex = *((struct elfhdr *)bprm->buf);  
  20. 706             break;  
  21. 707     }  
  22. 708     elf_ppnt++;  
  23. 709 }  

 這個for循環的目的在于尋找和處理目标映像的“解釋器”段。“解釋器”段的類型為PT_INTERP,找到後就根據其位置的p_offset和大小p_filesz把整個“解釋器”段的内容讀入緩沖區(640~640)。事個“解釋器”段實際上隻是一個字元串,即解釋器的檔案名,如“/lib/ld-linux.so.2”。有了解釋器的檔案名以後,就通過open_exec()打開這個檔案,再通過kernel_read()讀入其開關128個位元組(695~696),即解釋器映像的頭部。我們以Hello World程式為例,看一下這段中具體的内容:

ELF檔案的加載和動态連結過程

其實從readelf程式的輸出中,我們就可以看到需要解釋器/lib/ld-linux.so.2,為了進一步的驗證,我們用hd指令以16進制格式檢視下類型為INTERP的段所在位置的内容,在上面的各個域可以看到,它位于偏移量為0x000114的位置,檔案内占19個位元組:

ELF檔案的加載和動态連結過程

從上面紅色部分可以看到,這個段中實際儲存的就是“/lib/ld-linux.so.2”這個字元串。

C代碼 

ELF檔案的加載和動态連結過程
  1. 814         for(i = 0, elf_ppnt = elf_phdata;  
  2. 815             i < loc->elf_ex.e_phnum; i++, elf_ppnt++) {  
  3.                        ……   
  4. 819                 if (elf_ppnt->p_type != PT_LOAD)  
  5. 820                         continue;  
  6.                        ……   
  7. 870                 error = elf_map(bprm->file, load_bias + vaddr, elf_ppnt,  
  8. 871                                 elf_prot, elf_flags);  
  9.                        ……  
  10. 920         }  

 這段代碼從目标映像的程式頭中搜尋類型為PT_LOAD的段(Segment)。在二進制映像中,隻有類型為PT_LOAD的段才是需要裝入的。當然在裝入之前,需要确定裝入的位址,隻要考慮的就是頁面對齊,還有該段的p_vaddr域的值(上面省略這部分内容)。确定了裝入位址後,就通過elf_map()建立使用者空間虛拟位址空間與目标映像檔案中某個連續區間之間的映射,其傳回值就是實際映射的起始位址。

C代碼 

ELF檔案的加載和動态連結過程
  1. 946     if (elf_interpreter) {  
  2.                 ……  
  3. 951         elf_entry = load_elf_interp(&loc->interp_elf_ex,  
  4. 952                                 interpreter,  
  5. 953                                     &interp_load_addr);  
  6.                                 ……  
  7. 965     } else {  
  8. 966         elf_entry = loc->elf_ex.e_entry;  
  9.                 ……  
  10. 972     }  

 這段程式的邏輯非常簡單:如果需要裝入解釋器,就通過load_elf_interp裝入其映像(951~953),并把将來進入使用者空間的入口位址設定成load_elf_interp()的傳回值,即解釋器映像的入口位址。而若不裝入解釋器,那麼這個入口位址就是目标映像本身的入口位址。

C代碼 

ELF檔案的加載和動态連結過程
  1. 991        create_elf_tables(bprm, &loc->elf_ex,  
  2. 992                           (interpreter_type == INTERPRETER_AOUT),  
  3. 993                           load_addr, interp_load_addr);  
  4.                ……  
  5. 1028       start_thread(regs, elf_entry, bprm->p);  

 在完成裝入,啟動使用者空間的映像運作之前,還需要為目标映像和解釋器準備好一些有關的資訊,這些資訊包括正常的argc、envc等等,還有一些“輔助向量(Auxiliary Vector)”。這些資訊需要複制到使用者空間,使它們在CPU進入解釋器或目标映像的程式入口時出現在使用者空間堆棧上。這裡的create_elf_tables()就起着這個作用。

       最後,start_thread()這個宏操作會将eip和esp改成新的位址,就使得CPU在傳回使用者空間時就進入新的程式入口。如果存在解釋器映像,那麼這就是解釋器映像的程式入口,否則就是目标映像的程式入口。那麼什麼情況下有解釋器映像存在,什麼情況下沒有呢?如果目标映像與各種庫的連結是靜态連結,因而無需依靠共享庫、即動态連結庫,那就不需要解釋器映像;否則就一定要有解釋器映像存在。

       以我們的Hello World為例,gcc在編譯時,除非顯示的使用static标簽,否則所有程式的連結都是動态連結的,也就是說需要解釋器。由此可見,我們的Hello World程式在被核心加載到記憶體,核心跳到使用者空間後并不是執行Hello World的,而是先把控制權交到使用者空間的解釋器,由解釋器加載運作使用者程式所需要的動态庫(Hello World需要libc),然後控制權才會轉移到使用者程式。

ELF檔案中符号的動态解析過程

       上面一節提到,控制權是先交到解釋器,由解釋器加載動态庫,然後控制權才會到使用者程式。因為時間原因,動态庫的具體加載過程,并沒有進行深入分析。大緻的過程就是将每一個依賴的動态庫都加載到記憶體,并形成一個連結清單,後面的符号解析過程主要就是在這個連結清單中搜尋符号的定義。

       我們後面主要就是以Hello World為例,分析程式是如何調用printf的:

檢視一下gcc編譯生成的Hello World程式的彙編代碼(main函數部分):

C代碼 

ELF檔案的加載和動态連結過程
  1. 08048374 <main>:  
  2.  8048374:       8d 4c 24 04         lea     0x4(%esp),%ecx  
  3.                 ……  
  4.  8048385:       c7 04 24 6c 84 04 08    movl    $0x804846c,(%esp)  
  5.  804838c:       e8 2b ff ff ff          call        80482bc <[email protected]>  
  6.  8048391:       b8 00 00 00 00          mov     $0x0,%eax  

 從上面的代碼可以看出,經過編譯後,printf函數的調用已經換成了puts函數(原因讀者可以想一下)。其中的call指令就是調用puts函數。但從上面的代碼可以看出,它調用的是[email protected]這個标号,它代表什麼意思呢?在進一步說明符号的動态解析過程以前,需要先了解兩個概念,一個是global offset table,一個是procedure linkage table。

       Global Offset Table(GOT)

       在位置無關代碼中,一般不能包含絕對虛拟位址(如共享庫)。當在程式中引用某個共享庫中的符号時,編譯連結階段并不知道這個符号的具體位置,隻有等到動态連結器将所需要的共享庫加載時進記憶體後,也就是在運作階段,符号的位址才會最終确定。是以,需要有一個資料結構來儲存符号的絕對位址,這就是GOT表的作用,GOT表中每項儲存程式中引用其它符号的絕對位址。這樣,程式就可以通過引用GOT表來獲得某個符号的位址。

       在x86結構中,GOT表的前三項保留,用于儲存特殊的資料結構位址,其它的各項儲存符号的絕對位址。對于符号的動态解析過程,我們隻需要了解的就是第二項和第三項,即GOT[1]和GOT[2]:GOT[1]儲存的是一個位址,指向已經加載的共享庫的連結清單位址(前面提到加載的共享庫會形成一個連結清單);GOT[2]儲存的是一個函數的位址,定義如下:GOT[2] = &_dl_runtime_resolve,這個函數的主要作用就是找到某個符号的位址,并把它寫到與此符号相關的GOT項中,然後将控制轉移到目标函數,後面我們會詳細分析。

       Procedure Linkage Table(PLT)

       過程連結表(PLT)的作用就是将位置無關的函數調用轉移到絕對位址。在編譯連結時,連結器并不能控制執行從一個可執行檔案或者共享檔案中轉移到另一個中(如前所說,這時候函數的位址還不能确定),是以,連結器将控制轉移到PLT中的某一項。而PLT通過引用GOT表中的函數的絕對位址,來把控制轉移到實際的函數。

       在實際的可執行程式或者共享目标檔案中,GOT表在名稱為.got.plt的section中,PLT表在名稱為.plt的section中。

大緻的了解了GOT和PLT的内容後,我們檢視一下[email protected]中到底是什麼内容:

C代碼 

ELF檔案的加載和動态連結過程
  1. Disassembly of section .plt:  
  2. 0804828c <[email protected]>:  
  3.  804828c:       ff 35 68 95 04 08       pushl   0x8049568  
  4.  8048292:       ff 25 6c 95 04 08       jmp     *0x804956c  
  5.  8048298:       00 00  
  6.         ......  
  7. 0804829c <[email protected]>:  
  8.  804829c:       ff 25 70 95 04 08       jmp     *0x8049570  
  9.  80482a2:       68 00 00 00 00          push        $0x0  
  10.  80482a7:       e9 e0 ff ff ff          jmp     804828c <_init+0x18>  
  11. 080482ac <[email protected]>:  
  12.  80482ac:       ff 25 74 95 04 08       jmp     *0x8049574  
  13.  80482b2:       68 08 00 00 00          push        $0x8  
  14.  80482b7:       e9 d0 ff ff ff          jmp     804828c <_init+0x18>  
  15. 080482bc <[email protected]>:  
  16.  80482bc:       ff 25 78 95 04 08       jmp     *0x8049578  
  17.  80482c2:       68 10 00 00 00          push    $0x10  
  18.  80482c7:       e9 c0 ff ff ff          jmp     804828c <_init+0x18>  

 可以看到[email protected]包含三條指令,程式中所有對有puts函數的調用都要先來到這裡(Hello World裡隻有一次)。可以看出,除PLT0以外(就是[email protected]所标記的内容),其它的所有PLT項的形式都是一樣的,而且最後的jmp指令都是0x804828c,即PLT0為目标的。所不同的隻是第一條jmp指令的目标和push指令中的資料。PLT0則與之不同,但是包括PLT0在内的每個表項都占16個位元組,是以整個PLT就像個數組(實際是代碼段)。另外,每個PLT表項中的第一條jmp指令是間接尋址的。比如我們的<span l

繼續閱讀