1. 程式人生 > >連結裝載與庫 第6章 可執行檔案的裝載與程序

連結裝載與庫 第6章 可執行檔案的裝載與程序

可執行檔案的裝載與程序

在第一章中講到,程式直接使用實體記憶體地址有以下缺點:

  1. 地址空間不隔離。惡意程式可以很容易的改寫其他程式的資料。
  2. 記憶體使用效率低。一個程式需要執行時,需要將整個程式裝入記憶體之中。
  3. 程式執行地址的不確定。因為無法保證每次都將程式載入到相同的地址,會涉及到程式重定位的問題。

解決方法是使用分頁的方式:
作業系統將記憶體分成大小固定的頁(最常用的頁大小為4kb)。程序使用虛擬地址,虛擬地址也按頁分割。由作業系統負責將虛擬地址的頁對映到實體地址。
cpu-(virtual address)->MMU-(physical address)->Physical memory
疑問:
既然所有程序都使用虛擬地址,那麼cpu建起一樣的虛擬地址,是誰儲存了額外的資訊,將不同程序的虛擬地址對映到正確的實體地址?
大致理解:
核心記憶體管理模組儲存了額外的資訊。cpu在執行某個程序時,核心負責將MMU的對應的暫存器修改為正確的值,確保MMU將虛擬地址與暫存器的值結合起來得到正確的實體地址。

6.1 程序虛擬地址空間

每個程序擁有獨立的虛擬地址空間。虛擬地址空間的大小由CPU的位數確定。32位cpu的最大虛擬地址空間為2^32位元組。
並且這部分虛擬空間還有一部分是給作業系統預留的,應用程式訪問此部分地址為非法操作。
疑問:
既然是虛擬地址,為什麼還需要給作業系統預留呢?這不是相當於每個程序都給作業系統預留了部分空間???

PAE
在32位的CPU下,程式無法使用超過4GB的虛擬地址空間。但是卻有辦法使用超過4GB的物理空間。常用的方法叫視窗對映,在windows下叫做AWE(address windowing extensions),linux採用mmap()系統呼叫來實現。
以下理解參考:

實體地址擴充套件
實體地址擴充套件(PAE)分頁機制

虛擬地址對映到實體地址,在傳統的32位保護模式中,x86處理器使用一種兩級的轉換方案。
cr3暫存器指向一個4KB大的頁目錄。頁目錄包含1024個4KB大小頁表,每個頁表包含1024個頁。所以共有2^20次方個頁。
每個程序都有一個獨立的頁目錄,因為擁有獨立的頁目錄,從而也擁有了獨立的虛擬地址空間。
在以上方案中,每個地址的大小都是4個位元組,所以最大能使用的實體地址也只有4G。所以32位的cpu,要支援大於4G的實體記憶體,就必須使用PAE。
使用PAE擴充套件之後(設定CR4暫存器的第5位),地址變為8個位元組。頁目錄和頁表的大小沒變,所以表示的項變少為一半。為了解決這個問題,增加了一級:cr3不再指向頁目錄表,而是指向一個大小為4的頁目錄指標表

。(32位元組對齊,所以只需要27位從便足夠表示)
為了定址超過4GB的空間,就需要對cr3設定不同的值。
通過設定cr3不同的值,就可以訪問總共超過4GB大小的物理空間。
只有核心能夠修改程序的頁表,所以使用者態下執行的程序不能使用大於4GB的物理空間。

6.2 裝載的方式

6.2.1 覆蓋裝入

將記憶體管理的工作交給了程式設計師。

  1. 將模組按它們之間的呼叫依賴關係組織成樹狀結構。
  2. 從任何一個模組到樹的根模組叫呼叫路徑
  3. 禁止跨樹間呼叫
    因為子樹間有沒有呼叫依賴關係,所以需要使用的最大記憶體比整個程式實際的記憶體要小。

6.2.2 頁對映

將記憶體和磁碟上的資料都按頁進行劃分(最常見的頁大小為4096)。作業系統不再需要將整個程式載入到記憶體中,而是缺少哪個頁就載入哪個頁。記憶體管理的事情完全由作業系統來完成,程式設計師不需要操心。

6.3 從作業系統看可執行檔案的載入

在使用頁對映的機制中,程式的某個頁被載入到記憶體中的實體地址都是不確定的。如果程式中直接使用實體地址,那麼每一個頁載入之後,都需要對整個程式進行重定位。
現代作業系統使用虛擬地址進行操作,由MMU將虛擬地址對映為實體地址。

6.3.1 程序的建立

