介紹ELF文件在Linux下的裝載過程,探尋可執行文件裝載的本質git
32位硬件平臺決定了虛擬地址空間的地址爲0 到2^32 -1,即0x00000000 ~ 0xFFFFFFFF
,也就是4GB的虛擬空間大小;而63位的硬件平臺具備64位尋址能力,它的虛擬地址空間達到了2^64 字節,即0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF
,總共17179869184GB
。程序員
而從程序的角度看,C語言中的指針所佔空間可用於計算虛擬地址空間的大小,通常狀況下,C語言指針大小的位數與虛擬空間的位數相同,如32位平臺下的指針爲32位,4字節。github
如下以32爲地址空間爲主,64位做爲擴展。算法
默認狀況下Linux系統將進程的虛擬地址空間做以下分配:windows
其中的操做系統使用的空間,進程是不被容許訪問的,且進程並不能徹底使用剩下的3GB虛擬空間,其中一部分是預留給其餘用途的。bash
Linux下Intel在1995年的Pentium Pro CPU便開始使用36位的物理地址,便可以訪問64GB的物理內存。這時,操做系統只能有4GB的虛擬地址空間,沒法所有讀取完64GB的物理內存,而PAE就是爲了解決這個問題出現的。數據結構
PAE(Physical Address Extension)是一種地址擴展方式,Inter修改了頁映射的方式後,使得新的映射方式能夠訪問更多的物理內存。操做系統提供一個窗口映射的方法,將額外的內存映射進地址空間中。應用程序根據須要選擇申請和映射。好比應用程序中的0x10000000~0x20000000這一段256MB的虛擬地址空間做爲窗口,程序從高於4GB的物理空間中申請多個大小爲256MB的物理空間,編號爲A,B,C,而後根據須要將窗口映射到不一樣的物理空間塊,用到A時將0x10000000~0x20000000映射到A,用到B,C時在映射過去,如此重複。在Windows下,這種內存操做方式爲AWE(Address Windows Extensions)。像Linux等UNIX系統則採用mmp()系統調用來實現。app
沒有發明虛擬存儲以前使用得比較普遍,現已幾乎被淘汰。函數
覆蓋裝入的方法把挖掘內存潛力的任務交給了程序員程序員在編寫程序時必須手動將程序分割成若干塊,而後編寫小的輔助代碼管理這些模塊什麼時候駐留在內存,什麼時候被替換。這個輔助代碼被稱爲覆蓋管理器(Overlay Manager),好比下圖ui
模塊A與B之間相互沒有調用依賴關係,所以兩模塊共享內存區域,當
使用A時則覆蓋該內存,使用B時覆蓋該內存,覆蓋管理器則做爲常駐內存。
多模塊則以下,程序員須要手工將模塊按照它們之間的調用依賴關係組織成樹狀結構。
所以覆蓋管理器須要保證一下亮點。
樹狀結構中從任何一個模塊到樹的根(main)都叫調用路徑,當模塊被調用時,這個調用路徑上的模塊必須在內存之中。好比C模塊正在執行時,B和main都須要在內存中,確保E執行完畢後能正確返回到模塊B和main。
禁止跨樹間調用
任意模塊不容許跨樹狀結構進行調用,好比A不能夠調用B,E,F。但不少時候兩個模塊都依賴於同一個模塊,如模塊E和模塊C須要另一個模塊G,則最方便的方法就是把模塊G併入到main模塊中,這樣G就在E和C的調用路徑上了。
頁映射是虛擬存儲機制的一部分,隨着虛擬存儲的發明而誕生。
頁映射將內存和全部磁盤中的數據及指令按照**頁(Page)**爲單位劃分若干頁,之後全部的裝載和操做單位就是頁。
假設程序全部的指令和數據總共32KB,那麼程序被分爲8頁,並編號P0~P7。但16KB內存沒法將32KB程序裝入,此時將按照動態裝入的原理進行裝入過程。若是程序執行入口在P0則裝載管理器發現程序的P0不在內存中,則將內存F0分配給P0,並將P0的內存扎un購入F0中。運行後使用P5,則將P4裝入F1,以此類推,以下所示:
若是程序繼續運行須要訪問P4,則裝載管理器必須選擇放棄目前正在使用的4哥內存頁中的其中一個來裝載P4,放棄的算法有不少,如:
等算法。而這裏所謂的裝載管理器就是現代的操做系統,準確說影視就是操做系統的存儲管理器。
進程關鍵特徵在於它擁有獨立的虛擬地址空間。一個程序被執行,每每在最開始時須要作三件事:
建立獨立的虛擬地址空間
即建立映射函數所須要的相應的數據結構,而在i386的Linux下,建立虛擬地址空間實際上只是分配一個頁目錄,甚至不須要設置映射關係。也就是完成虛擬空間到物理內存的映射關係。
讀取可執行文件頭,創建虛擬空間與可執行文件的映射關係
完成虛擬空間與可執行文件的映射關係,這一步是整個裝載過程當中最重要的一步,也就是傳統意義上的「裝載」。
如圖,考慮最簡單的例子,虛擬地址如圖,文件大小爲0x000e1,對齊爲0x1000。因爲.text段大小不到0x1000,所以須要對齊。
這種映射關係是保存在操做系統內部的一個數據結構。Linux將進程虛擬空間中的一個段叫作虛擬內存區域(VMA.Virtual Memory Area)。windows叫作虛擬段(Virtual Section)。在上面例子中,會在進程相應的數據結構中設置有一個.text段的VMA,它在虛擬空間中的地址爲0x08048000~0x08049000
對應ELF文件中偏移爲0的.text,屬性爲只讀。
將CPU的指令寄存器設置成可執行文件的入口地址,啓動運行
上述步驟執行完後,只是經過可執行文件頭部信息創建起可執行文件和進程虛存之間的映射關係,並無將可執行文件的指令和數據裝入內存。
假設在上面的例子中,程序入口地址爲0x08048000
,恰好是.text段的其實地址,CPU打算執行時發現爲空頁面時,便認爲這是個頁錯誤(Page Fault)。CPU將控制權交給操做系統,操做系統將查詢上面說到的數據結構,找到空頁面所在VMA,計算出相應的頁面在可執行文件中的偏移,再在物理內存中分配一個物理頁面,將進程中該虛擬頁與分配的物理頁之間創建映射關係,再把控制權還給進程,進程從剛纔頁錯誤的位置從新開始執行,以下圖所示,爲可執行文件,進程虛存與物理內存之間的關係:
在實際場景裏面,ELF文件段數量是比較多的,但因爲須要進行頁對齊等操做,若是以一個段進行頁的分配的話,勢必會形成較大的浪費。
可是在操做系統的角度來看裝載可執行文件(操做系統並不須要知道哪一個段名稱是什麼,做用如何等等信息),發現並不關心可執行文件實際內容,而只是關心跟裝載相關的問題,最主要的就是段的權限問題。
段的權限組合基本是如下三種
所以找到一個以權限種類爲劃分,將相同權限的段合併在一塊兒進行映射的方案,而合併後的數據稱之爲Segment,如.text
段和.init
段合在一塊兒看做爲一個Segment,那麼在裝載時即可以把他們看做一個個總體進行裝載,這樣就能夠達到明顯減小頁面內部碎片化的問題,從而節省空間。
對好比下圖,左邊爲按段裝載,又邊爲合併後按Segment裝載
下面編寫一個例子程序:
SectionMapping.c
#include <stdlib.h>
int main(){
while(1){
sleep(1000);
}
return 0;
}
gcc -static SectionMapping.c -o SectionMapping.elf
複製代碼
而後再使用readelf
readelf -S SectionMapping.elf
複製代碼
獲得以下信息
There are 33 section headers, starting at offset 0xc4d50:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .note.ABI-tag NOTE 0000000000400190 00000190
0000000000000020 0000000000000000 A 0 0 4
[ 2] .note.gnu.build-i NOTE 00000000004001b0 000001b0
0000000000000024 0000000000000000 A 0 0 4
readelf: Warning: [ 3]: Link field (0) should index a symtab section.
[ 3] .rela.plt RELA 00000000004001d8 000001d8
00000000000001f8 0000000000000018 AI 0 24 8
[ 4] .init PROGBITS 00000000004003d0 000003d0
0000000000000017 0000000000000000 AX 0 0 4
[ 5] .plt PROGBITS 00000000004003e8 000003e8
00000000000000a8 0000000000000000 AX 0 0 8
[ 6] .text PROGBITS 0000000000400490 00000490
00000000000874fc 0000000000000000 AX 0 0 16
[ 7] __libc_freeres_fn PROGBITS 0000000000487990 00087990
00000000000024e9 0000000000000000 AX 0 0 16
[ 8] __libc_thread_fre PROGBITS 0000000000489e80 00089e80
00000000000003d7 0000000000000000 AX 0 0 16
[ 9] .fini PROGBITS 000000000048a258 0008a258
0000000000000009 0000000000000000 AX 0 0 4
[10] .rodata PROGBITS 000000000048a280 0008a280
000000000001bd7c 0000000000000000 A 0 0 32
[11] __libc_subfreeres PROGBITS 00000000004a6000 000a6000
0000000000000048 0000000000000000 A 0 0 8
[12] __libc_IO_vtables PROGBITS 00000000004a6060 000a6060
00000000000006a8 0000000000000000 A 0 0 32
[13] __libc_atexit PROGBITS 00000000004a6708 000a6708
0000000000000008 0000000000000000 A 0 0 8
[14] .stapsdt.base PROGBITS 00000000004a6710 000a6710
0000000000000001 0000000000000000 A 0 0 1
[15] __libc_thread_sub PROGBITS 00000000004a6718 000a6718
0000000000000010 0000000000000000 A 0 0 8
[16] .eh_frame PROGBITS 00000000004a6728 000a6728
0000000000009c38 0000000000000000 A 0 0 8
[17] .gcc_except_table PROGBITS 00000000004b0360 000b0360
0000000000000085 0000000000000000 A 0 0 1
[18] .tdata PROGBITS 00000000006b0b40 000b0b40
0000000000000020 0000000000000000 WAT 0 0 8
[19] .tbss NOBITS 00000000006b0b60 000b0b60
0000000000000040 0000000000000000 WAT 0 0 8
[20] .init_array INIT_ARRAY 00000000006b0b60 000b0b60
0000000000000010 0000000000000008 WA 0 0 8
[21] .fini_array FINI_ARRAY 00000000006b0b70 000b0b70
0000000000000010 0000000000000008 WA 0 0 8
[22] .data.rel.ro PROGBITS 00000000006b0b80 000b0b80
0000000000000464 0000000000000000 WA 0 0 32
[23] .got PROGBITS 00000000006b0fe8 000b0fe8
0000000000000008 0000000000000008 WA 0 0 8
[24] .got.plt PROGBITS 00000000006b1000 000b1000
00000000000000c0 0000000000000008 WA 0 0 8
[25] .data PROGBITS 00000000006b10c0 000b10c0
0000000000001af0 0000000000000000 WA 0 0 32
[26] .bss NOBITS 00000000006b2bc0 000b2bb0
0000000000001718 0000000000000000 WA 0 0 32
[27] __libc_freeres_pt NOBITS 00000000006b42d8 000b2bb0
0000000000000028 0000000000000000 WA 0 0 8
[28] .comment PROGBITS 0000000000000000 000b2bb0
0000000000000025 0000000000000001 MS 0 0 1
[29] .note.stapsdt NOTE 0000000000000000 000b2bd8
0000000000001408 0000000000000000 0 0 4
[30] .symtab SYMTAB 0000000000000000 000b3fe0
000000000000a6c8 0000000000000018 31 693 8
[31] .strtab STRTAB 0000000000000000 000be6a8
0000000000006532 0000000000000000 0 0 1
[32] .shstrtab STRTAB 0000000000000000 000c4bda
0000000000000176 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
l (large), p (processor specific)
複製代碼
查看ELF的Segment
信息,正如稱Section屬性的結構叫作段表,描述Segment的結構爲程序頭(Program Header),它描述ELF文件該如何被操做系統映射進進程的虛擬空間:
readelf -l SectionMapping.elf
複製代碼
結果以下:
Elf file type is EXEC (Executable file)
Entry point 0x400a00
There are 6 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000b03e5 0x00000000000b03e5 R E 0x200000
LOAD 0x00000000000b0b40 0x00000000006b0b40 0x00000000006b0b40
0x0000000000002070 0x00000000000037c0 RW 0x200000
NOTE 0x0000000000000190 0x0000000000400190 0x0000000000400190
0x0000000000000044 0x0000000000000044 R 0x4
TLS 0x00000000000b0b40 0x00000000006b0b40 0x00000000006b0b40
0x0000000000000020 0x0000000000000060 R 0x8
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x00000000000b0b40 0x00000000006b0b40 0x00000000006b0b40
0x00000000000004c0 0x00000000000004c0 R 0x1
Section to Segment mapping:
Segment Sections...
00 .note.ABI-tag .note.gnu.build-id .rela.plt .init .plt .text __libc_freeres_fn __libc_thread_freeres_fn .fini .rodata __libc_subfreeres __libc_IO_vtables __libc_atexit .stapsdt.base __libc_thread_subfreeres .eh_frame .gcc_except_table
01 .tdata .init_array .fini_array .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs
02 .note.ABI-tag .note.gnu.build-id
03 .tdata .tbss
04
05 .tdata .init_array .fini_array .data.rel.ro .got
複製代碼
從裝載的角度看,咱們只須要關心兩個「LOAD」類型的Segment,其餘只是在裝載中起輔助做用。這裏能夠看到,文件被從新劃分紅三個部分:
所以全部相同屬性的Section被歸類到了同一個Segment,並映射到同一個VMA裏面。因此說Segment和Section在不一樣的角度給ELF文件進行劃分。
能夠以下圖表示可執行文件的段與進程虛擬空間的映射關係:
ELF可執行文件和共享庫文件有個專門的數據結構叫**程序頭表(Program Header Table)**用來保存Segment信息,由於ELF文件不須要被裝載,所以沒有程序頭表具體結構以下,並與readelf -l
讀出來的數據一一對應
typedef struct{
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
}Elf32_Phdr;
複製代碼
基本含義以下
其中,p_memsz >= p_filesz
但若是p_memsz <= p_filesz
則表示Segment段在內存中分配的空間大小超過文件中實際的大小,這部分多餘的空間則被所有填充爲」0「。這樣咱們構造ELF可執行文件時就不須要再額外設立BSS的Segment了,能夠把數據Segment的p_memsz擴大,那些額外的部分就是BSS。數據段和BSS的區別在於,數據段從文件中初始化內容,而BSS則所有被初始化爲0。所以能夠看到前面的BSS其實已經被併入數據類型段裏面,而沒有顯示出來。
查看進程虛擬空間分佈如圖所示:
其中意義能夠見我另一篇文章,《/proc/{pid}/maps的文件結構解析》
其中,主設備號和次設備號及文件節點號都是0,表示沒有映射到文件,這種VMA叫作匿名虛擬內存區域(Anonymous Virtual Memory Area)。咱們目前關注Heap和Stack這兩個VMA幾乎在全部進程中都存在,malloc()函數內存分配就是從堆裏面進行分配的,堆由系統庫管理,而vsyscall
位於內核空間,具體做用待深究,不過在名稱來看猜想應該是和內核通訊相關的VMA了。
一個進程可主要分以下幾個區域:
具體如圖:
bash進程調用fork()
系統調用建立一個新的進程
調用execve()
系統調用執行指定的ELF文件,原先的bash進程繼續返回等待剛纔啓動的新進程結束,而後等待用戶輸入命令。execve()
被定義在unsitd.h
文件中,原型以下
int execve(const char * filename,char * const argv[ ],char * const envp[ ]);
三個參數分別是被執行的程序文件名,執行參數,環境變量
啊~~~這裏步驟比較多且複雜,沒有實際實驗也看不太懂,就先直接貼圖將就着看吧;
而後最後獲得的結果就是返回地址改爲被裝載的ELF程序入口地址:
至此,可執行文件的裝載部分已介紹完畢