愛悠閑 > ELF Format: 程序加載和動態鏈接

ELF Format: 程序加載和動態鏈接

分類: debug  |  標簽: elf,debug,binary format,program loading,dynamic linking  |  作者: navyhu 相關  |  發布日期 : 2015-09-14  |  熱度 : 191°

Refer to: http://www.skyfree.org/linux/references/ELF_Format.pdf


前一篇文講了ELF format相關的東西,這篇翻下ELF文件的程序加載和動態鏈接知識


1. 介紹

可執行文件和共享目標文件實際就是靜態的程序,要執行程序,系統需要創建這些文件對應的動態程序,就是進程映像(Process Image)。進程映像內放置了段(segments)來存儲文本(text)、數據(data)、棧(stack)等。本文主要討論以下幾個方面:

程序頭(Program Header):程序頭是描述與程序執行直接相關的主要目標文件結構,用于定位文件中的段映像(segment images),并且包含創建進程映像所必需的信息。

程序加載(Program Loading):將目標文件加載到內存中的過程

動態鏈接(Dynamic linking):某些程序加載完一個可執行文件還不完善,還必須進一步解析里面的符號引用(這些符號的定義在其他的共享目標文件中),這個過程通過動態鏈接完成。


2. 程序頭(Program Header)

可執行文件或者共享目標文件的程序頭表(Program Header Table)是一個固定結構(就是Program Header)的列表,每個列表條目描述一個用于程序執行的段或其他信息。目標文件的每個段都包含一個或多個節(sections)。程序頭表只對可執行文件或共享目標文件有意義,重定位文件不會用到程序頭表。ELF頭(ELF header)中的e_phoff, e_phentsize和e_phnum成員描述了該文件中程序頭的位置和大小信息。

p_type: 這個條目描述的段類型,或者怎樣解析此條目中的信息

p_offset: 從文件開始到段的第一個字節的偏移量

p_vaddr: 段在內存中的虛擬地址

p_paddr: 在使用物理地址的系統中,這個成員用于存儲段的物理地址

p_filesz: 段在文件中的大小,可能為0

p_memsz: 段在內存中的大小,可能為0

p_flags: 段相關的標志

p_align: 可加載進程中的段必須與p_vaddr和p_offset匹配,并且是頁大小的整數倍。這個成員描述了段在內存和文件中的對齊數值。0和1表示沒有對齊要求,否則p_align必須為2的正整數次方,并且p_vaddr等于p_offset且模p_align等于0。


2.1 段類型(Segment Types)

程序頭表中某些條目描述進程段,另一些只提供額外的信息而不會加載到進程映像。段條目除一些特殊的條目外沒有固定順序。以下為段類型:


PT_NULL: 未使用的列表條目,該條目中其他成員的值是未定義的

PT_LOAD: 可加載的段,將文件中p_filesz字節加載到對應內存段中的的開始位置,文件段不能小于內存段,如果內存段大小(p_memsz)大于文件段大小,多余空間填充為0。程序頭表中的可加載段根據p_vaddr值升序排列。

PT_DYNAMIC: 動態鏈接相關信息

PT_INTERP: 指定一個可調用解釋器的字符串路徑名(成員中存的是該路徑的位置和大小)。只有可執行文件才有這種段(共享文件也可以有。。),并且只能有一個。此段必須放在所有的可加載段條目之前。

PT_NOTE: 附加信息的位置和大小

PT_SHLIB: 預留類型,無意義

PT_PHDR: 指出該程序頭表(program header table)自身在文件和內存映像中的位置和大小,一個文件只能有一個這種類型,并且只有當程序頭表是進程內存映像一部分時才有。此類型條目必須在所有可加載段條目之前。

PT_LOPROC - PT_HIPROC: 預留給處理器相關語義


2.2 基地址(Base Address)

可執行和共享目標文件都有一個基地址,該地址是目標程序映像在內存中的最小虛擬地址。基地址被用于動態鏈接中重定位內存映像。

基地址是在執行過程中由以下3個值計算出:內存加載地址、最大內存頁大小、程序可加載段的最小虛擬地址。程序頭(program headers)中的虛擬地址不代表程序內存映像中的實際虛擬地址,當計算基地址時,需要找出所有可加載段中的最小的p_vaddr值,然后根據最大頁大小計算出基地址。對于有些類型的文件,內存地址可能不等于p_vaddr。