從作業系統的角度來看,一個程序最關鍵的特徵是它擁有獨立的虛擬地址空間。
在有虛擬儲存的情況下,建立一個程序最開始只需要做3件事情:

  1. 建立虛擬地址空間。並不是建立空間,而是建立頁對映函式所需要的資料結構,即頁目錄。
  2. 讀取可執行檔案文,並且建立虛擬地址空間與可執行檔案的對映關係。第一步是虛擬空間到實體地址的對映,這一步所做的是虛擬空間與可執行檔案的對映。當程式訪問某個虛擬地址發生缺頁錯誤時,作業系統分配實體記憶體頁,並將實體記憶體頁與虛擬地址對映起來。然後將可執行檔案載入到對應的虛擬地址。顯然,必須要儲存虛擬地址與可執行檔案的對映關係,才能由虛擬地址找到對應的可執行檔案。作業系統具體用什麼結構來儲存這個對映關係呢?書中並沒有提到。
  3. 將cpu指令暫存器設定成可執行檔案入口,啟動執行。

6.3.2 頁錯誤

6.4 程序虛擬空間分佈

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

ELF檔案被對映時,是以系統的頁的長度作為單位。每個段在對映時的長度都應該是系統頁的長度的整數倍。一般ELF可以執行檔案都有十多個段,會造成相當的記憶體浪費。
解決方法就是對於相同許可權的段,把它們合併到一起當作一個段進行對映。
所以ELF檔案引入一個segment的概念,一個segment包含一個或多個屬性類似的section。
連結器在把目標檔案連結成可執行檔案時,會盡量把相同許可權屬性的段分配在同一空間。

示例程式:

/*SectionMapping.c*/
#include <stdlib.h>

int main()
{
    while(1)
    {
        sleep(1000);
    }
    return 0;
}

段表結構:

[email protected]:~# readelf -S SectionMapping.elf
There are 31 section headers, starting at offset 0xb16c4:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .note.ABI-tag     NOTE            080480f4 0000f4 000020 00   A  0   0  4
  [ 2] .note.gnu.build-i NOTE            08048114 000114 000024 00   A  0   0  4
readelf: Warning: [ 3]: Link field (0) should index a symtab section.
  [ 3] .rel.plt          REL             08048138 000138 000078 08  AI  0  23  4
  [ 4] .init             PROGBITS        080481b0 0001b0 000023 00  AX  0   0  4
  [ 5] .plt              PROGBITS        080481e0 0001e0 0000f0 00  AX  0   0 16
  [ 6] .text             PROGBITS        080482d0 0002d0 073644 00  AX  0   0 16
  [ 7] __libc_freeres_fn PROGBITS        080bb920 073920 000a6d 00  AX  0   0 16
  [ 8] __libc_thread_fre PROGBITS        080bc390 074390 00009e 00  AX  0   0 16
  [ 9] .fini             PROGBITS        080bc430 074430 000014 00  AX  0   0  4
  [10] .rodata           PROGBITS        080bc460 074460 01a46c 00   A  0   0 32
  [11] __libc_subfreeres PROGBITS        080d68cc 08e8cc 000028 00   A  0   0  4
  [12] __libc_IO_vtables PROGBITS        080d6900 08e900 000354 00   A  0   0 32
  [13] __libc_atexit     PROGBITS        080d6c54 08ec54 000004 00   A  0   0  4
  [14] __libc_thread_sub PROGBITS        080d6c58 08ec58 000004 00   A  0   0  4
  [15] .eh_frame         PROGBITS        080d6c5c 08ec5c 012c90 00   A  0   0  4
  [16] .gcc_except_table PROGBITS        080e98ec 0a18ec 0000af 00   A  0   0  1
  [17] .tdata            PROGBITS        080eaf5c 0a1f5c 000010 00 WAT  0   0  4
  [18] .tbss             NOBITS          080eaf6c 0a1f6c 000018 00 WAT  0   0  4
  [19] .init_array       INIT_ARRAY      080eaf6c 0a1f6c 000008 04  WA  0   0  4
  [20] .fini_array       FINI_ARRAY      080eaf74 0a1f74 000008 04  WA  0   0  4
  [21] .jcr              PROGBITS        080eaf7c 0a1f7c 000004 00  WA  0   0  4
  [22] .data.rel.ro      PROGBITS        080eaf80 0a1f80 000070 00  WA  0   0 32
  [23] .got.plt          PROGBITS        080eb000 0a2000 000048 04  WA  0   0  4
  [24] .data             PROGBITS        080eb060 0a2060 000f20 00  WA  0   0 32
  [25] .bss              NOBITS          080ebf80 0a2f80 000e0c 00  WA  0   0 32
  [26] __libc_freeres_pt NOBITS          080ecd8c 0a2f80 000018 00  WA  0   0  4
  [27] .comment          PROGBITS        00000000 0a2f80 00002d 01  MS  0   0  1
  [28] .symtab           SYMTAB          00000000 0a2fb0 007c20 10     29 846  4
  [29] .strtab           STRTAB          00000000 0aabd0 00699a 00      0   0  1
  [30] .shstrtab         STRTAB          00000000 0b156a 000159 00      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),
  p (processor specif)

