通過參考內存布局及訪問機制的相關文章,本文試着整合一下相關知識點,希望能對有需要的朋友提供一點參考。但由於所參考文章皆為網友所作,相關知識並沒有形成系統的認識,所以有些知識點仍然不夠清楚,也難免有謬誤之處。如果大家發現錯誤,敬請指出,另外倘若有關於“系統啟動及內存布局”方面的可靠的資料或書籍,懇請大神留言告知。
參考文章(只列出幾個重要的):http://www.cnblogs.com/clover-toeic/p/3754433.html
http://blog.chinaunix.net/uid-26126915-id-2981205.html
http://blog.csdn.net/yeruby/article/details/39718119
http://www.cnblogs.com/qintangtao/p/3325985.html
http://www.cnblogs.com/wangccc/p/5342300.html
一、內存空間布局
1.1 虛擬地址
隨着圖形界面的興起和用戶需求的增大,內存空間變得容納不下程序了。因此人們將程序分割成許多片段,並由操作系統將這些片段調入內存運行,於是便產生一個虛擬地址的概念,其思想是:操作系統把程序當前使用的部分保留在內存中,而把其他未被使用的部分保存在磁盤上,並在需要切換時將外設中的數據調入內存空間。而程序運行在虛擬地址空間中,並通過相關機制將虛擬地址映射到物理內存。因此內存空間就分為物理存儲空間和虛擬存儲空間。
引入虛擬地址的好處在於:
1、程序可以使用一系列相鄰的虛擬地址來訪問物理內存中不相鄰的大內存緩沖區。
2、程序可以使用一系列虛擬地址來訪問大於可用物理內存的內存緩沖區。當物理內存的供應量變小時,內存管理器會將物理內存頁(通常大小為 4 KB)保存到磁盤文件。數據或代碼頁會根據需要在物理內存與磁盤之間移動。
3、不同進程使用的物理地址由系統進行映射管理,從而避免直接操作內存,防止惡意程序或bug破壞其它內存空間。
1.2 物理空間布局
物理存儲空間布局與處理器相關,詳細情況可以從處理器用戶手冊的存儲空間分布表(memory map)相關章節中查到。
1.3 虛擬空間布局
在多任務操作系統中,每個進程都運行在虛擬地址空間(Virtual Address Space),在32位模式下它是一個4GB的內存地址塊。在Linux系統中, 內核進程和用戶進程所占的虛擬內存比例是1:3,而Windows系統為2:2(通過設置Large-Address-Aware Executables標志也可為1:3)。
虛擬地址通過頁表(Page Table)映射到物理內存,頁表由操作系統維護並被處理器引用。內核空間在頁表中擁有較高特權級,因此用戶態程序試圖訪問這些頁時會導致一個頁錯誤(page fault)。在Linux中,內核空間是持續存在的,並且在所有進程中都映射到同樣的物理內存。內核代碼和數據總是可尋址,隨時准備處理中斷和系統調用。與此相反,用戶模式地址空間的映射隨進程切換的發生而不斷變化。
Linux進程在虛擬內存中的標准內存段布局如下圖所示:
其中,用戶地址空間中的藍色條帶對應於映射到物理內存的不同內存段,灰白區域表示未映射的部分。這些段只是簡單的內存地址范圍,與Intel處理器的段沒有關系。
上圖中Random stack offset和Random mmap offset等隨機值意在防止惡意程序。Linux通過對棧、內存映射段、堆的起始地址加上隨機偏移量來打亂布局,以免惡意程序通過計算訪問棧、庫函數等 地址。execve(2)負責為進程代碼段和數據段建立映射,真正將代碼段和數據段的內容讀入內存是由系統的缺頁異常處理程序按需完成的。另外,execve(2)還會將BSS段清零。
二、內存訪問機制
在80X86CPU的發展過程中,存儲器的管理機制發生了較大的變化。8086/8088CPU對存儲器的管理采用分段的實方式;80286CPU除了可在實方式下工作外,還可以在保護模式下工作;而80386CPU之后的處理器則具有三種工作方式:實方式、保護方式和虛擬8086方式。
2.1 分段機制
段的引入:8086為了用16位寄存器實現1MB(20位)的尋址內存空間,引入了段的概念。在沒有采用分頁管理時,邏輯地址計算而得的線性地址是直接映射物理地址(Physical Address)的,於是可以直接用線性地址訪問內存;否則,還要通過X86的分頁轉換,將線性地址轉換為物理地址。
2.1.1 實模式
在Real Mode下,我們對一個內存地址的訪問是通過Segment:Offset的方式來進行的,其中Segment是一個段的Base Address,一個Segment的最大長度是64 KB,這是16-bit系統所能表示的最大長度。而Offset則是相對於此Segment Base Address的偏移量。Base Address+Offset就是一個內存絕對地址。在實際編程的時候,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)來指定Segment,CPU將段寄存器中的數值向左偏移4-bit,放到20-bit的地址線上就成為20-bit的Base Address。
2.1.2 保護模式
到了Protected Mode,內存的管理模式分為兩種,段模式和頁模式。
由於 Protected Mode運行在32-bit系統上,那么Segment的兩個因素:Base Address和Limit也都是32位的。IA-32允許將一個段的Base Address設為32-bit所能表示的任何值(Limit則可以被設為32-bit所能表示的,以2^12為倍數的任何指),而不像Real Mode下,一個段的Base Address只能是16的倍數(因為其低4-bit是通過左移運算得來的,只能為0,從而達到使用16-bit段寄存器表示20-bit Base Address的目的),而一個段的Limit只能為固定值64 KB。另外,Protected Mode,顧名思義,又為段模式提供了保護機制,也就說一個段的描述符需要規定對自身的訪問權限(Access)。所以,在Protected Mode下,對一個段的描述則包括3方面因素:【Base Address, Limit, Access】,它們加在一起被放在一個64-bit長的數據結構中,被稱為段描述符。
(1)GDT
如果我們直接通過一個64-bit段描述符來引用一個段的時候,就必須使用一個64-bit長的段寄存器裝入這個段描述符。但Intel為了保持向后兼容,將段寄存器仍然規定為16-bit(盡管每個段寄存器事實上有一個64-bit長的不可見部分,但對於程序員來說,段寄存器就是16-bit的),那么很明顯,我們無法通過16-bit長度的段寄存器來直接引用64-bit的段描述符。解決的方法就是把這些長度為64-bit的段描述符放入一個數組中,而將段寄存器中的值作為下標索引來間接引用(事實上,是將段寄存器中的高13 -bit的內容作為索引)。這個全局的數組就是GDT。事實上,在GDT中存放的不僅僅是段描述符,還有其它描述符,它們都是64-bit長。GDT是Protected Mode所必須的數據結構,也是唯一的。
全局描述符表GDT含有每一個任務都可能或可以訪問的段的描述符,通常包含操作系統所使用的代碼段,數據段和堆棧段的描述符,也包含多種特殊數據段的描述符,如各個LDT的描述符等.每個GDT最多含有8192個描述符.注意,GDT的第0個描述符總不被處理,通常它置成全0.
GDT結構圖如下:
說明如下:
G:
(1)、G=0時,段限長的20位為實際段限長,最大限長為2^20=1MB
(2)、G=1時,則實際段限長為20位段限長乘以2^12=4KB,最大限長達到4GB
D/B:
當描述符指向的是可執行代碼段時,這一位叫做D位,D=1使用32位地址和32/8位操作數,D=0使用16位地址和16/8位操作數。如果指向的是向下擴展的數據段,這一位叫做B位,B=1時段的上界為4GB,B=0時段的上界為64KB。如果指向的是堆棧段,這一位叫做B位,B=1使用32位操作數,堆棧指針用ESP,B=0時使用16位操作數,堆棧指針用SP。
DPL:特權級,0為最高特權級,3為最低,表示訪問該段時CPU所需處於的最低特權級
type : 類型
(1)、type<8時:數據段
(2)、type>=8時:代碼段
GDT可以被放在內存的任何位置,那么當程序員通過段寄存器來引用一個段描述符時,CPU必須知道GDT的入口,也就是基地址放在哪里,所以Intel的設計者門提供了一個寄存器GDTR(GDTR是一個48位的全局描述符寄存器,高32位存放GDT的基址,低16位存放GDT限長。)用來存放GDT的入口地址,程序員將GDT設定在內存中某個位置之后,可以通過LGDT指令將GDT的入口地址裝入此寄存器,從此以后,CPU就根據此寄存器中的內容作為GDT的入口來訪問GDT了。
(2)LDT
除了GDT之外,IA-32還允許程序員構建與GDT類似的數據結構,它們被稱作LDT(Local Descriptor Table,局部描述符表),但與GDT不同的是,LDT在系統中可以存在多個,並且從LDT的名字可以得知,LDT不是全局可見的,它們只對引用它們的任務可見,保護模式支持多任務,每個任務都有自己的局部描述符表LDT,且每個任務最多只有一個LDT,每個任務的LDT含有該任務自己的代碼段,數據段和堆棧段的描述符。另外,每一個LDT自身作為一個段存在,它們的段描述符被放在GDT中。每個LDT最多含有8192個描述符。
IA-32為LDT的入口地址也提供了一個寄存器LDTR(LDTR是一個16位的局部描述符寄存器,高13位存放LDT在GDT中的索引值。),因為在任何時刻只能有一個任務在運行,所以LDT寄存器全局也只需要有一個。如果一個任務擁有自身的LDT,那么當它需要引用自身的LDT時,它需要通過lldt指令將其LDT的段描述符裝入此寄存器。lldt指令與lgdt指令不同的時,lgdt指令的操作數是一個32-bit的內存地址,這個內存地址處存放的是一個32-bit GDT的入口地址,以及16-bit的GDT Limit。而lldt指令的操作數是一個16-bit的選擇子,這個選擇子主要內容是:被裝入的LDT的段描述符在GDT中的索引值。
所以我們可以這樣理解GDT和LDT:GDT為一級描述符表,LDT為二級描述符表。如圖:
其中,selector選擇器(一個16位的數據結構)被裝入段寄存器,它的高13位作為被引用的段描述符在GDT/LDT中的下標索引,bit 2用來指定被引用段描述符被放在GDT中還是到LDT中,bit 0和bit 1是RPL——請求特權等級,被用來做保護目的。如圖所示:
(3)IDT
中斷描述符表(Interrupt Descriptor Table,IDT)將每個異常或中斷向量分別與它們的處理過程聯系起來。與GDT和LDT表類似,IDT也是由8字節長描述符組成的一個數組。
IDT表可以駐留在線性地址空間的任何地方,處理器使用IDTR寄存器來定位IDT表的位置。這個寄存器中含有IDT表32位的基地址和16位的長度(限長)值。
在實地址模式中,CPU把內存中從0開始的1K字節作為一個中斷向量表。表中的每個表項占四個字節,由兩個字節的段地址和兩個字節的偏移量組成,這樣構成的地址便是相應中斷處理程序的入口地址。但是,在保護模式下,由四字節的表項構成的中斷向量表顯然滿足不了要求。這是因為,除了兩個字節的段描述符,偏移量必用四字節來表示;要有反映模式切換的信息。在保護模式下,中斷向量表中的表項由8個字節組成,中斷向量表也改叫做中斷描述符表IDT(InterruptDescriptor Table)。其中的每個表項叫做一個門描述符(gate descriptor),“門”的含義是當中斷發生時必須先通過這些門,然后才能進入相應的處理程序。
主要門描述符是:
· 中斷門(Interrupt gate)
其類型碼為110,中斷門包含了一個中斷或異常處理程序所在段的選擇符和段內偏移量。當控制權通過中斷門進入中斷處理程序時,處理器清IF標志,即關中斷,以避免嵌套中斷的發生。中斷門中的DPL(Descriptor Privilege Level)為0,因此,用戶態的進程不能訪問Intel的中斷門。所有的中斷處理程序都由中斷門激活,並全部限制在內核態。
· 陷阱門(Trap gate)
其類型碼為111,與中斷門類似,其唯一的區別是,控制權通過陷阱門進入處理程序時維持IF標志位不變,也就是說,不關中斷。
· 系統門(System gate)
這是Linux內核特別設置的,用來讓用戶態的進程訪問Intel的陷阱門,因此,門描述符的DPL為3。通過系統門來激活4個Linux異常處理程序,它們的向量是3、4、5及128,也就是說,在用戶態下,可以使用int3、into、bound 及int0x80四條匯編指令。
最后,在保護模式下,中斷描述符表在內存的位置不再限於從地址0開始的地方,而是可以放在內存的任何地方。為此,CPU中增設了一個中斷描述符表寄存器IDTR,用來存放中斷描述符表在內存的起始地址。中斷描述符表寄存器IDTR是一個48位的寄存器,其低16位保存中斷描述符表的大小,高32位保存IDT的基址.
2.2 分頁機制
2.2.1 MMU
在沒有使用虛擬存儲器的機器上,虛擬地址被直接送到內存總線上,使具有相同地址的物理存儲器被讀寫。而在使用了虛擬存儲器的情況下,虛擬地址不是被直接送到內存地址總線上,而是送到內存管理單元——MMU。他由一個或一組芯片組成,一般存在於協處理器中,其功能是把虛擬地址映射為物理地址,這是MMU的基本作用之一,除了硬件的支持外,軟件上實際就是維護一張表,表中的內容是VA到PA的轉換法則;另一作用是可以實現不同的訪問權限。
2.2.2 地址映射
分頁的最大作用就在於:使得進程的物理地址空間可以是非連續的。在分段的方法中,每次程序運行時總是把程序全部裝入內存,而分頁的方法按照程序運行的局部性原理只將部分程序裝入內存。
當創建一個進程時,操作系統會為該進程分配一個4GB(32位系統中)大小的虛擬進程地址空間。創建4GB虛擬地址空間其實並不是要真的創建空間,只是要創建那種映射機制所需要的數據結構而已,這種數據結構就是頁目和頁表。
一個頁表的大小為4K字節,放在一個物理頁中。由1024個4字節的頁表項組成。頁表項的大小為4個字節(32bit),所以一個頁表中有1024 個頁表項。頁表中的每一項的內容(每項4個字節,32bit)高20bit用來放一個物理頁的物理地址,低12bit放着一些標志。
頁目錄,一個頁目錄大小為4K字節,放在一個物理頁中。由1024個4字節的頁目錄項組成。頁目錄項的大小為4個字節(32bit),所以一個頁目錄中有 1024個頁目錄項。頁目錄中的每一項的內容(每項4個字節)高20bit用來放一個頁表(頁表放在一個物理頁中)的物理地址,低12bit放着一些標志。
對於x86系統,頁目錄的物理地址放在CPU的CR3寄存器中。
頁表中包含物理頁面基地址和頁的屬性。對於頁表有兩級頁表和三級頁表之分。linux為了保證可移植行,采用了三級分頁機制,當然其在某些情況下可以返回到二級分頁。
三、系統啟動過程的內存變化
參考http://blog.csdn.net/huangzhipeng/article/details/6159169
四、小結
1、在實模式邏輯地址是直接映射到物理地址的,所以可以直接使用邏輯地址訪問物理存儲單元;在分段模式下,邏輯地址對應線性地址(虛擬地址),需要轉換為對應的物理地址;在分頁模式下,內存被划分為一系列較段小的頁,從而減少了內存交換的開銷,提高了內存使用效率,其使用的虛擬地址與物理地址並非一一對應,所以需要進行轉換。
2、實模式下通過段寄存器:偏移量確定邏輯地址;段模式下通過GDT(R)、LDT(R)和selector(就是段寄存器)確定虛擬地址;分頁模式下通過MMU和多級頁表確定存儲單元,而CPU使用的是虛擬地址。