連結裝載與庫 第6章 可執行檔案的裝載與程序
可執行檔案的裝載與程序
在第一章中講到,程式直接使用實體記憶體地址有以下缺點:
- 地址空間不隔離。惡意程式可以很容易的改寫其他程式的資料。
- 記憶體使用效率低。一個程式需要執行時,需要將整個程式裝入記憶體之中。
- 程式執行地址的不確定。因為無法保證每次都將程式載入到相同的地址,會涉及到程式重定位的問題。
解決方法是使用分頁的方式:
作業系統將記憶體分成大小固定的頁(最常用的頁大小為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的頁目錄指標表
為了定址超過4GB的空間,就需要對cr3設定不同的值。
通過設定cr3不同的值,就可以訪問總共超過4GB大小的物理空間。
只有核心能夠修改程序的頁表,所以使用者態下執行的程序不能使用大於4GB的物理空間。
6.2 裝載的方式
6.2.1 覆蓋裝入
將記憶體管理的工作交給了程式設計師。
- 將模組按它們之間的呼叫依賴關係組織成樹狀結構。
- 從任何一個模組到樹的根模組叫呼叫路徑
- 禁止跨樹間呼叫
因為子樹間有沒有呼叫依賴關係,所以需要使用的最大記憶體比整個程式實際的記憶體要小。
6.2.2 頁對映
將記憶體和磁碟上的資料都按頁進行劃分(最常見的頁大小為4096)。作業系統不再需要將整個程式載入到記憶體中,而是缺少哪個頁就載入哪個頁。記憶體管理的事情完全由作業系統來完成,程式設計師不需要操心。
6.3 從作業系統看可執行檔案的載入
在使用頁對映的機制中,程式的某個頁被載入到記憶體中的實體地址都是不確定的。如果程式中直接使用實體地址,那麼每一個頁載入之後,都需要對整個程式進行重定位。
現代作業系統使用虛擬地址進行操作,由MMU將虛擬地址對映為實體地址。
6.3.1 程序的建立
從作業系統的角度來看,一個程序最關鍵的特徵是它擁有獨立的虛擬地址空間。
在有虛擬儲存的情況下,建立一個程序最開始只需要做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 段地址對齊
待補充…