檔案居然多達31個段。
描述section屬性的結構叫做段表,描述segment的結構叫做程式頭。描述ELF檔案該如何被作業系統對映到記憶體空間。

[email protected]:~# readelf -l SectionMapping.elf

Elf file type is EXEC (Executable file)
Entry point 0x804887f
There are 6 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000000 0x08048000 0x08048000 0xa199b 0xa199b R E 0x1000
  LOAD           0x0a1f5c 0x080eaf5c 0x080eaf5c 0x01024 0x01e48 RW  0x1000
  NOTE           0x0000f4 0x080480f4 0x080480f4 0x00044 0x00044 R   0x4
  TLS            0x0a1f5c 0x080eaf5c 0x080eaf5c 0x00010 0x00028 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10
  GNU_RELRO      0x0a1f5c 0x080eaf5c 0x080eaf5c 0x000a4 0x000a4 R   0x1

 Section to Segment mapping:
  Segment Sections...
   00     .note.ABI-tag .note.gnu.build-id .rel.plt .init .plt .text __libc_freeres_fn __libc_thread_freeres_fn .fini .rodata __libc_subfreeres __libc_IO_vtables __libc_atexit __libc_thread_subfreeres .eh_frame .gcc_except_table
   01     .tdata .init_array .fini_array .jcr .data.rel.ro .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 .jcr .data.rel.ro

只有6個segment,其中兩個需要裝載。section到segement的對應關係也補顯示了出來。
從section的角度來看ELF檔案就是連結檢視。從segment的角度來看就是執行檢視。

ELF可執行檔案和共享庫檔案中有一個專門的資料結構叫做程式頭表,用來儲存segment的資訊。因為ELF目標檔案不需要被裝載,所以它沒有程式頭表。
程式頭表是一個結構體陣列:

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;

結構體成員與readelf -l的輸出一一對應。
但是文章沒講到,section與segment的對應關係是儲存在什麼資料結構裡面的。

6.4.2 堆和棧

VMA除了被用來對映可執行檔案的各個Segment外,還用來對程序和地址空間進行管理。
堆和棧都有對應的VMA。可以通過cat /proc/pid/maps檢視。

一個程序基本上可以分為如下幾種VMA區域:
程式碼VMA 許可權只讀,可執行。有對映檔案
資料VMA 許可權可讀寫,可執行。有映像檔案
堆VMA 許可權可讀寫,可執行。無映像檔案,匿名,可向上擴充套件
棧VMA 許可權可讀寫,不可執行 無映像檔案 可向下擴充套件

6.4.3 堆的最大申請數量

用malloc測試,32位機器。作者最大能申請2.9GB左右。
每次執行結果可能不同,因為一些作業系統使用了ASLR技術,使得程序堆空間變小。
但是我在我的虛擬機器上跑這個程式,只能申請到1.9G。

/*mallocTest1.c*/
#include <stdio.h>
#include <stdlib.h>

unsigned int maximum = 0;

int main(void)
{
    unsigned blocksize[] = {1024 * 1024, 1024, 1};
    void *block;
    int i, count;

    for(i = 0; i < 3; i++) {
        for(count = 1; ; count++) {
            block = malloc(maximum + blocksize[i] * count);
            if (block) {
                maximum = maximum + blocksize[i] * count;
                free(block);
            } else {
                break;
            }
        }
    }

    printf("maximum malloc size = %u bytes.\n", maximum);
    printf("maximum malloc size = %f MB\n",((float)maximum)/1024/1024);
    printf("maximum malloc size = %f GB\n",((float)maximum)/1024/1024/1024);
}

測試結果:

maximum malloc size = 2021424851 bytes.
maximum malloc size = 1927.781006 MB
maximum malloc size = 1.882599 GB
[email protected]:~# ./mallocTest
maximum malloc size = 2021432980 bytes.
maximum malloc size = 1927.788696 MB
maximum malloc size = 1.882606 GB
[email protected]:~# ./mallocTest
maximum malloc size = 2021453807 bytes.
maximum malloc size = 1927.808594 MB
maximum malloc size = 1.882626 GB
[email protected]:~# ./mallocTest
maximum malloc size = 2021494727 bytes.
maximum malloc size = 1927.847656 MB
maximum malloc size = 1.882664 GB

6.4.4 段地址對齊

待補充…