.bss Section是一個系統section,它的類型為SHT_NOBITS。這個section雖然不占用文件空間,但是卻會影響對應段的內存映像。通常這些未初始化數據都被放在段尾,因此需要在相應段頭條目中將p_memsz設為大于p_filesz的值。


2.3 注解節(Note Section)

有些系統或者供應商需要在目標文件中標記特殊信息以供其他程序識別或者兼容,類型為SHT_NOTE的section和類型為PT_NOTE的程序頭條目就是做這件事的。section或者程序頭條目中的Note信息存儲一些條目,每個條目中包含是一個序列(4-byte word)。下圖是一個例子

namesz and name: name中的頭namesz個字節是一個表示所有者或者開發者的字符串

descsz and desc: desc中的頭descsz個字節是描述信息

type: 用于解析描述信息


如下是一個Note段實例:


3. 程序加載(Program Loading)

當系統創建一個進程映像時,它只是邏輯上將文件中的段拷貝到對應的虛擬內存段,而實際上只有當執行是需要用到某個邏輯頁時,它才會被加載到內存中,一個進程中有大量的沒有被使用的邏輯頁。因此延遲物理讀取可以提高系統性能。要取得這樣的效率,我們就要讓可執行文件和共享目標文件中段映像的偏移和虛擬地址模頁尺寸相等(就是全等模頁尺寸,congruent modulo the page size)


上圖是一個目標文件實例,Text和Data段的的偏移和虛擬地址都全等模4KB,需要4個文件頁來存儲text或者data段

第一個text頁包括ELF header、程序頭表(program header table)、和其他信息

最后一個text頁持有data段開始部分的拷貝

第一個data頁存有text段尾部分拷貝

最后一個data也可能包含與運行中的進程無關的文件信息


下圖是程序頭段(Program Header Segments)


系統在邏輯上將每個段看成是完整且獨立的,并以此來設置內存權限;段的地址會被調整來保證地址空間中每個邏輯頁都有一個單獨的權限集。上例中,文件中存儲text段尾部分和data段首部分的區域會被映射兩次:text段的虛擬地址和一個data段的虛擬地址

data段尾部分對未初始化數據需要進行特殊處理,系統會將這部分填充為0。因此如果文件的最后一個data頁包含了邏輯內存頁中不存在的信息,那這些額外的數據會被設置為0(沒太明白。。)。另外3個頁中的’雜質‘邏輯上不在進程映像中,未明確規定系統是否清除他們。該文件對應的內存映像如下(假設頁尺寸為4KB,0x1000):



可執行文件與共享目標文件在段的加載方面有一點不同。可執行文件的段一般包含絕對代碼(absolute code),為了使進程正確執行,這些段必須位于用來構建可執行文件的虛擬地址中。所以系統直接使用p_vaddr當作段在內存中的虛擬地址。

而共享目標文件的段一般包含位置無關代碼,對于不同進程,這些段的虛擬地址會變動。雖然系統對于每個單獨的進程使用虛擬地址,但是對于這些段卻使用相對地址。因為地址無關代碼使用段與段之間的相對地址,因此文件虛擬地址之間的差異必須與內存虛擬地址之間的差異相同。下表展示了一個共享目標文件在不同進程中的虛擬地址,不同進程中虛擬地址雖然不同,但虛擬地址之間的差是相同的,此例同樣展示了基地址的計算:


4. 動態鏈接(Dynamic Linking)

4.1 程序解釋器(Program Interpreter)

每個可執行文件都可能包含一個PT_INTERP類型的程序頭條目,在執行exec(BA_OS)期間,系統從PT_INTERP段中獲取解釋器(其實也是一個可執行/共享目標文件)路徑,并且用改解釋器文件的段來創建初始進程映像。系統使用解釋器來組建內存映像而不是使用原來那個可執行文件,這種情況下解釋器就要負責從系統那得到控制權,并為應用程序提供運行環境。

