程序员的自我修养--链接、装载与库笔记:可执行文件的装载与进程
可執(zhí)行文件只有裝載到內(nèi)存以后才能被CPU執(zhí)行。
1. 進程虛擬地址空間
程序和進程有什么區(qū)別:程序(或者狹義上講可執(zhí)行文件)是一個靜態(tài)的概念,它就是一些預(yù)先編譯好的指令和數(shù)據(jù)集合的一個文件;進程則是一個動態(tài)的概念,它是程序運行時的一個過程,很多時候把動態(tài)庫叫做運行時(Runtime)也有一定的含義。
每個程序被運行起來以后,它將擁有自己獨立的虛擬地址空間(Virtual Address Space),這個虛擬地址空間的大小由計算機的硬件平臺決定,具體地說是由CPU的位數(shù)決定的。硬件決定了地址空間的最大理論上限,即硬件的尋址空間大小,比如32位的硬件平臺決定了虛擬地址空間的地址為0到2^32-1,即0x00000000~0xFFFFFFFF,也就是我們常說的4GB虛擬空間大小;而64位的硬件平臺具有64位尋址能力,它的虛擬地址空間達到了2^64字節(jié),即0x0000000000000000~0xFFFFFFFFFFFFFFFF,總共17179869184GB。
從程序的角度看,我們可以通過判斷C語言程序中的指針所占的空間來計算虛擬地址空間的大小。一般來說,C語言指針大小的位數(shù)與虛擬地址空間的位數(shù)相同,如32位平臺下的指針為32位,即4字節(jié);64位平臺下的指針為64位,即8字節(jié)。當然有些特殊情況下,這種規(guī)則不成立。
在下文中以32位的地址空間為主,64位的與32位類似。
那么32位平臺下的4GB虛擬空間,我們的程序是否可以任意使用呢?不行。因為程序在運行的時候處于操作系統(tǒng)的監(jiān)管下,操作系統(tǒng)為了達到監(jiān)控程序運行等一系列目的,進程的虛擬空間都在操作系統(tǒng)的掌握之中。進程只能使用那些操作系統(tǒng)分配給進程的地址,如果訪問未經(jīng)允許的空間,那么操作系統(tǒng)就會捕獲到這些訪問,將進程的這種訪問當作非法操作,強制結(jié)束進程。我們經(jīng)常在Windows下碰到”進程因非法操作需要關(guān)閉”或Linux下的”Segmentation fault”很多時候是因為訪問了未經(jīng)允許的地址。
PAE(Physical Address Extension):從硬件層面上來講,原先的32位地址線只能訪問最多4GB的物理內(nèi)存。但是自從擴展至36位地址線之后,Intel修改了頁映射的方式,使得新的映射方式可以訪問到更多的物理內(nèi)存。Intel把這個地址擴展方式叫做PAE。擴展的物理地址空間,對于普通應(yīng)用程序來說正常情況下感覺不到它的存在,因為這主要是操作系統(tǒng)的事,在應(yīng)用程序里,只有32位的虛擬地址空間。那么應(yīng)用程序該如何使用這些大于常規(guī)的內(nèi)存空間呢?一個很常見的方法就是操作系統(tǒng)提供一個窗口映射的方法,把這些額外的內(nèi)存映射到進程地址空間中來。在Windows下,這種訪問內(nèi)存的操作方式叫做AWE(Address Windowing Extensions);而像Linux等UNIX類操作系統(tǒng)則采用mmap()系統(tǒng)調(diào)用來實現(xiàn)。
2. 裝載的方式
程序執(zhí)行時所需要的指令和數(shù)據(jù)必須在內(nèi)存中才能夠正常運行,最簡單的方法就是將程序運行所需要的指令和數(shù)據(jù)全都裝入內(nèi)存中,這樣程序就可以順利運行,這就是最簡單的靜態(tài)裝入的方法。但是很多情況下程序所需要的內(nèi)存數(shù)量大于物理內(nèi)存的數(shù)量,當內(nèi)存的數(shù)量不夠時,根本的解決辦法就是添加內(nèi)存。程序運行時是有局部性原理,所以我們可以將程序最常用的部分駐留在內(nèi)存中,而將一些不太常用的數(shù)據(jù)存放在磁盤里面,這就是動態(tài)裝入的基本原理。
覆蓋裝入(Overlay)和頁映射(Paging)是兩種很典型的動態(tài)裝載方法,它們所采用的思想都差不多,原則上都是利用了程序的局部性原理。動態(tài)裝入的思想就是程序用到哪個模塊,就將哪個模塊裝入內(nèi)存,如果不用就暫時不裝入,存放在磁盤中。
覆蓋裝入:在沒有發(fā)明虛擬存儲之前使用比較廣泛,現(xiàn)在已經(jīng)幾乎被淘汰了。覆蓋裝入的方法把挖掘內(nèi)存嵌入的任務(wù)交給了程序員,程序員在編寫程序的時候必須手工將程序分割成若干塊,然后編寫一個小的輔助代碼來管理這些模塊何時應(yīng)該駐留內(nèi)存而何時應(yīng)該被替換掉。這個小的輔助代碼就是所謂的覆蓋管理器(Overlay Manager)。覆蓋裝入是典型的利用時間換取空間的方法。
頁映射:是虛擬存儲機制的一部分,它隨著虛擬存儲的發(fā)明而誕生。與覆蓋裝入的原理相似,頁映射也不是一下子就把程序的所有數(shù)據(jù)和指令都裝入內(nèi)存,而是將內(nèi)存和所有磁盤中的數(shù)據(jù)和指令按照”頁(Page)”為單位劃分成若干個頁,以后所有的裝載和操作的單位都是頁。硬件規(guī)定頁的大小有4096字節(jié)、8192字節(jié)、2MB、4MB等。
3. 從操作系統(tǒng)角度看可執(zhí)行文件的裝載
進程的建立:從操作系統(tǒng)的角度來看,一個進程最關(guān)鍵的特征是它擁有獨立的虛擬地址空間,這使得它有別于其它進程。很多時候一個程序被執(zhí)行同時都伴隨著一個新的進程的創(chuàng)建。創(chuàng)建一個進程,然后裝載相應(yīng)的可執(zhí)行文件并且執(zhí)行。在有虛擬存儲的情況下,上述過程最開始只需要做三件事情:
(1). 首先是創(chuàng)建虛擬地址空間:一個虛擬空間由一組頁映射函數(shù)將虛擬空間的各個頁映射至相應(yīng)的物理空間,那么創(chuàng)建一個虛擬空間實際上并不是創(chuàng)建空間而是創(chuàng)建映射函數(shù)所需要的相應(yīng)的數(shù)據(jù)結(jié)構(gòu)。在i386的Linux下,創(chuàng)建虛擬地址空間實際上只是分配一個頁目錄(Page Directory)就可以了,甚至不設(shè)置頁映射關(guān)系,這些映射關(guān)系等到后面程序發(fā)生頁錯誤的時候再進行設(shè)置。
(2). 讀取可執(zhí)行文件頭,并且建立虛擬空間與可執(zhí)行文件的映射關(guān)系:上面那一步的頁映射關(guān)系函數(shù)是虛擬空間到物理內(nèi)存的映射關(guān)系,這一步所做的是虛擬空間與可執(zhí)行文件的映射關(guān)系。當程序執(zhí)行發(fā)生頁錯誤時,操作系統(tǒng)將從物理內(nèi)存中分配一個物理頁,然后將該”缺頁”從磁盤中讀取到內(nèi)存中,再設(shè)置缺頁的虛擬頁和物理頁的映射關(guān)系,這樣程序才得以正常運行。當操作系統(tǒng)捕獲到缺頁錯誤時,它應(yīng)知道程序當前所需要的頁在可執(zhí)行文件中的哪一個位置。這就是虛擬空間與可執(zhí)行文件之間的映射關(guān)系。從某種角度來看,這一步是整個裝載過程中最重要的一步,也是傳統(tǒng)意義上”裝載”的過程。
由于可執(zhí)行文件在裝載時實際上是被映射的虛擬空間,所以可執(zhí)行文件很多時候又被叫做映像文件(Image)。
Linux中將進程虛擬空間中的一個段叫做虛擬內(nèi)存區(qū)域(VMA, Virtual Memory Area),在Windows中將這個叫做虛擬段(Virtual Section),其實它們都是同一個概念。
(3). 將CPU指令寄存器設(shè)置成可執(zhí)行文件入口,啟動運行:操作系統(tǒng)通過設(shè)置CPU的指令寄存器將控制權(quán)轉(zhuǎn)交給進程,由此進程開始執(zhí)行。可執(zhí)行文件入口即是ELF文件頭中保存的入口地址。
頁錯誤(Page Fault):隨著進程的執(zhí)行,頁錯誤也會不斷地產(chǎn)生,操作系統(tǒng)也會為進程分配相應(yīng)的物理頁來滿足進程執(zhí)行的需求。
4. 進程虛存空間分布
ELF文件鏈接視圖和執(zhí)行視圖:在一個正常的進程中,可執(zhí)行文件中包含的往往不止代碼段,還有數(shù)據(jù)段、BSS等,所以映射到進程虛擬空間的往往不止一個段。當段的數(shù)量增多時,就會產(chǎn)生空間浪費的問題。ELF文件被映射時,是以系統(tǒng)的頁長度作為單位的,那么每個段在映射時的長度應(yīng)該都是系統(tǒng)頁長度的整數(shù)倍;如果不是,那么多余部分也將占用一個頁。一個ELF文件中往往有十幾個段,那么內(nèi)存空間的浪費是可想而知的。
ELF文件中,段的權(quán)限往往只有為數(shù)不多的幾種組合,基本上是三種:(1).以代碼段為代表的權(quán)限為可讀可執(zhí)行的段;(2).以數(shù)據(jù)段和BSS段為代表的權(quán)限為可讀可寫的段;(3).以只讀數(shù)據(jù)段為代表的權(quán)限為只讀的段。對于相同權(quán)限的段,把它們合并到一起當作一個段進行映射。
ELF可執(zhí)行文件引入了一個概念叫做”Segment”,一個”Segment”包含一個或多個屬性類似的”Section”。從鏈接的角度看,ELF文件是按”Section”存儲的;從裝載的角度看,ELF文件又可以按照”Segment”劃分。”Segment”的概念實際上是從裝載的角度重新劃分了ELF的各個段。在將目標文件鏈接成可執(zhí)行文件的時候,鏈接器會盡量把相同權(quán)限屬性的段分配在同一空間。比如可讀可執(zhí)行的段都放在一起,這種段的典型是代碼段;可讀可寫的段放在一起,這種段的典型是數(shù)據(jù)段。在ELF中把這些屬性相似的、又連在一起的段叫做一個”Segment”,而系統(tǒng)正是按照”Segment”而不是”Section”來映射可執(zhí)行文件的。
下面是一個很小的例子程序(SectionMapping.c):
#include <stdlib.h>int main()
{while (1) {sleep(1000);}return 0;
}
使用靜態(tài)鏈接的方式將其編譯鏈接成可執(zhí)行文件SectionMapping.elf,執(zhí)行:
gcc -static SectionMapping.c -o SectionMapping.elf
使用readelf可以看到,可執(zhí)行文件SectionMappint.elf中總共有31個段(Section),如下圖所示:
正如描述”Section”屬性的結(jié)構(gòu)叫做段表,描述”Segment”的結(jié)構(gòu)叫程序頭(Program Header),它描述了ELF文件該如何被操作系統(tǒng)映射到進程的虛擬空間,執(zhí)行結(jié)果如下圖所示:
可以看到,這個可執(zhí)行文件共有6個Segment。從裝載的角度看,目前只關(guān)心兩個”LOAD”類型的Segment,因為只有它是需要被映射的,其它的諸如”NOTE”、”TLS”、”GNU_STACK”都是在裝載時起輔助作用的。所有相同屬性的”Section”被歸類到一個”Segment”,并且映射到同一個VMA。總的來說,”Segment”和”Section”是從不同的角度來劃分同一個ELF文件。這個在ELF中被稱為不同的視圖(View),從”Section”的角度來看ELF文件就是鏈接視圖(Linking View),從”Segment”的角度來看就是執(zhí)行視圖(Execution View)。當我們在談到ELF裝載時,”段”專門指”Segment”;而在其它的情況下,”段”指的是”Section”。
ELF可執(zhí)行文件中有一個專門的數(shù)據(jù)結(jié)構(gòu)叫做程序頭表(Program Header Table)用來保存”Segment”的信息。因為ELF目標文件不需要被裝載,所以它沒有程序頭表,而ELF的可執(zhí)行文件和共享庫文件都有。跟段表結(jié)構(gòu)一樣,程序頭表也是一個結(jié)構(gòu)體數(shù)組,它的結(jié)構(gòu)體Elf32_Phdr或Elf64_Phdr(聲明在/usr/include/elf.h)如下:
/* Program segment header. */
typedef struct
{Elf32_Word p_type; /* Segment type */Elf32_Off p_offset; /* Segment file offset */Elf32_Addr p_vaddr; /* Segment virtual address */Elf32_Addr p_paddr; /* Segment physical address */Elf32_Word p_filesz; /* Segment size in file */Elf32_Word p_memsz; /* Segment size in memory */Elf32_Word p_flags; /* Segment flags */Elf32_Word p_align; /* Segment alignment */
} Elf32_Phdr;typedef struct
{Elf64_Word p_type; /* Segment type */Elf64_Word p_flags; /* Segment flags */Elf64_Off p_offset; /* Segment file offset */Elf64_Addr p_vaddr; /* Segment virtual address */Elf64_Addr p_paddr; /* Segment physical address */Elf64_Xword p_filesz; /* Segment size in file */Elf64_Xword p_memsz; /* Segment size in memory */Elf64_Xword p_align; /* Segment alignment */
} Elf64_Phdr;
Elf32_Phdr或Elf64_Phdr結(jié)構(gòu)體的幾個成員與使用”readelf -l”打印文件頭表顯示的結(jié)果一一對應(yīng)。結(jié)構(gòu)體的各個成員的基本含義,如下表所示:
對于”LOAD”類型的”Segment”來說,p_memsz的值不可以小于p_filesz,否則就是不符合常理的。如果p_memsz大于p_filesz,就表示該”Segment”在內(nèi)存中所分配的空間大小超過文件中實際的大小,這部分”多余”的部分則全部填充為”0”。這樣做的好處是,我們在構(gòu)造ELF可執(zhí)行文件時不需要再額外設(shè)立BSS的”Segment”了,可以把數(shù)據(jù)”Segment”的p_memsz擴大,那些額外的部分就是BSS。因為數(shù)據(jù)段和BSS的唯一區(qū)別就是:數(shù)據(jù)段從文件中初始化內(nèi)容,而BSS段的內(nèi)容全都初始化為0。這也就是在前面的例子中只看到了兩個”LOAD”類型的段,而不是三個,BSS已經(jīng)被合并到了數(shù)據(jù)類型的段里面。
堆和棧:在操作系統(tǒng)里面,VMA除了被用來映射可執(zhí)行文件中的各個”Segment”以外,它還可以有其它的作用,操作系統(tǒng)通過使用VMA來對進程的地址空間進行管理。進程在執(zhí)行的時候它還需要用到棧(Stack)、堆(Heap)等空間,事實上它們在進程的虛擬空間中的表現(xiàn)也是以VMA的形式存在的,很多情況下,一個進程中的棧和堆分別都有一個對應(yīng)的VMA。
在Linux下,可以通過查看”/proc”來查看進程的虛擬空間分布,如下圖所示:
上圖的輸出結(jié)果中:第一列是VMA的地址范圍;第二列是VMA的權(quán)限,”r”表示可讀,”w”表示可寫,”x”表示可執(zhí)行,”p”表示私有(COW, Copy on Write),”s”表示共享。第三列是偏移,表示VMA對應(yīng)的Segment在映像文件中的偏移;第四列表示映像文件所在設(shè)備的主設(shè)備號和次設(shè)備號;第五列表示映像文件的節(jié)點號。最后一列是映像文件的路徑。我們可以看到進程中有8個VMA,只有前兩個是映射到可執(zhí)行文件中的兩個Segment。另外六個段的文件所在設(shè)備主設(shè)備號和次設(shè)備號及文件節(jié)點號都是0,則表示它們沒有映射到文件中,這種VMA叫做匿名虛擬內(nèi)存地址(Anonymous Virtual Memory Area)。我們可以看到有兩個區(qū)域分別是堆(Heap)和棧(Stack),它們的大小分別為(0x0122d000-0x0120a000)/1024=140KB和(0x7ffc01c44000-0x7ffc01c23000)/1024=132KB。這兩個VMA幾乎在所有的進程中存在,我們在C語言程序里面最常用的malloc()內(nèi)存分配函數(shù)就是從堆里面分配的,堆由系統(tǒng)庫管理。棧一般也叫做堆棧,每個線程都有屬于自己的堆棧,對于單線程的程序來講,這個VMA堆棧就全都歸它使用。另外有一個很特殊的VMA叫做”vdso”,它的地址已經(jīng)位于內(nèi)核空間了,事實上它是一個內(nèi)核的模塊,進程可以通過訪問這個VMA來跟內(nèi)核進行一些通信。
進程虛擬地址空間的概念:操作系統(tǒng)通過給進程空間劃分出一個個VMA來管理進程的虛擬空間;基本原則是將相同權(quán)限屬性的、有相同映像文件的映射成一個VMA;一個進程基本上可以分為如下幾種VMA區(qū)域:(1). 代碼VMA,權(quán)限只讀、可執(zhí)行;有映像文件。(2). 數(shù)據(jù)VMA,權(quán)限可讀寫、可執(zhí)行;有映像文件。(3). 堆VMA,權(quán)限可讀寫、可執(zhí)行;無映像文件,匿名,可向上擴展。(4). 棧VMA,權(quán)限可讀寫、不可執(zhí)行;無映像文件,匿名,可向下擴展。當我們在討論進程虛擬空間的”Segment”的時候,基本上就是指上面的幾種VMA。
堆的最大申請數(shù)量:32位,Linux下虛擬地址空間分給進程本身的是3GB(Windows默認是2GB),一般程序中使用malloc()函數(shù)進行地址空間的申請,那么malloc的最大申請數(shù)量會受到操作系統(tǒng)版本、程序本身大小、用到的動態(tài)/共享庫數(shù)量大小、程序棧數(shù)量大小等,甚至有可能每次最大可申請數(shù)量都會不同,因為有些操作系統(tǒng)使用了一種叫做隨機地址空間分布的技術(shù)(主要是出于安全考慮,防止程序受惡意攻擊),使得進程的堆空間變小。
段地址對齊:可執(zhí)行文件最終是要被操作系統(tǒng)裝載運行的,這個裝載的過程一般是通過虛擬內(nèi)存的頁映射機制完成的。在映射過程中,頁是映射的最小單位。對于Intel 80x86系列處理器來說,默認的頁大小為4096字節(jié),也就是說,我們要映射將一段物理內(nèi)存和進程虛擬地址空間之間建立映射關(guān)系,這段內(nèi)存空間的長度必須是4096的整數(shù)倍,并且這段空間在物理內(nèi)存和進程虛擬地址空間中的起始地址必須是4096的整數(shù)倍。由于有著長度和起始地址的限制,對于可執(zhí)行文件來說,它應(yīng)該盡量地優(yōu)化自己的空間和地址的安排,以節(jié)省空間。在ELF文件中,對于任何一個可裝載的”Segment”,它的p_vaddr除以對齊屬性的余數(shù)等于p_offset除以對齊屬性的余數(shù)。
進程棧初始化:進程剛開始啟動的時候,須知道一些進程運行的環(huán)境,最基本的就是系統(tǒng)環(huán)境變量和進程的運行參數(shù)。很常見的一種做法是操作系統(tǒng)在進程啟動前將這些信息提前保存到進程的虛擬空間的棧中(也就是VMA中的Stack VMA)。
5. Linux內(nèi)核裝載ELF過程簡介
當我們在Linux系統(tǒng)的bash下輸入一個命令執(zhí)行某個ELF程序時,首先在用戶層面,bash進程會調(diào)用fork()系統(tǒng)調(diào)用創(chuàng)建一個新的進程,然后新的進程調(diào)用execve()系統(tǒng)調(diào)用執(zhí)行指定的ELF文件,原先的bash進程繼續(xù)返回等待剛才啟動的新進程結(jié)束,然后繼續(xù)等待用戶輸入命令。execve()系統(tǒng)調(diào)用被聲明在/usr/include/unistd.h中。Glibc對execve()系統(tǒng)調(diào)用進行了包裝,提供了execl()、execlp()、execle()、execv()和execvp()等5個不同形式的exec系列API,它們只是在調(diào)用的參數(shù)形式上有所區(qū)別,但最終都會調(diào)用到execve()這個系統(tǒng)中。
在進入execve()系統(tǒng)調(diào)用之后,Linux內(nèi)核就開始進行真正的裝載工作。在內(nèi)核中,execve()系統(tǒng)調(diào)用相應(yīng)的入口是sys_execve(),sys_execve()進行一些參數(shù)的檢查復制之后,調(diào)用do_execve()。do_execve()會首先查找被執(zhí)行的文件,如果找到文件,則讀取文件的前128個字節(jié),目的是判斷文件的格式,每種可執(zhí)行文件的格式的開頭幾個字節(jié)都是很特殊的,特別是開頭4個字節(jié),常常被稱做魔數(shù)(Magic Number),通過對魔數(shù)的判斷可以確定文件的格式和類型。比如ELF的可執(zhí)行文件格式的頭4個字節(jié)為0x7F、’E’、’L’、’F’;而Java的可執(zhí)行文件格式的頭4個字節(jié)為’c’、’a’、’f’、’e’;如果被執(zhí)行的是Shell腳本或perl、python等這種解釋型語言的腳本,那么它的第一行往往是”#!/bin/sh”或”#!/usr/bin/perl”或”#!/usr/bin/python”,這時候前兩個字節(jié)’#’和”!”就構(gòu)成了魔數(shù),系統(tǒng)一旦判斷到這兩個字節(jié),就對后面的字符串進行解析,以確定具體的解釋程序的路徑。
當do_execve()讀取了這128個字節(jié)的文件頭部以后,然后調(diào)用search_binary_handle()去搜索和匹配合適的可執(zhí)行文件裝載處理過程。Linux中所有被支持的可執(zhí)行文件格式都有相應(yīng)的裝載處理過程,search_binary_handle()會通過判斷文件頭部的魔數(shù)確定文件的格式,并且調(diào)用相應(yīng)的裝載處理過程。比如ELF可執(zhí)行文件的裝載處理過程叫做load_elf_binary();a.out可執(zhí)行文件的裝載處理過程叫做load_aout_binary();而裝載可執(zhí)行腳本程序的處理過程叫做load_script()。
load_elf_binary()的主要步驟是:
(1). 檢查ELF可執(zhí)行文件格式的有效性,比如魔數(shù)、程序頭表中段(Segment)的數(shù)量。
(2). 尋找動態(tài)鏈接的”.interp”段,設(shè)置動態(tài)鏈接器路徑。
(3). 根據(jù)ELF可執(zhí)行文件的程序頭表的描述,對ELF文件進行映射,比如代碼、數(shù)據(jù)、只讀數(shù)據(jù)。
(4). 初始化ELF進程環(huán)境,比如進程啟動時EDX寄存器的地址應(yīng)該是DT_FINI的地址。
(5). 將系統(tǒng)調(diào)用的返回地址修改成ELF可執(zhí)行文件的入口點,這個入口點取決于程序的鏈接方式,對于靜態(tài)鏈接的ELF可執(zhí)行文件,這個程序入口就是ELF文件的文件頭中e_entry所指的地址;對于動態(tài)鏈接的ELF可執(zhí)行文件,程序入口點就是動態(tài)鏈接器。
當load_elf_binary()執(zhí)行完畢,返回至do_execve()再返回sys_execve()時,上面的第5步中已經(jīng)把系統(tǒng)調(diào)用的返回地址改成了被裝載的ELF程序的入口地址了。所以當sys_execve()系統(tǒng)調(diào)用從內(nèi)核態(tài)返回到用戶態(tài)時,EIP寄存器直接跳轉(zhuǎn)到了ELF程序的入口地址,于是新的程序開始執(zhí)行,ELF可執(zhí)行文件加載完成。
6. Windows PE的裝載
PE文件的裝載跟ELF有所不同,由于PE文件中,所有段的起始地址都是頁的倍數(shù),段的長度如果不是頁的整數(shù)倍,那么在映射時向上補齊到頁的整數(shù)倍,我們也可以簡單地認為在32位的PE文件中,段的起始地址和長度都是4096字節(jié)的整數(shù)倍。由于這個特點,PE文件的映射過程會比ELF簡單得多,因為它無須考慮如ELF里面諸多段地址對齊之類的問題,雖然這種會浪費一些磁盤和內(nèi)存空間。PE可執(zhí)行文件的段的數(shù)量一般很少,不像ELF中經(jīng)常有十多個”Section”,最后不得不使用”Segment”的概念把它們合并到一起裝載,PE文件中,鏈接器在生產(chǎn)可執(zhí)行文件時,往往將所有的段盡可能地合并,所以一般只有代碼段、數(shù)據(jù)段、只讀數(shù)據(jù)段和BSS等為數(shù)不多的幾個段。
PE里面很常見的術(shù)語叫做RVA(Relative Virtual Address),它表示一個相對虛擬地址,就是相當于文件中的偏移量的東西。它是相對于PE文件的裝載基地址的一個偏移地址。比如,一個PE文件被裝載到虛擬地址(VA)0x00400000,那么一個RVA為0x1000的地址就是0x00401000。每個PE文件在裝載時都會有一個裝載目標地址(Target Address),這個地址就是所謂的基地址(Base Address)。由于PE文件被設(shè)計成可以裝載到任何地址,所以這個基地址并不是固定的,每次裝載時都可能會變化。如果PE文件中的地址都使用絕對地址,它們都要隨著基地址的變化而變化。但是,如果使用RVA這樣一種基于基地址的相對地址,那么無論基地址怎么變化,PE文件中的各個RVA都保持一致。
裝載一個PE可執(zhí)行文件過程:
(1). 先讀取文件的第一個頁,在這個頁中,包含了DOS頭、PE文件頭和段表。
(2). 檢查進程地址空間中,目標地址是否可用,如果不可用,則另外選一個裝載地址。這個問題對于可執(zhí)行文件來說基本不存在,因為它往往是進程第一個裝入的模塊,所以目標地址不太可能被占用。主要是針對DLL文件的裝載而言的。
(3). 使用段表中提供的信息,將PE文件中所有的段一一映射到地址空間中相應(yīng)的位置。
(4). 如果裝載地址不是目標地址,則進行Rebasing。
(5). 裝載所有PE文件所需要的DLL文件。
(6). 對PE文件中的所有導入符號進行解析。
(7). 根據(jù)PE頭中指定的參數(shù),建立初始化棧和堆。
(8). 建立主線程并且啟動進程。
PE文件中,與裝載相關(guān)的主要信息都包含在PE擴展頭(PE Optional Header)和段表。
GitHub:https://github.com/fengbingchun/Messy_Test
總結(jié)
以上是生活随笔為你收集整理的程序员的自我修养--链接、装载与库笔记:可执行文件的装载与进程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: gRPC简介及简单使用(C++)
- 下一篇: 程序员的自我修养--链接、装载与库笔记: