``本文的一些截圖來自于<IntelDEV卷3>和<x86匯編從實模式到保護模式>`
最近復習一些操作系統(tǒng)的知識,首先遇到了個坑便是計算機尋址問題.
本文是一些偏理論的東西(匯編可能在工作中用不到,需要的時候再深入研究吧- -!)
本文參考的一些博客和書本:
<a href=http://blog.csdn.net/trochiluses/article/details/8954527>[實模式與保護模式解惑之(一)——二者的起源與區(qū)別]</a>
匯編相關-從匯編研究局部變量機制
我看保護模式
x86匯編語言:從實模式到保護模式
實模式
- 是什么
INTEL 8086CPU的尋址方式.
具體利用分段機制訪問內存,訪問時給出一個段基地址和一個段內偏移;(如DS:AX,其中DS和AX在這里的字長為16),訪問的地址為實實在在的物理地址.
-
為什么
一個原因是
8086特點 - 數據總線16位(字長為16)、地址總線卻有20位(準確地說是CPU的尋址能力需要被設計成20位).這樣程序員在沒有特殊機制的引入時,只能訪存
2^16 B = 64 KB的空間(16位字長),達不到產品經理的需求(20位地址空間 = 2^20 B = 1 MB)于是,這里設計成了使用
段基地址 + 段內偏移的方式:物理地址 = 段基地址<<16 + 段內偏移
有個小問題就是這種方式實際上可訪問的空間大于1M,即最大可以訪問到0xFFFF*16 + 0xFFFF = 0x10FFEF > 0xFFFFF,多出的這些地址將從0開始(即對1M取模). -
實模式的段的特點 -
每個段基地址都是16的倍數;
-
每個段的最小長度是16 Bytes,最大是64 KB;<----------更新: 段的大小從1B到64KB都可以.見wiki:
1. WIKIPEDIA.png 訪問的每個地址都是實際的物理地址.
遺留的問題
系統(tǒng)程序與用戶程序訪問的地址共存于同一地址空間,并且一視同仁:
- 沒有權限保護(用戶可以隨意訪問1M的全部內存地址空間)
- 不支持多任務.
保護模式
保護模式之所以叫“保護模式”,因為他對多任務提供了保護,并加入了權限.
瑪德我想起來了大二匯編時保護模式這一章跳過了因為期末不考沃日.
是什么
80386及后續(xù)系列CPU所資磁的內存尋址方式.-
為什么
引入保護模式的目的有:- 保護 - 權限管理
- 多任務隔離
向下兼容 - ...
-
怎么做
這里簡要介紹一下保護模式.尋址方式的改變:
實模式下 - 可以直接訪問
段基地址(段寄存器16_bit)<<16 + 段內偏移并做一些單字長操作(訪存/賦值) .-
保護模式下 -
先來點廢話:
由于需要引入權限管理, 需要:訪存時指出當前執(zhí)行指令的人有沒有權限訪問這個地址;
這句話至少包括了2點:- 當前指令的權限描述;
- 目標地址的權限描述.
所以當前的解決方案已經呼之欲出了:需要一個數據結構來描述內存地址.
保護模式下的尋址_part1段式(1MB --> 4GB 32Bit)
機器啟動流程 - 一個PC啟動時都會先進入實模式,接著由某指令轉入保護模式.
-
段描述符表- 是一堆段描述符的集合,有GDT和LDT.其中GDT是全局描述符表Global Descriptor Table,該表是為整個OS服務的,在進入保護模式之前定義好(由bootloader). LDT,相對應地,則是局部段描述表,是每個進程自己獨有的?.GDTR寄存器存放著GDT的地址.
理論上說GDT可以位于4GB中的任何一個位置,但因為需要從實模式轉保護模式,所以一般只在1M以內的位置...(參見<x86從實模式到保護模式>)
2. GDTR,48bit的寄存器 -
描述符表項- 無論是全局還是局部的描述符表,表項字段都如下圖所示:(it‘s a fucking Data Structure!)
3. 描述符表項里面記錄了這個段的:
- 段基地址BASE(32位一共,被分割成了3部分)
- 段界限LIMIT(20位)
- 段界限Granularity (Seg.LIMIT字段)的單位(0 - 1B/1 - 4KB).
段界限及其單位表示 段內偏移的最值(對于向上增長的段而言表示了最大值,而向下增長的段如堆棧段則是表示段內偏移的最小值哦)<------why?舉個例子如下:(如有錯誤請指出)
0xFFFFFFFF |********|
...
0x00A01001 |********|<-----SS:EBP (指向棧底) }
0x00A00FFF |********|<-----SS:ESP (指向棧頂) }EBP,ESP兩個的值可能都是段內偏移
...
0x00A00002 | |
0x00A00000 |********|<-----SS:<段界限*粒度+1> Suppose a MIN_VAL's linear addr
...
|
0x0000FF00 |********|
...
0x00000002 | |(每次存進來一個字2 bytes,所以地址上相差2)
0x00000000 |********|<-----SS:0
0.假設某程序運行時調用了一個函數,這時該程序的動作包括`保存現場`,`聲明堆棧段`;我們關心的是聲明堆棧段(用來存儲函數參數、局部變量等).
1.這個段也有基址(我們假設為0x0,后面你就知道其實這個假設是ok的)
2.聲明時要做的事是指定EBP、ESP(有點欽定的感覺),接著ESP減2,push進來函數參數之類的,每次push,ESP會自動減2(別問我為什么自動),每次pop,ESP會自動加2
3.上面所說的段內偏移的最小值,指的就是當ESP一直減,最多只能減到<段界限*粒度> ,再減就提示堆棧溢出了.
<a href=http://www.360doc.com/content/16/0223/12/28062682_536641957.shtml>匯編相關-從匯編研究局部變量機制</a>
所以現在一個段最大可以是4G,最小可以是1B.
理論上訪問4G內存不再需要什么段啥的,一個段足矣(這種情況便是所謂的平坦模式):

-
段選擇子- 段寄存器CS/DS/SS/etc...此時不再是被用來左移并相加的對象,而是存放一個索引+標記+權限,這個索引指向了需要訪問的段所在描述符.這些存放的內容被稱為段選擇子.
5. 段選擇子
由段選擇子的索引量可以看出一個表最多有2^13=8192個表項
(實際上32位處理器如80386,他們的段寄存器的長度為32位,只不過后面16位我們不可見,是處理器用作描述符高速緩存的) -
整體描述 (from high level)
6. 線性地址如何得到-此圖來自<a href=http://bbs.chinaunix.net/thread-2083672-1-1.html>thread</a>('8'是表示立即數填充Index字段) -
權限 :
權限字 0 - 3(高 - 低),如下:
7. 特權級別 -
權限的判斷機制:
- 代碼段CS寄存器里的CPL為當前執(zhí)行權限,稱其為CPL(current);
- 數據段DS(或SS等,發(fā)出尋址請求指令所在寄存器)里的CPL為請求權限,稱其為RPL(request);
- 段描述符中的段權限DPL;
3者進行一個判斷,如果合法(比較復雜的判斷,例如滿足DPL>=CPL,DPL >= RPL,且CPL <= RPL等),則進行地址轉換(此時得出的是虛擬地址,需要轉為物理地址,轉換又跟此時的另一些東西相關,見part2),轉換后最終尋址到所需地址.
總之,在保護模式下做什么事都得先進行權限檢查.
PS: 這里的ring 0也就是所謂的內核態(tài),而linux中的用戶態(tài)是ring 3.
參考:<a href=http://blog.csdn.net/xiao_0429/article/details/47165169>我看保護模式</a>
(具體如何做的不深入了,涉及太多匯編相關的東西,知道就好)<-------fuck that!既然有興趣讀到這些知識,最好搞懂, 不然辜負這些知識.
保護模式下的尋址_part2段頁式(更復雜的機制來了)
- 整理一下目前為止接觸到的東西:
- 機器啟動階段先進入的是實模式(1M內存尋址空間,20Bit地址 + 16Bit數據), 然后由某指令轉入保護模式.
-
保護模式_段式- 最主要是為了解決權限問題,比如數據段不能被拿來當代碼段運行,當前權限低的不能訪問權限高的段等.
2.1 保護模式_段式 - GDT 和LDT(本來該設計是整個OS有一個GDT,每個進程有自己的LDT,而linux的進程極少使用LDT,基本上GDT和LDT起始為止都是0x00000000)
2.2 GDT的地址和長度存放于GDTR(Global Descriptor Table Register),其中0~15為GDT長度,16~47為GDT所在地址.
- 保護模式_段式 - the entry of GDT(or LDT),每個表項描述了一個段.
- 段選擇子 - 段寄存器(如CS,SS等)存放的內容包括INDEX、TI、RPL.
- 權限4個等級(特權0 - 內核態(tài), 特權3 - 用戶態(tài))- also known as
ring 0 ~ 3.不管做什么都需要進行權限判斷。
- 目前為止,訪問內存是這樣訪問的:
用戶提供段選擇子(Index,TI,RPL)和段內偏移(offset) |
GDTR/LDTR提供了GDT和LDT的地址和長度 |-->權限檢查(結合代碼段CS中的權限字,段選擇子中的權限字和GDT/LDT表項中的權限字DPL進行檢查)
-->找到Index所在段,取出其段基地址
-->把用戶提供的段內偏移與段基地址結合構成一個完整的地址(這里稱為**線性地址**)
也就是上面的圖6表示的.
在單純只有分段的情形下,這個線性地址就是物理地址.
- 目前為止,基于段的內存替換是這樣進行的:

- 分段的缺點 - 內存外部碎片.(段大小不確定,使用一段時間之后,內存可能會有很多微小的空洞,不足以提供分配)===>因此
分頁機制就來了.
分頁機制
- 大致描述:
- 4GB物理內存以4KB大小分為
4GB / 4KB = 1048576(1M)個頁,頁也稱為頁框(page frame). - 引入
虛擬內存的概念.對于每個進程而言,大家都有自己的4GB內存空間,并且通過一定的手段(后面解釋), 按頁為單位映射到實際的物理內存上. - 映射表,上一點提到的虛擬內存中的頁面與實際內存物理頁面間通過映射表來聯系,甚至每個進程都有自己的映射表, 另外這個映射表也很可能是分級的以解決空間利用效率.
- 頁面的管理和頁面的分配沒有關系,線性地址(也就是段管理單元得到的地址)也與頁面的分配沒有關系.
- 4GB物理內存以4KB大小分為

- 詳細深入:
- MMU(Memory Management Unit)是CPU中的內存管理單元.
- 頁目錄:分頁機制實際上更復雜一些,上面說的"頁表"一共有1M個,每個表項有4Bytes,那么整個表有4MB這么大. 每個進程有自己的頁表,并且一般不會用到這么大,每次換入換出RAM是不是TM很煩?頁目錄就是解決這個的.(也就是上文里提到的頁映射表分級問題)
- 4GB內存中一共有1M個頁 --> 現在把這1M分成2個層次,即 1K * 1K,給第一個1K一個新的名字——頁目錄表(Page Dir. Tbl.),第二個1K是真正的頁表. 頁表的規(guī)模變小了但相應地數目變?yōu)?K個,所謂頁目錄表,就是存放這1K個頁表的頁.這里需要注意兩種表的表項大小都是4B,所以兩種表的大小正好都是4K即一個頁的大小.
還是有點亂,見下面一系列的圖.(退后 我要開始裝逼了?。?br> Powered By Processon.com -
Naive的頁表:
Naive頁表 -
分層次的頁表:
分層次的頁表 -
分段/分頁的關系:
分段/分頁的關系 -
段頁式物理地址的獲取:
段頁式物理地址的獲取
一些其他問題:
-
吐槽 - 為什么經常弄不懂呢?一部分原因是自己太懶,另一部分書確實也有些沒講清楚或者考試不考尼瑪就跳過了.(歸根到底還是自己的問題,別怪別人...)
9.-1 教科書中出現的地址轉換還是不夠精確,沒有說明段選擇子也沒有設計頁的分層 - 目前關注的這些應該還算是尋址方式的一些東西,而上面有提到需要給進程分配頁面,以及在該頁面不怎么使用時換出,那么這些動作是怎么做的呢?
9. 有錯請指出,該圖來自<a href = http://www.cnblogs.com/bizhu/archive/2012/10/09/2717303.html>cnblog</a>
上面最后幾個圖如果看不清請猛戳<a href = https://www.processon.com/view/link/578a274ce4b0701cc02852c6> 我的文件</a>
歡迎大家糾錯,共同進步.
寫這篇的時候聯想到的一些問題:(亟待深入)
快表? - ok.
缺頁中斷 ?
dirty ?
伙伴系統(tǒng) ?
slab/slub ?
malloc ?
page cache / buffer cache 又是什么呢 ?