解釋器有兩種獲取控制權的方式。第一種是獲得一個讀取可執行文件的文件描述符(指向文件開頭位置),它可以使用這個文件描述符讀取(或者映射)可執行文件的段到內存中。第二種是系統根據可執行文件格式(特定格式),將可執行文件加載到內存中。解釋器本身是一個可執行文件或者共享 目標文件:

    如果解釋器是共享目標文件,那該文件會以位置無關代碼加載(加載地址在不同進程里可能不同);系統在動態段區域中創建它的段,共享目標類型解釋器通常不會與原本的可執行文件的段地址產生沖突。

    如果解釋器是可執行文件,那解釋器會被加載到固定地址。系統使用程序頭表中的虛擬地址來創建解釋器的段,可執行文件類型解釋器的虛擬地址可能與原本的可執行文件產生沖突,解釋器要負責解決沖突。


4.2 動態鏈接器(Dynamic Linker)

使用動態鏈接構建一個可執行文件時,鏈接器會把一個PT_INTERP類型的程序頭條目(program header element)插入可執行文件,以使系統將動態鏈接器當成程序解釋器來調用。

Exec(BA_OS)與動態鏈接器一起創建程序的內存映像,包括以下步驟:

    將可執行文件的內存段添加到進程映像

    將共享目標的內存段添加到進程映像

    為可執行文件和它的共享目標文件進行重定位

    如果沒有其他進程使用(除動態鏈接器外),關閉讀取可執行文件的文件描述符

    將控制權交給程序,使其看起來就像是直接從exec(BA_OS)獲得的控制權一樣


連接器還會為輔助可執行文件與共享目標文件的動態鏈接構建一些數據,在上面的’程序頭(Program Header)‘中,這些數據在可加載(loadable)段中。

    類型為SHT_DYNAMIC的.dynamic section存儲了其中一些數據記錄了其他動態鏈接信息的地址,這些數據位于section的開始位置

    類型為SHT_HASH的.hash section存儲了一個符號哈希表

    類型為SHT_PROGBITS的.got和.plt sections分別存有兩個表:全局偏移表(the global offset table)和進程鏈接表(the procedure linkage table)。下面將介紹動態連接器怎樣使用這些表來建立目標文件的內存映像。


由于ABI(Application Binary Interface)標準的程序都需要從共享目標庫中引入一些基礎的系統服務,所以動態鏈接器將參與到每個ABI程序的執行中。

如’程序加載(Program Loading)’中介紹,共享目標占有的虛擬內存地址可能與文件程序頭表(program header table)中記錄的地址不一樣,動態鏈接器會重定位內存映像,在應用程序獲得控制前更新絕對地址。

如果進程環境【見exec(BA_OS)】中包含一個名為LD_BIND_NOW的非null變量,動態鏈接器會在將控制交給程序之前處理所有的重定位。比如,以下所有的環境變量都會引起此行為:

LD_BIND_NOW = 1

LD_BIND_NOW = on

LD_BIND_NOW = off

動態鏈接器可以使用惰性策略來獲取進程鏈接表條目(procedure linkage table entries),這樣可避免解析和重定位那些沒有被調用的函數。


4.3 動態Section(Dynamic Section)

如果一個目標文件參與了動態鏈接,那此文件的程序頭表(program header table)中就會存在一個類型為PT_DYNAMIC的段。此段中包含.dynamic section。這個section被一個特殊的符號 _DYNAMIC 標記,包含下列結構的一個數組:


d_tag的值決定了d_un的意義:

dval: 類型為Elf32_Word的聯合變量代表一個整數,不同值有不同意義

d_ptr: 類型為Elf32_Addr的聯合變量代表程序虛擬地址。一個文件的虛擬地址可能與執行時的內存實際虛擬地址不同,當解析動態結構中的地址時,動態鏈接器會根據文件中的值和內存基地址來計算實際地址。為統一起見,文件的動態結構中不包含存有”正確“地址的重定位條目。

下表是d_tag的可能取值,如果tag標記為”mandatory"(強制),則ABI文件的動態鏈接鏈表必須有一個該類型的條目。如果標記為“optional”(可選),則這種類型的條目則不是必要的:


表續:


DT_NULL:tag為DT_NULL的條目標記_DYNAMIC列表結尾

DT_NEEDED:這個條目存儲指向一個字符串表中一個字符串,表明一個需要的(needed)庫的名字。動態列表(dynamic array)可能包含多個這種類型的條目,這種條目之間的相對位置很重要,與其他類型的條目的相對位置則無所謂。

