程式設計師的自我修養筆記之裝載

amlloc發表於2019-02-25

可執行檔案的裝載與程式

介紹ELF檔案在Linux下的裝載過程,探尋可執行檔案裝載的本質

  • 什麼是程式的虛擬地址空間
  • 為什麼程式要有自己獨立的虛擬地址空間
  • 幾種裝載方式
  • 程式虛擬地址空間的分佈情況

程式虛擬地址空間

32位硬體平臺決定了虛擬地址空間的地址為0 到2^32 -1,即0x00000000 ~ 0xFFFFFFFF,也就是4GB的虛擬空間大小;而63位的硬體平臺具有64位定址能力,它的虛擬地址空間達到了2^64 位元組,即0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF,總共17179869184GB

而從程式的角度看,C語言中的指標所佔空間可用於計算虛擬地址空間的大小,一般情況下,C語言指標大小的位數與虛擬空間的位數相同,如32位平臺下的指標為32位,4位元組。

以下以32為地址空間為主,64位作為擴充套件。

預設情況下Linux系統將程式的虛擬地址空間作如下分配:

1550220847534

其中的作業系統使用的空間,程式是不被允許訪問的,且程式並不能完全使用剩下的3GB虛擬空間,其中一部分是預留給其他用途的。

1550220996919

PAE

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()系統呼叫來實現

裝載方式

覆蓋裝入

沒有發明虛擬儲存之前使用得比較廣泛,現已幾乎被淘汰。

覆蓋裝入的方法把挖掘記憶體潛力的任務交給了程式設計師程式設計師在編寫程式時必須手動將程式分割成若干塊,然後編寫小的輔助程式碼管理這些模組何時駐留在記憶體,何時被替換。這個輔助程式碼被稱為覆蓋管理器(Overlay Manager),比如下圖

1550223485233

模組A與B之間相互沒有呼叫依賴關係,因此兩模組共享記憶體區域,當

使用A時則覆蓋該記憶體,使用B時覆蓋該記憶體,覆蓋管理器則作為常駐記憶體。

多模組則如下,程式設計師需要手工將模組按照它們之間的呼叫依賴關係組織成樹狀結構

1550223740492

因此覆蓋管理器需要保證一下亮點。

  • 樹狀結構中從任何一個模組到樹的根(main)都叫呼叫路徑,當模組被呼叫時,這個呼叫路徑上的模組必須在記憶體之中。比如C模組正在執行時,B和main都需要在記憶體中,確保E執行完畢後能正確返回到模組B和main。

  • 禁止跨樹間呼叫

    任意模組不允許跨樹狀結構進行呼叫,比如A不可以呼叫B,E,F。但很多時候兩個模組都依賴於同一個模組,如模組E和模組C需要另外一個模組G,則最方便的方法就是把模組G併入到main模組中,這樣G就在E和C的呼叫路徑上了。

頁對映

頁對映是虛擬儲存機制的一部分,隨著虛擬儲存的發明而誕生。

頁對映將記憶體和所有磁碟中的資料及指令按照**頁(Page)**為單位劃分若干頁,以後所有的裝載和操作單位就是頁。

1550224180737

假設程式所有的指令和資料總共32KB,那麼程式被分為8頁,並編號P0~P7。但16KB記憶體無法將32KB程式裝入,此時將按照動態裝入的原理進行裝入過程。如果程式執行入口在P0則裝載管理器發現程式的P0不在記憶體中,則將記憶體F0分配給P0,並將P0的記憶體扎un購入F0中。執行後使用P5,則將P4裝入F1,以此類推,如下所示:

1550224497404

如果程式繼續執行需要訪問P4,則裝載管理器必須選擇放棄目前正在使用的4哥記憶體頁中的其中一個來裝載P4,放棄的演算法有很多,如:

  • FIFO先進先出,則放棄F0,P4裝入F0
  • LUR最少使用,則放棄F2,P4裝入F2

等演算法。而這裡所謂的裝載管理器就是現代的作業系統,準確說影視就是作業系統的儲存管理器。

從作業系統角度看可執行檔案的裝載並在程式中執行

程式的建立

程式關鍵特徵在於它擁有獨立的虛擬地址空間。一個程式被執行,往往在最開始時需要做三件事:

  • 建立獨立的虛擬地址空間

    即建立對映函式所需要的相應的資料結構,而在i386的Linux下,建立虛擬地址空間實際上只是分配一個頁目錄,甚至不需要設定對映關係。也就是完成虛擬空間到實體記憶體的對映關係。

  • 讀取可執行檔案頭,建立虛擬空間與可執行檔案的對映關係

    完成虛擬空間與可執行檔案的對映關係,這一步是整個裝載過程中最重要的一步,也就是傳統意義上的“裝載”。

    如圖,考慮最簡單的例子,虛擬地址如圖,檔案大小為0x000e1,對齊為0x1000。由於.text段大小不到0x1000,因此需要對齊。

    1550650714552

    這種對映關係是儲存在作業系統內部的一個資料結構。Linux將程式虛擬空間中的一個段叫做虛擬記憶體區域(VMA.Virtual Memory Area)。windows叫做虛擬段(Virtual Section)。在上面例子中,會在程式相應的資料結構中設定有一個.text段的VMA,它在虛擬空間中的地址為0x08048000~0x08049000對應ELF檔案中偏移為0的.text,屬性為只讀。

  • 將CPU的指令暫存器設定成可執行檔案的入口地址,啟動執行

