可執行文件只有裝載到內存以后才能被CPU執行
6.1進程虛擬地址空間
程序和進程的區別:
程序:是一個靜態概念,它就是一些預先編譯好的指令和數據集合的一個文件。
進程:是一個動態概念,它是程序運行時的一個過程,很多時候把動態庫叫做運行時。
程序被運行起來以后,它就擁有了獨立的虛擬地址空間。虛擬地址空間大小由CPU的位數決定。
下面使用的CUP都是以32位為主。程序運行的時候,進程只能使用那些操作系統分配給進程的地址。
Linux操作系統將進程的虛擬地址做了如下分配:
6.2 裝載的方式
程序執行時所需要的指令和數據必須在內存中才能夠運行。
最簡單的就是靜態裝入:將程序運行所需要的指令和數據全都裝入內存中。
當當程序所需要內存大於物理內存時,這個時候就要使用動態裝入了:因為程序運行時是有局部性原理的,所以我們將程序最常用的部分駐留在內存中,不太常用的數據存放在磁盤里面。
動態裝入的兩種方法:
- 覆蓋裝入
- 頁映射
6.2.1 覆蓋裝入
覆蓋裝入在沒有發明虛擬存儲之前使用比較廣泛,現在已經幾乎被淘汰。
覆蓋裝入的方法就是程序員在編寫程序的時候必須手工將程序分割成若干塊,然后編寫一個小的輔助代碼來管理這些模塊何時應該駐留內存何時應該被替換掉。
如果程序有多個模塊,程序員需要手工將模塊按照它們之間的調用依賴關系組織成樹狀結構。
6.2.2 頁映射
頁映射是虛擬存儲機制的一部分,它隨着虛擬存儲的發明而誕生。
頁映射就是將內存和所有磁盤中的數據和指令按照頁為單位划分成若干個頁,以后所有的轉載和操作的單位就是頁。硬件規定頁的大小有4096字節、8192字節、2MB、4MB等。
6.3 從操作系統的角度看可執行文件的裝載
在動態裝載中,可執行文件中的頁可能被裝入內存中的任意頁。
6.3.1 進程的建立
從操作系統的角度看,一個進程最關鍵的特征是它擁有獨立的虛擬地址空間,這使得它有別於其他進程。
程序創建的最通常情況:創建一個進程,然后裝載相應的可執行文件並且執行。
在有虛擬存儲的情況下,上述過程只需要要做三件事:
- 創建一個獨立的虛擬地址空間
- 讀取可執行文件頭,並且建立虛擬空間與可執行文件的映射關系
- 將CPU的指令寄存器設置成可執行文件的入口地址,啟動運行
首先創建虛擬地址空間:創建一個虛擬地址空間並不是創建空間而是創建映射函數所需要的數據結構。
讀取可執行文件頭,並且建立虛擬空間與可執行文件的映射關系:當程序發生頁錯誤的時候,操作系統將從物理內存中分配一個物理頁,然后將該”缺頁”從磁盤中讀取到內存中,再設置缺頁的虛擬頁和物理頁的映射關系。當操作系統捕獲到缺頁錯誤時,它應知道程序當前所需要的頁在可執行文件中的哪一個位置。這就是虛擬空間與可執行文件之間的映射關系。Linux中將進程虛擬空間中的一個段叫做虛擬內存區域,在Windows中將這個叫做”虛擬段”。
將CPU指令寄存器設置成可執行文件入口,啟動運行:操作系統通過設置CPU的指令寄存器將控制權交給進程,由此進程開始執行。
6.3.2 頁錯誤
當CPU開始打算執行一個地址指令時,發現頁面是空頁面,於是它認為這是一個頁錯誤。CPU將控制權交給操作系統,操作系統將查詢第二步建立的數據結構,然后找到VMA,計算出相應的頁面在可執行文件中的偏移,然后在物理內存中分配一個物理頁面,將進程中該虛擬頁與分配的物理頁之間建立映射關系,然后把控制權交回給進程,進程從剛才錯誤頁位置重新開始執行。
6.4 進程虛存空間分布
6.4.1 ELF文件鏈接視圖和執行視圖
前面的例子的可執行文件只有一個代碼段,所有它被操作系統轉載至進程地址空間之后,相對應的只有一個VMA。
當段的數量增多時,就會產生空間浪費問題。為了避免這種問題,可以把相同的段合並。
操作系統至關心段的權限(可讀,可寫,可執行)。
ELF文件中,段的權限基本是三種:
- 以代碼段為代表的權限可讀可執行的段
- 以數據段和BSS段為代碼的權限為可讀可寫的段
- 以只讀數據為代表的權限為只讀的段
一個簡單的方案:對於相同權限的段,把它們合並到一起當作一個段進行映射。
一個”Segment”包含一個或多個屬性類似的”Section”。
從鏈接的角度看,ELF文件是按”Section”存儲的,從裝載的角度看ELF文件又可以按照”Segment”划分。
“Section”和”Segment”是從不同角度來划分同一個ELF文件的。這個在ELF種被稱為視圖,從”Section”的角度來看ELF文件就是鏈接視圖,從”Segment”的角度來看就是執行視圖。
ELF可執行文件中有一個專門的數據結構叫做程序頭表用來保存”Segment”信息。
數據段和BSS段唯一區別就是:數據段從文件中初始化內容,而BSS段的內容全都初始化為0。
6.4.2 堆和棧
操作系統通過使用VMA來對進程的地址空間進行管理。進程在執行的時候它還需要用到棧、堆等空間。一個進程中的棧和堆分別對應一個VMA。
進程虛擬地址空間的概念:操作系統通過給進程空間划分一個個VMA來管理進程的虛擬空間;基本原則就是將相同權限屬性的、有相同映像文件的映射成一個VMA;一個進程基本上可以分為幾種VMA區域:
- 代碼VMA,權限只讀、可執行;有映像文件。
- 數據VMA,權限可讀寫、可執行;有映像文件。
- 堆VMA,權限可讀寫、可執行;無映像文件,匿名,可向上擴展。
- 棧VMA,權限可讀寫、不可執行;無映像文件,匿名,可向下擴展。
6.4.3 堆的最大申請數量
理論上Linum下虛擬地址空間分給進程本身的是3GB,win是2GB,但實際上,Linux分配的程序的最大內存是2.9GB,win分配的為1.5GB。
有些操作系統使用了一種叫做隨機地址空間分布技術,使得進程的堆空間變小。
6.4.4 段地址對齊
可執行文件最終是要被操作系統裝載運行的,這個裝載的過程一般是通過虛擬內存頁映射機制完成。在映射過程中,頁是映射的最小單位。
6.4.5 進程棧初始化
進程剛開始啟動的時候,須知道一些進程運行的環境,最基本的就是系統環境變量和進程的運行參數,很常見的一種做法是操作系統在進程啟動前將這些信息提前保存到進程的虛擬空間的棧中。
6.5 Linux內核裝載ELF過程簡介
首先在用戶層面,bash進程會調用fork()系統調用創建一個新的進程,然后新的進程調用execve()系統調用執行指定的ELF文件,原先的bash進程繼續返回等待剛才啟動的新進程結束,然后繼續等待用戶輸入命令。
6.6 Windows PE的裝載
RVA:它表示一個相對虛擬地址,相當於文件中的偏移量的東西。它是相對於PE文件的裝載基地址的一個偏移地址。每個PE文件在裝載時都會有一個裝載目標地址,這個地址就是基地址。
裝載一個PE可執行文件的過程:
- 先讀取文件的第一個頁,這個頁包含了DOS頭、PE文件頭、段表
- 檢查進程地址空間中,目標地址是否可用,如果不可用,則另外選一個轉載地址
- 使用段表中提供的信息,將PE文件中所有段一一映射到地址空間中相應的位置
- 如果裝載地址不是目標地址,則進行Rebasing
- 裝載所有PE文件所需要的DLL文件
- 對PE文件中所有導入符號進行解析
- 根據PE頭中指定的參數,建立初始化棧和堆
- 建立主線程並且啟動進程
PE文件中,與裝載相關的主要信息都包含在PE擴展頭和段表中。