DT_PLTRELSZ:此條存儲與進程鏈接表(procedure linkage table)關聯的所有重定位條目總的大小(字節)。如果存在DT_JMPREL類型的條目,則DT_PLTRELSZ條目也必須存在

DT_HASH:符號哈希表(symbol hash table)的地址,此哈希表與DT_SYMTAB條目指向的符號表相對應

DT_STRTAB:字符串表的地址,符號名、庫名,以及其他字符串都存在這個表中

DT_SYMTAB:符號表地址

DT_RELA:重定位表的地址。一個目標文件可保護多個重定位sections,為一個可執行文件或共享目標文件創建重定位表時,連接器會把所有這些sections連成一個表。雖然文件中這些sections相互獨立,但是動態鏈接器值看到一個表。

DT_RELASZ:DT_RELA重定位表的總大小(字節)

DT_RELAENT:DT_RELA重定位表中每個表項的大小(字節)

DT_STRSZ:字符串表的大小(字節)

DT_SYMENT:符號表中每個表項的大小(字節)

DT_INIT:初始化函數的地址

DT_FINI:終止函數的地址

DT_SONAME:字符串表中的字符串偏移,共享目標文件的名字。

DT_RPATH:字符串表中的字符串偏移,搜索庫的路徑

DT_SYMBOLIC:這個元素在共享目標庫中可以改變動態鏈接器對自身的符號解析算法,動態鏈接器將首先在共享目標文件自身中搜索,而不是先在可執行文件中搜索。如果沒找到,才會開始搜索可執行文件和其他共享目標文件。

DT_REL:與DT_RELA相似,不過它的表只有隱式的附加數(addends)。如果這種tag的條目存在,那動態結構中也必需有DT_RELSZ和DT_RELENT條目

DT_RELSZ:DT_REL重定位表的總大小(字節)

DT_RELENT:DT_REL重定位表中每一項的大小(字節)

DT_PLTREL:指明進程鏈接表(procedure linkage table)引用的重定位條目類型。d_val存儲DT_REL或者DT_RELA,進程鏈接表中的重定位必需是同樣的類型

DT_DEBUG:用于調試,它的內容不是ABI標準,使用這個條目的程序不符合ABI標準

DE_TEXTREL:如果動態列表中沒有這種類型,則表明重定位條目不應該修改任何一個不可寫段(段權限定義在程序頭表【program header table】中)。如果存在這種tag類型的條目,則至少有一個重定位條目可能會更改某個不可寫段,動態鏈接器據此要做一些準備。

DT_JMPREL:如果存在,這個條目的d_ptr成員存儲那些只關聯到進程鏈接表(procedure linkage table)的重定位條目的地址。如果使用了延遲綁定(lazy binding),分開這些重定位條目會使動態鏈接器在進程初始化時忽略他們。

DT_LOPROC to DT_HIPROC:預留給處理器相關語義

除了DT_NULL必須在列表尾部,以及DT_NEEDED條目之間的相對順序,其他條目之間可以任意順序存儲。


4.4 共享目標依賴(Shared Object Dependencies)

當鏈接器處理歸檔庫(經過壓縮之類的庫)時,它首先提取出庫成員并將它們拷貝到輸出目標文件。執行期間,這些靜態鏈接服務不需要動態鏈接器就是可用的。共享目標文件也會提供服務,動態鏈接器必須將那些適當的共享目標文件加載到進程映像中執行。所以可執行文件和共享目標文件會描述他們的依賴關系。

當動態鏈接器創建一個目標文件的內存段時,這些依賴(存在動態結構的DT_NEEDED條目中)指出需要哪些共享目標文件來提供支持。通過重復連接這些被引用的共享目標文件和他們的依賴,動態鏈接器就可以創建一個完整的進程映像了。解析符號引用時,動態鏈接器使用寬度優先搜索檢查符號表,它首先查看可執行文件本身的符號表,然后在DT_NEEDED條目指定的庫中查找(按條目順序),然后在DT_NEEDED條目庫中的DT_NEEDED庫中查找,一直進行下去。進程必須擁有共享目標文件的可讀權限。

依賴列表中的名字要么拷貝自DT_SONAME字符串,要么拷貝自用來構建目標文件的共享目標文件的路徑名。比如,如果鏈接器使用了一個共享目標文件(含有DT_SONAME條目lib1)和共享目標文件(路徑名為/usr/lib/lib2),那么這個可執行文件的依賴列表就包括了lib1和/usr/lib/lib2。