頁錯誤

上述步驟執行完後,只是通過可執行檔案頭部資訊建立起可執行檔案和程式虛存之間的對映關係,並沒有將可執行檔案的指令和資料裝入記憶體。

假設在上面的例子中,程式入口地址為0x08048000,剛好是.text段的其實地址,CPU打算執行時發現為空頁面時,便認為這是個頁錯誤(Page Fault)。CPU將控制權交給作業系統,作業系統將查詢上面說到的資料結構,找到空頁面所在VMA,計算出相應的頁面在可執行檔案中的偏移,再在實體記憶體中分配一個物理頁面,將程式中該虛擬頁與分配的物理頁之間建立對映關係,再把控制權還給程式,程式從剛才頁錯誤的位置重新開始執行,如下圖所示,為可執行檔案,程式虛存與實體記憶體之間的關係:

1550995358009

程式虛存空間分佈

ELF檔案連結檢視和執行檢視

在實際場景裡面,ELF檔案段數量是比較多的,但由於需要進行頁對齊等操作,如果以一個段進行頁的分配的話,勢必會造成較大的浪費。

但是在作業系統的角度來看裝載可執行檔案(作業系統並不需要知道哪個段名稱是什麼,作用如何等等資訊),發現並不關心可執行檔案實際內容,而只是關心跟裝載相關的問題,最主要的就是段的許可權問題。

段的許可權組合基本是以下三種

  • 以程式碼段為代表的可讀可執行段
  • 以資料段和BSS段為代表的可讀可寫段
  • 以只讀資料段為代表的許可權為只讀的段

因此找到一個以許可權種類為劃分,將相同許可權的段合併在一起進行對映的方案,而合併後的資料稱之為Segment,如.text段和.init段合在一起看作為一個Segment,那麼在裝載時便可以把他們看作一個個整體進行裝載,這樣就可以達到明顯減少頁面內部碎片化的問題,從而節省空間。

對比如下圖,左邊為按段裝載,又邊為合併後按Segment裝載

1550997079148

下面編寫一個例子程式:

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,其他只是在裝載中起輔助作用。這裡可以看到,檔案被重新劃分成三個部分:

  • 可讀可執行LOAD段
  • 可讀寫的LOAD段
  • 沒有被對映的段

因此所有相同屬性的Section被歸類到了同一個Segment,並對映到同一個VMA裡面。所以說Segment和Section在不同的角度給ELF檔案進行劃分。

  • 連結檢視:從Section的角度進行劃分
  • 執行檢視:從Segment的角度進行劃分

可以如下圖表示可執行檔案的段與程式虛擬空間的對映關係:

1550998021300

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;
複製程式碼

基本含義如下

1550998603509

其中,p_memsz >= p_filesz但如果p_memsz <= p_filesz則表示Segment段在記憶體中分配的空間大小超過檔案中實際的大小,這部分多餘的空間則被全部填充為”0“。這樣我們構造ELF可執行檔案時就不需要再額外設立BSS的Segment了,可以把資料Segment的p_memsz擴大,那些額外的部分就是BSS。資料段和BSS的區別在於,資料段從檔案中初始化內容,而BSS則全部被初始化為0。因此可以看到前面的BSS其實已經被併入資料型別段裡面,而沒有顯示出來。

堆和棧

檢視程式虛擬空間分佈如圖所示:

1550999813925

其中意義可以見我另外一篇文章,《/proc/{pid}/maps的檔案結構解析》

其中,主裝置號和次裝置號及檔案節點號都是0,表示沒有對映到檔案,這種VMA叫做匿名虛擬記憶體區域(Anonymous Virtual Memory Area)。我們目前關注Heap和Stack這兩個VMA幾乎在所有程式中都存在,malloc()函式記憶體分配就是從堆裡面進行分配的,堆由系統庫管理,而vsyscall位於核心空間,具體作用待深究,不過在名稱來看猜測應該是和核心通訊相關的VMA了。

一個程式可主要分如下幾個區域:

  • 程式碼VMA,許可權只讀,可執行;有映像檔案
  • 資料VMA,許可權可讀寫;有映像檔案
  • 堆VMA,可讀可寫不可執行;匿名,可向上擴充套件
  • 棧VMA,可讀可寫不可執行;匿名,可向下擴充套件

具體如圖:

1551000603318

Linux核心裝載ELF過程

  • bash程式呼叫fork()系統呼叫建立一個新的程式

  • 呼叫execve()系統呼叫執行指定的ELF檔案,原先的bash程式繼續返回等待剛才啟動的新程式結束,然後等待使用者輸入命令。execve()被定義在unsitd.h檔案中,原型如下

    int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

    三個引數分別是被執行的程式檔名,執行引數,環境變數

啊~~~這裡步驟比較多且複雜,沒有實際實驗也看不太懂,就先直接貼圖將就著看吧;

1551002209326

1551002228233

然後最後得到的結果就是返回地址改成被裝載的ELF程式入口地址:

  • 靜態連結:ELF檔案的標頭檔案中e_entry所指的地址
  • 動態連結:動態連結器地址

至此,可執行檔案的裝載部分已介紹完畢

相關文章