80386的分段機制、分頁機制和實體地址的形成
注:本分類下文章大多整理自《深入分析linux核心原始碼》一書,另有參考其他一些資料如《linux核心完全剖析》、《linux c 程式設計一站式學習》等,只是為了更好地理清系統程式設計和網路程式設計中的一些概念性問題,並沒有深入地閱讀分析原始碼,我也是草草翻過這本書,請有興趣的朋友自己參考相關資料。此書出版較早,分析的版本為2.4.16,故出現的一些概念可能跟最新版本核心不同。
此書已經開源,閱讀地址 http://www.kerneltravel.net
MOVE REG,ADDR ; 它把地址為ADDR(假設為10000)的記憶體單元的內容複製到REG 中
在8086 的真實模式下,把某一段暫存器(段基址)左移4 位,然後與地址ADDR 相加後被直接送到內存匯流排上,這個相加後的地址(20位)就是記憶體單元的實體地址,而程式中的這個地址ADDR就叫邏輯地址(或叫虛地址)。
在80386 的段機制中,邏輯地址由兩部分組成,即段部分(選擇符)及偏移部分。
段是形成邏輯地址到線性地址轉換的基礎。如果我們把段看成一個物件的話,那麼對它的描述如下。
(1)段的基地址(Base Address):線上性地址空間中段的起始地址。
(2)段的界限(Limit):表示在邏輯地址中,段內可以使用的最大偏移量。
(3)段的屬性(Attribute): 表示段的特性。例如,該段是否可被讀出或寫入,或者該段是否作為一個程式來執行,以及段的特權級等。
1、邏輯地址、線性地址和實體地址
![](https://i.iter01.com/images/b56de31cd94b87482ee561c0ecb32e50bfe98f29aef9e6478a0521224afe7902.jpg)
所謂描述符(Descriptor),就是描述段的屬性的一個8 位元組儲存單元。
2、使用者段描述符(Descriptor)
![](https://i.iter01.com/images/cb91d2312bd2bc192a8c12012c33b56fa7ca286f3a894bd803846f89da982f3a.jpg)
![](https://i.iter01.com/images/e4d40b503e477a709b797a0b311643abc14ad815c3d61c59ef22a1c5a47630b0.jpg)
一個段描述符指出了段的32 位基地址和20 位段界限(即段大小)。第6 個位元組的G 位是粒度位,當G=0 時,段長表示段格式的位元組長度,即一個段最長可達1M
位元組。當G=1 時,段長表示段的以4K 位元組為一頁的頁的數目,即一個段最長可達1M×4K=4G 位元組。D 位表示預設運算元的大小,如果D=0,運算元為16 位,如果D=1,運算元為32 位。
第7 位P 位(Present) 是存在位,表示段描述符描述的這個段是否在記憶體中,如果在記憶體中。P=1;如果不在記憶體中,P=0。
DPL(Descriptor Privilege Level),就是描述符特權級,它佔兩位,其值為0~3,用來確定這個段的特權級即保護等級。0為核心級別,3為使用者級別。
S 位(System)表示這個段是系統段還是使用者段。如果S=0,則為系統段,如果S=1,則為使用者程式的程式碼段、資料段或堆疊段。
型別佔3 位,第3 位為E 位,表示段是否可執行。當E=0 時,為資料段描述符,這時的第2 位ED 表示地址增長方向。第1 位(W)是可寫位。當段為程式碼段時,第3
位E=1,這時第2 位為一致位(C)。當C=1 時,如果當前特權級低於描述符特權級,並且當前特權級保持不變,那麼程式碼段只能執行。所謂當前特權級CPL(Current
Privilege Level),就是當前正在執行的任務的特權級。第1 位為可讀位R。
存取權位元組的第0 位A 位是訪問位,用於請求分段不分頁的系統中,每當該段被訪問時,將A 置1。對於分頁系統,則A 被忽略未用。
![](https://i.iter01.com/images/c4df64cd5940eaa4ab72608fe9f93b97b3814f7bbda25d1bca70b888b45fd3b7.jpg)
3、系統段描述符
![](https://i.iter01.com/images/7c28ee8d50eb724c3d7040b831fb9d5452de1f5dbc4c268159938498b4d79fde.jpg)
系統段描述符的第5 個位元組的第4 位為0,說明它是系統段描述符,型別佔4 位,沒有A 位。第6 個位元組的第6 位為0,說明系統段的長度是位元組粒度,所以,一個系統段的最大長度為1M
位元組。
系統段的型別為16 種,如圖2.15 所示。在這16 種型別中,保留型別和有關286 的型別不予考慮。門也是一種描述符,有呼叫門、任務門、中斷門和陷阱門4
種門描述符。
![](https://i.iter01.com/images/d7fe89a90cb32af6fd6ac4c27e1573383cfdc0f8262b42aa4f05bf23a5c7beea.jpg)
4、選擇符、描述符表和描述符表暫存器
描述符表(即段表)定義了386 系統的所有段的情況。所有的描述符表本身都佔據一個位元組為8 的倍數的儲存器空間,空間大小在8 個位元組(至少含一個描述符)到64K 位元組(至多含8K=8192)個描述符之間。
1.全域性描述符表(GDT)
全域性描述符表GDT(Global Descriptor Table),除了任務門,中斷門和陷阱門描述符外,包含著系統中所有任務都共用的那些段的描述符。它的第一個8 位元組位置沒有使用。
2.中斷描述符表(IDT)
中斷描述符表IDT(Interrupt Descriptor Table),包含256 個門描述符。IDT 中只能包含任務門、中斷門和陷阱門描述符,雖然IDT 表最長也可以為64K 位元組,但只能存取2K位元組以內的描述符,即256
個描述符,這個數字是為了和8086 保持相容。
3.區域性描述符表(LDT)
區域性描述符表LDT(Local Descriptor Table),包含了與一個給定任務有關的描述符,每個任務各自有一個的LDT。有了LDT,就可以使給定任務的程式碼、資料與別的任務相隔離。每一個任務的區域性描述符表LDT
本身也用一個描述符來表示,稱為LDT 描述符,它包含了有關區域性描述符表的資訊,被放在全域性描述符表GDT 中,使用LDTR進行索引。
在真實模式下,段暫存器儲存的是真實的段基址,在保護模式下,16 位的段暫存器無法放下32 位的段基址,因此,它們被稱為選擇符,即段暫存器的作用是用來選擇描述符。選擇符的結構如圖2.16
所示。
![](https://i.iter01.com/images/6f99ff5966e2f72dd34a0691456eb683127bd931b2dd5d85749dee839efccd4b.jpg)
可以看出,選擇符有3 個域:第15~3 位這13 位是索引域,表示的資料為0~8129,用於指向全域性描述符表中相應的描述符。第2 位為選擇域,如果TI=1,就從區域性描述符表中選擇相應的描述符,如果TI=0,就從全域性描述符表中選擇描述符。第1、0
位是特權級,表示選擇符的特權級,被稱為請求者特權級RPL(Requestor Privilege Level)。只有請求者特權級RPL
高於(數字低於)或等於相應的描述符特權級DPL,描述符才能被存取,這就可以實現一定程度的保護。
下面講一下在沒有分頁操作時,定址一個儲存器運算元的步驟。
(1)在段選擇符中裝入16 位數,同時給出32 位地址偏移量(比如在ESI、EDI 中等)。
(2)先根據相應描述符表暫存器中的段地址(確定描述符表的地址)和段界限(確定描述符表的大小),根據段選擇符的TI決定從哪種描述符表中取,再根據段選擇符的索引找到相應段描述符的位置,比較RPL與DPL,若該段無問題,就取出相應的段描述符放入段描述符高速緩衝暫存器中。
(3)將段描述符中的32 位段基地址和放在ESI、EDI 等中的32 位有效地址相加,就形成了32 位實體地址。
![](https://i.iter01.com/images/5e103292a741ebd4dcc92fac7fe924406ac56c783cf0d2de491570bafcedfaa7.jpg)
5、linux中的段機制
從2.2 版開始,Linux 讓所有的程式(或叫任務)都使用相同的邏輯地址空間,因此就沒有必要使用區域性描述符表LDT。
Linux 在啟動的過程中設定了段暫存器的值和全域性描述符表GDT 的內容,段暫存器的定義在include/asm-i386/segment.h 中:
C++ Code
1
2 3 4 5 |
#define __KERNEL_CS 0x10 //核心程式碼段,index=2,TI=0,RPL=0
#define __KERNEL_DS 0x18 //核心資料段, index=3,TI=0,RPL=0 #define __USER_CS 0x23 //使用者程式碼段, index=4,TI=0,RPL=3 #define __USER_DS 0x2B //使用者資料段, index=5,TI=0,RPL=3 |
從定義看出,沒有定義堆疊段,實際上,Linux 核心不區分資料段和堆疊段,這也體現了Linux 核心儘量減少段的使用。因為沒有使用LDT,因此,TI=0,並把這4 個段描述符都放在GDT中,
index 就是某個段描述符在GDT 表中的下標。核心程式碼段和資料段具有最高特權,因此其RPL為0,而使用者程式碼段和資料段具有最低特權,因此其RPL 為3。
全域性描述符表的定義在arch/i386/kernel/head.S 中:
C++ Code
1
2 3 4 5 6 7 8 9 10 |
ENTRY(gdt_table)
.quad 0x0000000000000000 /* NULL descriptor */ .quad 0x0000000000000000 /* not used */ .quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */ .quad 0x00cf92000000ffff /* 0x18 kernel 4GB data at 0x00000000 */ .quad 0x00cffa000000ffff /* 0x23 user 4GB code at 0x00000000 */ .quad 0x00cff2000000ffff /* 0x2b user 4GB data at 0x00000000 */ .quad 0x0000000000000000 /* not used */ .quad 0x0000000000000000 /* not used */ |
從程式碼可以看出,GDT 放在陣列變數gdt_table 中。按Intel 規定,GDT 中的第一項為空,這是為了防止加電後段暫存器未經初始化就進入保護模式而使用GDT 的。第二項也沒用。從下標2~5
共4 項對應於前面的4 種段描述符值。對照圖2.10,從描述符的數值可以得出:
• 段的基地址全部為0x00000000;
• 段的上限全部為0xffff;
• 段的粒度G 為1,即段長單位為4KB;
• 段的D 位為1,即對這4 個段的訪問都為32 位指令;
• 段的P 位為1,即4 個段都在記憶體。
由此可以得出,每個段的邏輯地址空間範圍為0~4GB。每個段的基地址為0,因此,邏輯地址到線性地址對映保持不變,也就是說,偏移量就是線性地址,我們以後所提到的邏輯地址(或虛擬地址)和線性地址指的也就是同一地址。看來,Linux
巧妙地把段機制給繞過去了,它只把段分為兩種:使用者態(RPL=3)的段和核心態(RPL=0)的段,而完全利用了分頁機制。
按Intel 的規定,每個程式有一個任務狀態段(TSS)和區域性描述符表LDT,但Linux 也沒有完全遵循Intel 的設計思路。如前所述,Linux 的程式沒有使用LDT,而對TSS 的使用也非常有限,每個CPU
僅使用一個TSS。TSS 有它自己 8 位元組的任務段描述符(Task State Segment Descriptor ,簡稱TSSD)。這個描述符包括指向TSS 起始地址的32 位基地址域,20 位界限域,界限域值不能小於十進位制104(由TSS
段的最小長度決定)。TSS 描述符存放在GDT 中,它是GDT 中的一個表項,由中斷描述符表(IDT)中的任務門(存放TSS段的選擇符)裝入TR來進行索引。
7、頁目錄項、頁表項、頁面項
80386 使用4K 位元組大小的頁。每一頁都有4K 位元組長,並在4K 位元組的邊界上對齊,即每一頁的起始地址都能被4K 整除。因此,80386 把4G 位元組的線性地址空間,劃分為1G 個頁面,每頁有4K
位元組大小。分頁機制通過把線性地址空間中的頁,重新定位到實體地址空間來進行管理,因為每個頁面的整個4K 位元組作為一個單位進行對映,並且每個頁面都對齊4K 位元組的邊界,因此,線性地址的低12 位經過分頁機制直接地作為實體地址的低12 位使用。
頁目錄表,儲存在一個4K 位元組的頁面中,最多可包含1024
個頁目錄項,每個頁目錄項為4 個位元組,結構如圖2.22 所示。
![](https://i.iter01.com/images/d580c60aef58490b78945ed84d0129d93e9d788684d65918608ef9fae23d1233.jpg)
![](https://i.iter01.com/images/b9d94db088c4b6a35dc97a4a101e27d285cc87e94cf520edb14e04f719078507.jpg)
• 第31~12 位是20 位頁表地址,由於頁表地址的低12 位總為0,所以用高20 位指出32 位頁表地址就可以了。
• 第0 位是存在位,如果P=1,表示頁表地址指向的該頁在記憶體中,如果P=0,表示不在記憶體中。
• 第1 位是讀/寫位,第2 位是使用者/管理員位,這兩位為頁目錄項提供硬體保護。當特權級為3 的程式要想訪問頁面時,需要通過頁保護檢查,而特權級為0
的程式就可以繞過頁保護,如圖2.23 所示。
• 第3 位是PWT(Page Write-Through)位,表示是否採用寫透方式,寫透方式就是既寫記憶體(RAM)也寫快取記憶體,該位為1
表示採用寫透方式。第4 位是PCD(Page Cache Disable)位,表示是否啟用快取記憶體,該位為1 表示啟用快取記憶體。
• 第5 位是訪問位,當對頁目錄項進行訪問時,A 位=1。
• 第7 位是Page Size 標誌,只適用於頁目錄項。如果置為1,頁目錄項指的是4MB 的頁面,即擴充套件分頁。
80386 的每個頁目錄項指向一個頁表,儲存在一個4K
位元組的頁面中,頁表最多含有1024 個頁面項,每項4
個位元組,包含頁面的起始地址和有關該頁面的資訊。頁面的起始地址也是4K 的整數倍,所以頁面的低12 位也留作它用,如圖2.24 所示。
![](https://i.iter01.com/images/c47c89f64d294d56f22c2321aa57e6f3c8847427c61c300f0945a45b303b05b9.jpg)
第31~12 位是20 位物理頁面地址,除第6 位外第0~5 位及9~11 位的用途和頁目錄項一樣,第6
位是頁面項獨有的,當對涉及的頁面進行寫操作時,D 位被置1。
4GB 的儲存器只有一個頁目錄,它最多有1024 個頁目錄項,每個頁目錄項又含有1024個頁面項,因此,儲存器一共可以分成1024×1024=1M 個頁面。由於每個頁面為4K 個位元組,所以,儲存器的大小正好最多為4GB。
當訪問一個操作單元時,如何由分段結構確定的32 位線性地址通過分頁操作轉化成32位實體地址呢?
第一步,CR3 包含著頁目錄的起始地址,用32 位線性地址的最高10 位A31~A22 作為頁目錄表的頁目錄項的索引,將它乘以4,與CR3 中的頁目錄表的起始地址相加,形成相應頁目錄項的地址。
第二步,從指定的地址中取出32 位頁目錄項,它的低12 位為0,這32 位是頁表的起始地址。用32 位線性地址中的A21~A12 位作為頁表中的頁表項的索引,將它乘以4,與頁表的起始地址相加,形成相應頁表項的地址。
第三步,從指定地址中取出32位頁表項,它的低12位為0,這32位是頁面地址,將A11~A0 作為相對於頁面地址的偏移量,與32 位頁面地址相加,形成32 位實體地址。
![](https://i.iter01.com/images/a0458b5cfdddcefb0a51f829b59846ae8dbba58062fa01ac93577cfb955f5238.jpg)
8、linux 中的分頁機制
Linux 的分段機制使得所有的程式都使用相同的段暫存器值,這就使得記憶體管理變得簡單,也就是說,所有的程式都使用同樣的線性地址空間(0~4GB)。Linux
採用三級分頁模式而不是兩級。如圖2.28 所示為三級分頁模式,為此,Linux定義了3 種型別的表。
• 總目錄PGD(Page Global Directory)
• 中間目錄PMD(Page Middle Derectory)
• 頁表PT(Page Table)
![](https://i.iter01.com/images/18f8d694364ed11040108d477b51921e3febde2130d33dc07af16ccf6a8fc83c.jpg)
相關文章
- 80386分頁機制與虛擬記憶體記憶體
- 80386學習(五) 80386分頁機制與虛擬記憶體記憶體
- Redis的記憶體和實現機制Redis記憶體
- 分頁機制圖文詳解
- HDFS 02 - HDFS 的機制:副本機制、機架感知機制、負載均衡機制負載
- Nginx accept鎖的機制和實現Nginx
- Android View 的事件體系 -- 事件分發機制AndroidView事件
- 頁面渲染機制
- 【記憶體管理】頁面分配機制記憶體
- Android 事件分發機制的理解Android事件
- Android的MotionEvent事件分發機制Android事件
- 網路:IP地址分類和分段
- 一文了解微分段應用場景與實現機制
- springMVC 的工作原理和機制SpringMVC
- javascript的垃圾回收機制和記憶體管理JavaScript記憶體
- Spring AOP 的實現機制Spring
- Nestjs模組機制的概念和實現原理JS
- Android事件分發機制Android事件
- 作業系統:x86下記憶體分頁機制 (1)作業系統記憶體
- Linux的管道機制和重定向Linux
- Roguelike機制的原理和應用
- Java 中的 Wait 和 Notify 機制JavaAI
- MySQL中的MVCC實現機制MySqlMVC
- docker 實現 Redis 的哨兵機制DockerRedis
- Js非同步機制的實現JS非同步
- 分段與分頁
- 響應式流的核心機制——背壓機制
- 博文推薦|Pulsar 的訊息儲存機制和 Bookie 的 GC 機制原理GC
- Session和Cookie機制SessionCookie
- 2018.03.08、View的事件分發機制筆記View事件筆記
- 8分鐘瞭解TDengine的WAL機制
- 8分鐘瞭解 TDengine 的 WAL 機制
- 淺談Android中的事件分發機制Android事件
- EDF:2021國內碳價格形成機制研究報告
- Java的記憶體管理機制之記憶體區域劃分Java記憶體
- 關於JavaScript的記憶體機制JavaScript記憶體
- 記憶體管理機制的發展記憶體
- View事件分發機制分析View事件
- cocos EventDispatcher事件分發機制事件