如果一個共享目標文件名包含反斜杠(\),比如上面的/usr/lib/lib2,那動態鏈接器就會將它直接當成路徑名(包含路徑和文件名)使用。如果沒有包含反斜杠,則通過以下3中方式搜索:

    第一,動態列表標志DT_RPATH可能提供一個包含路徑列表的字符串,以冒號分隔。比如字符串,/home/dir/lib:/home/dir2/lib: 告訴動態鏈接器先搜索/home/dir/lib,再搜索/home/dir2/lib,然后是當前目錄。

    第二,有一個名為LD_LIBRARY_PATH的環境變量可能存有像上面那樣的目錄列表,此列表以分號結尾(;),后面可以跟其他目錄列表。例子如下:

        LD_LIBRARY_PATH=/home/dir/lib:/home/dir2/lib:

        LD_LIBRARY_PATH=/home/dir/lib;/home/dir2/lib:

        LD_LIBRARY_PATH=/home/dir/lib:/home/dir2/lib:;

    所有LD_LIBRARY_PATH目錄都會在DT_RPATH 目錄之后搜索,雖然有一些程序(比如鏈接器)對待分號前后的列表的行為不同,但是動態鏈接器都是一樣的。然而動態鏈接器遵循上述的分號規則。

    第三,如果通過以上兩種方法都沒找到,動態鏈接器就會搜索/usr/lib


4.5 全局偏移表(Global Offset Table)

位置無關代碼通常不能保護絕對虛擬地址。全局偏移表在私有數據中存有絕對地址,讓這些地址即使在和程序的位置無關性和共享性不協調的情況下也可用。程序通過位置無關地址引用它的全局便宜表,并且從中提取絕對地址,以此將位置無關地址重定向到絕對位置。

開始時,全局偏移表存儲了它的重定位條目需要的信息,當系統為可加載目標文件創建了內存段之后,動態鏈接器就會處理重定位條目,其中一些類型為R_386_GLOB_DAT的條目指向全局偏移表。動態鏈接器決定相關的符號值、計算它們的絕對地址、并且將適當的內存表條目設置為合適的值。雖然鏈接器構建目標文件時絕對地址是未知的,但是由于動態鏈接器知道所有內存段的地址,所以它可以計算出里面包含的符號的絕對地址。

如果一個程序需要使用一個符號的絕對地址,這個符號就會有一個全局偏移表條目。由于可執行文件和共享目標文件各自有自己的全局偏移表,同一個符號的地址可能會出現同時出現在幾個表中。動態鏈接器會在將控制權交給進程映像之前處理所有全局偏移表的重定位,保證執行期間所有絕對地址都可用。

表的第零個條目(在位置0上的條目)是預留來存儲動態結構(dynamic structure)的地址,動態結構被符號_DYNAMIC引用。這樣程序(比如動態鏈接器)就可以在處理重定位條目前找到自己的動態結構了。這對于動態鏈接器來說尤其重要,因為它必須獨立地初始化自己來重定位它的內存映像。在32-bit因特爾體系結構中,全局偏移表的第一和第二個條目也是預留的。

系統在不同的程序中可能為相同的共享目標選擇不同的內存段地址,甚至可能在同一個程序的不同的執行中選擇不同的庫地址。然而,一旦進程映像創建完成,內存段地址就不會改變了。

全局偏移表的格式和意義根據處理器不同而不同,32bit因特爾體系結構,可能用符號_GLOBAL_OFFSET_TABLE_來定位此表,這個符號可能位于.got section的中間位置。


4.6 進程鏈接表(Procedure Linkage Table)

與全局偏移表用于將位置無關地址轉化為絕對地址相似,進程鏈接表用來將位置無關的函數調用轉化為絕對位置。鏈接器不能解析轉移自其他可執行文件或共享目標文件的函數調用,因此鏈接器就將這些程序轉移控制安排到進程鏈接表的條目中。在SYSTEM V體系結構中,進程鏈接表位于共享文本中,但是它使用私有全局偏移表中的地址。動態鏈接器確定目標的絕對地址并且更改相應全局偏移表的內存映像。動態鏈接器因此可以直接重定向這些條目,而不用管程序文本的位置無關性和共享性。可執行文件和共享目標文件都有自己的進程鏈接表。

