這節仍然是從實現的角度來講述分頁機制。
為什麼要引入分頁機制。我們都知道分段機制是為了提供保護機制,那麼為什麼還要引入分頁機制呢?
為什麼引入分頁機制
想象一下這樣一種情況:假設我們用的計算機物理內存是4GB,但是我們的程序大小是5GB。那麼這個時候我們無法將程序全部放到內存中,也就無法運行程序。分頁機制引入的原因之一就是為了解決這個問題。分頁機制的引入實現了虛擬存儲器的機制。
另外,程序執行具有局部性,也就是說一段時間內,只需要程序代碼中的一小部分(相對於整個程序)就可以實現程序的執行。那麼我們就不用把所有代碼和數據都存放在內存中,而是將現在或很近的將來需要的代碼和數據放入內存就行了。要實現這個功能需要分頁機制。在這種情況下,相同大小的內存在引入分頁機制後可以同時存放更多的程序。這由進一步提高了存儲器的容量。
在介紹分頁機制之前,首先我們需要了解三種地址:
邏輯地址、線性地址、物理地址
這三種地址的關系如下圖:
這裡是地址轉換圖:OS-adressAfterPagingAndSegment
引入段頁式存儲之後才有完整的三種地址的概念。這時候邏輯地址通過分段機制轉換成線性地址,然後再通過分頁機制轉換成物理地址。
在沒有引入頁式存儲的情況下,邏輯地址通過分段機制轉換成的線性地址等於物理地址。
如果段式存儲和頁式存儲都不存在。那麼也就不存在邏輯地址和線性地址,我們對內存的所有操作都直接使用物理地址。
通過上面的分析,我們很容易明白分頁機器就像一個函數:
物理地址 = f(線性地址)
接下來,以二級頁表機制對分頁機制進行描述
分頁機制概述
二級頁表的分頁機制如下圖:
上圖的轉換使用兩級頁表,第一級叫做頁目錄,大小為4KB,存儲在一個物理頁中,每個表項4字節長,共有1024個表項。每個表項對應第二級的一個頁表,第二級的每一個頁表也有1024個表項,每一個表項對應一個物理頁。頁目錄表的表項簡稱PDE(Page Directory Entry),頁表的表項簡稱PTE(Page Table Entry)。
進行轉換時,先是從由寄存器cr3指定的頁目錄中根據線性地址的高10位得到頁表地址,然後在頁表中根據線性地址的第12到21位得到物理頁首地址,將這個首地址加上線性地址低12位便得到了物理地址。
分頁機制是否生效的開關位於cr0的最高位PG位。如下圖:
如果PG=1,則分頁機制生效。所以,當我們准備好了頁目錄表和頁表,並將cr3指向頁目錄表之後,只需要置PG位,分頁機制就開始工作了。
接下來描述PDE和PTE的結構和各位的詳細解釋:
PDE和PTE
下圖是PDE的結構:
下圖是PTE的結構:
下面是關於PDE和PTE中各位的解釋:
P 存在位,表示當前條目所指向的頁或頁表是否在物理內存中。P=0表示頁不在內存中,如果處理器試圖訪問此頁,將會產生頁異常(page-fault exception,#PF);P=1表示頁在內存中。
R / W 指定一個頁或者一組頁(比如,此條目是指向頁表的頁目錄條目)的讀寫權限。此位與U/S位和寄存器cr0中的WP位相互作用。R/W=0表示只讀;R/W=1表示可讀並可寫。
U / S 指定一個頁或者一組頁(比如,此條目是指向頁表的頁目錄條目)的特權級。此位與R/W位和寄存器cr0中的WP位相互作用。U/S=0表示系統級別(Supervisor Privilege Level),如果CPL為0、1或2,那麼它便是在此級別;U/S=1表示用戶級別(User Privilege Level),如果CPL為3,那麼它便是在此級別。
如果cr0中的WP位為0,那麼即便用戶級(User P.L.)頁面的R/W=0,系統級(Supervisor P.L.)程序仍然具備寫權限;如果WP位為1,那麼系統級(Supervisor P.L.)程序也不能寫入用戶級(User P.L.)只讀頁。
P W T 用於控制對單個頁或者頁表的緩沖策略。PWT=0時使用Write-back緩沖策略;PWT=1時使用Write-through緩沖策略。當cr0寄存器的CD(Cache-Disable)位被設置時會被忽略。
P C D 用於控制對單個頁或者頁表的緩沖。PCD=0時頁或頁表可以被緩沖;PCD=1時頁或頁表不可以被緩沖。當cr0寄存器的CD(Cache-Disable)位被設置時會被忽略。
A 指示頁或頁表是否被訪問。此位往往在頁或頁表剛剛被加載到物理內存中時被內存管理程序清零,處理器會在第一次訪問此頁或頁面時設置此位。而且,處理器並不會自動清除此位,只有軟件能清除它。
D 指示頁或頁表是否被寫入。此位往往在頁或頁表剛剛被加載到物理內存中時被內存管理程序清零,處理器會在第一次寫入此頁或頁面時設置此位。而且,處理器並不會自動清除此位,只有軟件能清除它。
A位和D位都是被內存管理程序用來管理頁和頁表從物理內存中換入和換出的。
P S 決定頁大小。PS=0時頁大小為4KB,PDE指向頁表。
P A T 選擇PAT(Page Attribute Table)條目。Pentium III以後的CPU開始支持此位,在此不予討論,並在我們的程序中設為0。
G 指示全局頁。如果此位被設置,同時cr4中的PGE位被置,那麼此頁的頁表或頁目錄條目不會在TLB中變得無效,即便cr3被加載或者任務切換時也是如此。
處理器會將最近常用的頁目錄和頁表項保存在一個叫做TLB(Translation Lookaside Buffer)的緩沖區中。只有在TLB中找不到被請求頁的轉換信息時,才會到內存中去尋找。這樣就大大加快了訪問頁目錄和頁表的時間。
當頁目錄或頁表項被更改時,操作系統應該馬上使TLB中對應的條目無效,以便下次用到此條目時讓它獲得更新。
當cr3被加載時,所有TLB都會自動無效,除非頁或頁表條目的G位被設置。
接下來看看cr3的結構:
cr3
cr3的結構如下圖:
cr3又叫做PDBR(Page-Directory Base Register)。它的高20位將是頁目錄表首地址的高20位,頁目錄表首地址的低12位會是零,也就是說,頁目錄表會是4KB對齊的。類似地,PDE中的頁表基址(PageTable Base Address)以及PTE中的頁基址(PageBase Address)也是用高20位來表示4KB對齊的頁表和頁。
至於第3位和第4位的兩個標志,我們暫時可以忽略它們。
對cr0、cr3、PDE、PTE的結構有了解之後,接下來編寫代碼啟動分頁機制:
編寫代碼啟動分頁機制
這裡不考慮特權級的變化,這樣更能專注於分頁機制的實現。
這裡僅列出新增代碼,完整代碼會放在本文的最後。
8 PageDirBase equ 200000h ; 頁目錄開始地址: 2M
9 PageTblBase equ 201000h ; 頁表開始地址: 2M+4K
...
19 LABEL_DESC_PAGE_DIR: Descriptor PageDirBase, 4095, DA_DRW;Page Directory
20 LABEL_DESC_PAGE_TBL: Descriptor PageTblBase, 1023, DA_DRW|DA_LIMIT_4K;Page Tables
...
34 SelectorPageDir equ LABEL_DESC_PAGE_DIR - LABEL_GDT
35 SelectorPageTbl equ LABEL_DESC_PAGE_TBL - LABEL_GDT
...
166 [SECTION .s32]; 32 位代碼段. 由實模式跳入.
167 [BITS 32]
168
169 LABEL_SEG_CODE32:
170 call SetupPaging
...
202 ; 啟動分頁機制 --------------------------------------------------------------
203 SetupPaging:
204 ; 為簡化處理, 所有線性地址對應相等的物理地址.
205
206 ; 首先初始化頁目錄
207 mov ax, SelectorPageDir ; 此段首地址為 PageDirBase
208 mov es, ax
209 mov ecx, 1024 ; 共 1K 個表項
210 xor edi, edi
211 xor eax, eax
212 mov eax, PageTblBase | PG_P | PG_USU | PG_RWW
213 .1:
214 stosd
215 add eax, 4096 ; 為了簡化, 所有頁表在內存中是連續的.
216 loop .1
217
218 ; 再初始化所有頁表 (1K 個, 4M 內存空間)
219 mov ax, SelectorPageTbl ; 此段首地址為 PageTblBase
220 mov es, ax
221 mov ecx, 1024 * 1024 ; 共 1M 個頁表項, 也即有 1M 個頁
222 xor edi, edi
223 xor eax, eax
224 mov eax, PG_P | PG_USU | PG_RWW
225 .2:
226 stosd
227 add eax, 4096 ; 每一頁指向 4K 的空間
228 loop .2
229
230 mov eax, PageDirBase
231 mov cr3, eax
232 mov eax, cr0
233 or eax, 80000000h
234 mov cr0, eax
235 jmp short .3
236 .3:
237 nop
238
239 ret
240 ; 分頁機制啟動完畢 ----------------------------------------------------------
上面的指令中,只有stosd沒有學過。類似的指令有stosb、stosw、stosd。這三個指令就是把al、ax、eax的內容存儲到edi指向的內存單元中,同時edi的值根據方向標志的值增加或者減少。這裡使用的是loop指令。它還可以同rep前綴聯合使用。這裡我沒找到設置方向標志位的指令,難道是初始時候方向標志位已經為0了?
上面的代碼實現的功能如下圖:
開頭的第207行和第208行將段寄存器es對應頁目錄表段,下面讓edi等於0,於是es:edi就指向了頁目錄表的開始。第214行的指令stosd第一次執行時就把eax中的PageTblBase|PG_P|PG_USU|PG_RWW存入了頁目錄表的第一個PDE。
那麼來看看這個PDE是什麼值。PageTblBase|PG_P|PG_USU|PG_RWW(第212行)讓當前(第一個)PDE對應的頁表首地址變成PageTblBase,而且屬性顯示其指向的是存在的可讀可寫的用戶級別頁表。
實際上,當為頁目錄表中的第一個PDE賦值時,一個循環就已經開始了。循環的每一次執行中,es:edi會自動指向下一個PDE,而第215行也將下一個頁表的首地址增加4096字節,以便與上一個頁表首尾相接。這樣,經過1024次循環(第209行由ecx指定)之後,頁目錄表中的所有PDE都被賦值完畢,它們的屬性相同,都為指向可讀可寫的用戶級別頁表,並且所有的頁表連續排列在以
PageTblBase為首地址的4MB(4096×1024)的空間中。
接下來的工作是初始化所有頁表中的PTE(第218行到第228行)。由於總共有1024×1024個PTE,於是將ecx賦值為1024×1024,以便讓循環進行1024×1024次。開始對es和edi的處理讓es:edi指向了頁表段的首地址,即地址PageTblBase處,也是第一個頁表的首地址。
第一個頁表中的第一個PTE被賦值為PG_P|PG_USU|PG_RWW,不難理解,它表示此PTE指示的頁首地址為0,並且是個可讀可寫的用戶級別頁。這同時意味著第0個頁表中第0個PTE指示的頁的首地址是0,於是線性地址0~0FFFh將被映射到物理地址0~0FFFh,即f(x)=x,其中0x0FFFh。接下來進行的循環初始化了剩下的所有頁表中的PTE,將4GB空間的線性地址映射到相同的物理地址。如上圖所示
這樣,頁目錄表和所有的頁表被初始化完畢。接下來到了正式啟動分頁機制的時候了。首先讓cr3指向頁目錄表(第230行和第231行),然後設置cr0的PG(第232行到第234行),這樣,分頁機制就啟動完成了。
運行結果如下:
從這裡我們看到啟動分頁機制後。我們無法在屏幕上看到分頁機制的影子。這是因為我們只是把所有的線性地址映射到完全相同的物理地址上,而並沒有對其做其他的操作。所以我們看不出來表面上的變化。而且這種方式會出現兩個問題:一是頁表顯然浪費得太多了,我們可能根本沒有那麼大的內存;二是我們除了“實現了”分頁,並沒有“得益於”分頁,也就是說,我們還沒有體會到分頁的妙處。上面的問題我們在下節介紹。
源代碼
; ==========================================
; pmtest6.asm
; 編譯方法:nasm pmtest6.asm -o pmtest6.com
; ==========================================
%include "pm.inc" ; 常量, 宏, 以及一些說明
PageDirBase equ 200000h ; 頁目錄開始地址: 2M
PageTblBase equ 201000h ; 頁表開始地址: 2M+4K
org 0100h
jmp LABEL_BEGIN
[SECTION .gdt]
; GDT
; 段基址, 段界限, 屬性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符
LABEL_DESC_PAGE_DIR: Descriptor PageDirBase, 4095, DA_DRW;Page Directory
LABEL_DESC_PAGE_TBL: Descriptor PageTblBase, 1023, DA_DRW|DA_LIMIT_4K;Page Tables
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len-1, DA_C+DA_32 ; 非一致代碼段, 32
LABEL_DESC_CODE16: Descriptor 0, 0ffffh, DA_C ; 非一致代碼段, 16
LABEL_DESC_DATA: Descriptor 0, DataLen-1, DA_DRW ; Data
LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA + DA_32 ; Stack, 32 位
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 顯存首地址
; GDT 結束
GdtLen equ $ - LABEL_GDT ; GDT長度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; GDT 選擇子
SelectorNormal equ LABEL_DESC_NORMAL - LABEL_GDT
SelectorPageDir equ LABEL_DESC_PAGE_DIR - LABEL_GDT
SelectorPageTbl equ LABEL_DESC_PAGE_TBL - LABEL_GDT
SelectorCode32 equ LABEL_DESC_CODE32 - LABEL_GDT
SelectorCode16 equ LABEL_DESC_CODE16 - LABEL_GDT
SelectorData equ LABEL_DESC_DATA - LABEL_GDT
SelectorStack equ LABEL_DESC_STACK - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT
; END of [SECTION .gdt]
[SECTION .data1] ; 數據段
ALIGN 32
[BITS 32]
LABEL_DATA:
SPValueInRealMode dw 0
; 字符串
PMMessage: db "In Protect Mode now. ^-^", 0 ; 進入保護模式後顯示此字符串
OffsetPMMessage equ PMMessage - $$
DataLen equ $ - LABEL_DATA
; END of [SECTION .data1]
; 全局堆棧段
[SECTION .gs]
ALIGN 32
[BITS 32]
LABEL_STACK:
times 512 db 0
TopOfStack equ $ - LABEL_STACK - 1
; END of [SECTION .gs]
[SECTION .s16]
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
mov [LABEL_GO_BACK_TO_REAL+3], ax
mov [SPValueInRealMode], sp
; 初始化 16 位代碼段描述符
mov ax, cs
movzx eax, ax
shl eax, 4
add eax, LABEL_SEG_CODE16
mov word [LABEL_DESC_CODE16 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE16 + 4], al
mov byte [LABEL_DESC_CODE16 + 7], ah
; 初始化 32 位代碼段描述符
xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_SEG_CODE32
mov word [LABEL_DESC_CODE32 + 2], ax
shr eax, 16
mov byte [LABEL_DESC_CODE32 + 4], al
mov byte [LABEL_DESC_CODE32 + 7], ah
; 初始化數據段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_DATA
mov word [LABEL_DESC_DATA + 2], ax
shr eax, 16
mov byte [LABEL_DESC_DATA + 4], al
mov byte [LABEL_DESC_DATA + 7], ah
; 初始化堆棧段描述符
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_STACK
mov word [LABEL_DESC_STACK + 2], ax
shr eax, 16
mov byte [LABEL_DESC_STACK + 4], al
mov byte [LABEL_DESC_STACK + 7], ah
; 為加載 GDTR 作准備
xor eax, eax
mov ax, ds
shl eax, 4
add eax, LABEL_GDT ; eax <- gdt 基地址
mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址
; 加載 GDTR
lgdt [GdtPtr]
; 關中斷
cli
; 打開地址線A20
in al, 92h
or al, 00000010b
out 92h, al
; 准備切換到保護模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 真正進入保護模式
jmp dword SelectorCode32:0
; 執行這一句會把 SelectorCode32 裝入 cs, 並跳轉到 Code32Selector:0 處
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
LABEL_REAL_ENTRY: ; 從保護模式跳回到實模式就到了這裡
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, [SPValueInRealMode]
in al, 92h ; ┓
and al, 11111101b ; ┣ 關閉 A20 地址線
out 92h, al ; ┛
sti ; 開中斷
mov ax, 4c00h ; ┓
int 21h ; ┛回到 DOS
; END of [SECTION .s16]
[SECTION .s32]; 32 位代碼段. 由實模式跳入.
[BITS 32]
LABEL_SEG_CODE32:
call SetupPaging
mov ax, SelectorData
mov ds, ax ; 數據段選擇子
mov ax, SelectorVideo
mov gs, ax ; 視頻段選擇子
mov ax, SelectorStack
mov ss, ax ; 堆棧段選擇子
mov esp, TopOfStack
; 下面顯示一個字符串
mov ah, 0Ch ; 0000: 黑底 1100: 紅字
xor esi, esi
xor edi, edi
mov esi, OffsetPMMessage ; 源數據偏移
mov edi, (80 * 10 + 0) * 2 ; 目的數據偏移。屏幕第 10 行, 第 0 列。
cld
.1:
lodsb
test al, al
jz .2
mov [gs:edi], ax
add edi, 2
jmp .1
.2: ; 顯示完畢
; 到此停止
jmp SelectorCode16:0
; 啟動分頁機制 --------------------------------------------------------------
SetupPaging:
; 為簡化處理, 所有線性地址對應相等的物理地址.
; 首先初始化頁目錄
mov ax, SelectorPageDir ; 此段首地址為 PageDirBase
mov es, ax
mov ecx, 1024 ; 共 1K 個表項
xor edi, edi
xor eax, eax
mov eax, PageTblBase | PG_P | PG_USU | PG_RWW
.1:
stosd
add eax, 4096 ; 為了簡化, 所有頁表在內存中是連續的.
loop .1
; 再初始化所有頁表 (1K 個, 4M 內存空間)
mov ax, SelectorPageTbl ; 此段首地址為 PageTblBase
mov es, ax
mov ecx, 1024 * 1024 ; 共 1M 個頁表項, 也即有 1M 個頁
xor edi, edi
xor eax, eax
mov eax, PG_P | PG_USU | PG_RWW
.2:
stosd
add eax, 4096 ; 每一頁指向 4K 的空間
loop .2
mov eax, PageDirBase
mov cr3, eax
mov eax, cr0
or eax, 80000000h
mov cr0, eax
jmp short .3
.3:
nop
ret
; 分頁機制啟動完畢 ----------------------------------------------------------
SegCode32Len equ $ - LABEL_SEG_CODE32
; END of [SECTION .s32]
; 16 位代碼段. 由 32 位代碼段跳入, 跳出後到實模式
[SECTION .s16code]
ALIGN 32
[BITS 16]
LABEL_SEG_CODE16:
; 跳回實模式:
mov ax, SelectorNormal
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov eax, cr0
and eax, 7FFFFFFEh ; PE=0, PG=0
mov cr0, eax
LABEL_GO_BACK_TO_REAL:
jmp 0:LABEL_REAL_ENTRY ; 段地址會在程序開始處被設置成正確的值
Code16Len equ $ - LABEL_SEG_CODE16
; END of [SECTION .s16code]