程序员人生 网站导航

一个操作系统的实现(6)-初识分页机制

栏目:服务器时间:2016-06-03 13:18:15

这节依然是从实现的角度来说述分页机制。

为何要引入分页机制。我们都知道分段机制是为了提供保护机制,那末为何还要引入分页机制呢?

为何引入分页机制

想象1下这样1种情况:假定我们用的计算机物理内存是4GB,但是我们的程序大小是5GB。那末这个时候我们没法将程序全部放到内存中,也就没法运行程序。分页机制引入的缘由之1就是为了解决这个问题。分页机制的引入实现了虚拟存储器的机制。

另外,程序履行具有局部性,也就是说1段时间内,只需要程序代码中的1小部份(相对全部程序)就能够实现程序的履行。那末我们就不用把所有代码和数据都寄存在内存中,而是将现在或很近的将来需要的代码和数据放入内存就好了。要实现这个功能需要分页机制。在这类情况下,相同大小的内存在引入分页机制后可以同时寄存更多的程序。这由进1步提高了存储器的容量。

在介绍分页机制之前,首先我们需要了解3种地址:

逻辑地址、线性地址、物理地址

这3种地址的关系以下图:

<font color=”red>这里是地址转换图:OS-adressAfterPagingAndSegment

引入段页式存储以后才有完全的3种地址的概念。这时候候逻辑地址通过分段机制转换成线性地址,然后再通过分页机制转换成物理地址。

在没有引入页式存储的情况下,逻辑地址通过分段机制转换成的线性地址等于物理地址。

如果段式存储和页式存储都不存在。那末也就不存在逻辑地址和线性地址,我们对内存的所有操作都直接使用物理地址。

通过上面的分析,我们很容易明白分页机器就像1个函数:

物理地址 = f(线性地址)

接下来,以2级页表机制对分页机制进行描写

分页机制概述

2级页表的分页机制以下图:

上图的转换使用两级页表,第1级叫做页目录,大小为4KB,存储在1个物理页中,每一个表项4字节长,共有1024个表项。每一个表项对应第2级的1个页表,第2级的每个页表也有1024个表项,每个表项对应1个物理页。页目录表的表项简称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 指定1个页或1组页(比如,此条目是指向页表的页目录条目)的读写权限。此位与U/S位和寄存器cr0中的WP位相互作用。R/W=0表示只读;R/W=1表示可读并可写。

  • U / S 指定1个页或1组页(比如,此条目是指向页表的页目录条目)的特权级。此位与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 唆使页或页表是不是被访问。此位常常在页或页表刚刚被加载到物理内存中时被内存管理程序清零,处理器会在第1次访问此页或页面时设置此位。而且,处理器其实不会自动清除此位,只有软件能清除它。

  • D 唆使页或页表是不是被写入。此位常常在页或页表刚刚被加载到物理内存中时被内存管理程序清零,处理器会在第1次写入此页或页面时设置此位。而且,处理器其实不会自动清除此位,只有软件能清除它。
    A位和D位都是被内存管理程序用来管理页和页表从物理内存中换入和换出的。

  • P S 决定页大小。PS=0时页大小为4KB,PDE指向页表。

  • P A T 选择PAT(Page Attribute Table)条目。Pentium III以后的CPU开始支持此位,在此不予讨论,并在我们的程序中设为0。

  • G 唆使全局页。如果此位被设置,同时cr4中的PGE位被置,那末此页的页表或页目录条目不会在TLB中变得无效,即使cr3被加载或任务切换时也是如此。

处理器会将最近经常使用的页目录和页表项保存在1个叫做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位的两个标志,我们暂时可以疏忽它们。

cr0cr3PDEPTE的结构有了解以后,接下来编写代码启动分页机制:

编写代码启动分页机制

这里不斟酌特权级的变化,这样更能专注于分页机制的实现。

这里仅列出新增代码,完全代码会放在本文的最后。

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没有学过。类似的指令有stosbstoswstosd。这3个指令就是把alaxeax的内容存储到edi指向的内存单元中,同时edi的值根据方向标志的值增加或减少。这里使用的是loop指令。它还可以同rep前缀联合使用。这里我没找到设置方向标志位的指令,难道是初始时候方向标志位已为0了?

上面的代码实现的功能以下图:

开头的第207行和第208即将段寄存器es对应页目录表段,下面让edi等于0,因而es:edi就指向了页目录表的开始。第214行的指令stosd第1次履行时就把eax中的PageTblBase|PG_P|PG_USU|PG_RWW存入了页目录表的第1个PDE。

那末来看看这个PDE是甚么值。PageTblBase|PG_P|PG_USU|PG_RWW(第212行)让当前(第1个)PDE对应的页表首地址变成PageTblBase,而且属性显示其指向的是存在的可读可写的用户级别页表。

实际上,当为页目录表中的第1个PDE赋值时,1个循环就已开始了。循环的每次履行中,es:edi会自动指向下1个PDE,而第215行也将下1个页表的首地址增加4096字节,以便与上1个页表首尾相接。这样,经过1024次循环(第209行由ecx指定)以后,页目录表中的所有PDE都被赋值终了,它们的属性相同,都为指向可读可写的用户级别页表,并且所有的页表连续排列在以
PageTblBase为首地址的4MB(4096×1024)的空间中。

接下来的工作是初始化所有页表中的PTE(第218行到第228行)。由于总共有1024×1024个PTE,因而将ecx赋值为1024×1024,以便让循环进行1024×1024次。开始对es和edi的处理让es:edi指向了页表段的首地址,即地址PageTblBase处,也是第1个页表的首地址。

第1个页表中的第1个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行),这样,分页机制就启动完成了。

运行结果以下:

从这里我们看到启动分页机制后。我们没法在屏幕上看到分页机制的影子。这是由于我们只是把所有的线性地址映照到完全相同的物理地址上,而并没有对其做其他的操作。所以我们看不出来表面上的变化。而且这类方式会出现两个问题:1是页表明显浪费得太多了,我们可能根本没有那末大的内存;2是我们除“实现了”分页,并没有“得益于”分页,也就是说,我们还没有体会到分页的妙处。上面的问题我们在下节介绍。

源代码

; ========================================== ; pmtest6.asm ; 编译方法:nasm pmtest6.asm -o pmtest6.com ; ========================================== %include "pm.inc" ; 常量, 宏, 和1些说明 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⑴, DA_C+DA_32 ; 非1致代码段, 32 LABEL_DESC_CODE16: Descriptor 0, 0ffffh, DA_C ; 非1致代码段, 16 LABEL_DESC_DATA: Descriptor 0, DataLen⑴, 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 ; 履行这1句会把 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 ; 下面显示1个字符串 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]
------分隔线----------------------------
------分隔线----------------------------

最新技术推荐