下圖為絕對位置進程鏈接表


下圖為位置無關進程鏈接表:



動態鏈接器根據以下步驟與程序一起‘合作’通過進程鏈接表和全局偏移表來解析符號引用

A. 當開始創建程序內存映像時,動態鏈接器將全局偏移表的第二和第三項設置為特殊值,下面詳細講解

B. 如果進程鏈接表是位置無關的,全局偏移表必須位于%ebx寄存器中。內存映像中的每個共享目標文件都有自己的進程鏈接表,控制之后從目標文件中轉移至同一目標文件中的進程鏈接表條目。因此,調用函數(調用其他函數的函數)要在調用進程鏈接表條目之前負責設置全局偏移表的基址寄存器。

C. 比如程序調用name1,將控制轉移至標簽.PLT1處

D. 第一個指令跳轉至name1對應的全局偏移表條目的地址,開始時全偏移表存儲的是push1指令的地址,而非name1的真正地址。

E. 然后程序將重定位偏移壓入棧中,重定位偏移是重定位表中一個32bit、非負的字節偏移。這個重定位條目類型為R_386_JMP_SLOT,它的偏移指明了前一個jmp指令中使用的全局偏移表條目。這個重定位條目也包含了一個符號表索引,告訴動態鏈接器被應用的符號是什么(這個例子中是name1)。

F. 把重定位偏移壓入棧之后,程序就跳轉到.PLT0位置,這是進程鏈接表的第一個條目。push1指令將全局偏移表的第二個條目(got_plus_4或者4(%ebx))壓入棧中,將一個字的定位信息提供給動態鏈接器。然后程序跳轉到全局偏移表的第三個地址(got_plus_8或許8(%ebx)),將控制交給動態鏈接器。

G. 當動態鏈接器獲得控制后,就展開棧(unwind the stack),在指定的重定位條目中查找符號值,將name1的‘真實’地址存入它的全局偏移表條目中,并把控制轉交給指定的目的地址。

H. 接下來進程鏈接表條目的執行會直接轉移到name1,動態鏈接器不會再被調用。也就是說,.PLT1位置上的jmp指令轉移到name1,而不是'陷入'(fall through)壓棧指令中。


LD_BIND_NOW環境變量可以改變動態鏈接器的行為。如果它的值非null,動態鏈接器就會在將控制轉交給程序前解析進程鏈接表條目。就是說,動態鏈接器在進程初始化過程中處理類型為R_386_JMP_SLOT的重定位條目。否則,動態鏈接器就使用延遲策略來解析進程連接表條目,在表的任何一個條目執行之前不進行符號解析和重定位。


4.6 哈希表(Hash Table)

Elf32_Word對象的哈希表支持符號表,以下標簽顯示了哈希表的結構,但是這不是規范(spec)的一部分


bucket數組包含nbucket個條目,chain數組包含nchain個條目,從0開始索引。bucket和chain都存有符號表索引,chain表項與符號表一一對應,符號表條目數應等于nchain,所以符號表的索引頁可以用來選擇chain表的條目。哈希函數接受一個符號名并且返回一個值用于計算bucket里的索引。如果哈希函數(算法)為某個名字返回值x,那么bucket[x%nbucket]也是一個索引y,此索引可用于chain和符號表中。如果得到的符號表條目(symbol[x]不是想要的那個,chain[y]就是具有相同哈希值的下一個符號表條目。我們可以一直在chain表中找下去,直到找到想要的條目或者chain表項的值為STN_UNDEF。


4.7 初始化和終結函數(Initialization and Termination Functions)

當動態鏈接器創建了進程映像并且執行了重定位后,每個共享目標文件都會執行一些初始化代碼。這些初始化函數的調用沒有特定順序,但是所有共享目標文件的初始化都要在可執行文件獲得控制前完成。

同樣,共享目標文件也可能含有終結函數,它會在基進程開始終結過程后,與atexit(BA_OS)機制一起執行。終結函數的順序也不定。

共享目標文件通過動態結構(dynamic structure)中的DT_INIT和DT_FINI條目來指定初始化和終結函數。通常這些函數的代碼位于.init和.fini sections中。



同類文章:debug
快乐彩中奖说明