《ORANGE-s-一个操作系统实现》保护模式(2-3)

页式存储

说到这个,应该不会太陌生,但是对它可能值停留在理论学习阶段,没法像数据结构那样,理性的认识,下面我们先从几个问题出发

  • 什么叫做“页”
    所谓“页”,就是一块内存,在80386中,页的大小是固定的4096字节。而后来,页的大小还可以是2MB或者4MB,并且可以访问多于4GB的内存。
  • 逻辑地址、线性地址、物理地址
    在未打开分页机制时,线性地址等同于物理地址。当分页开启时,分段机制将逻辑地址转换成线性地址,然后在通过分页机制变为物理地址。
  • 为什么分页
    -分段管理机制,已经提供了很好的保护机制,而分页的主要目的是在线实现虚拟存储器。线性地址中,任意一个页都能映射到物理地址中的任何一个页。

    1.分页机制概述

    分页机制就像一个函数:
    物理地址 = f(线下地址)
    我们通过下图看看f是怎样的。
    mark
    如图所示,转换使用两级页表,第一级叫做页目录,大小为4KB,存储在一个物理页中,每个表项4字节长,公有1024个表项。每个表项对应第二级的一个页表,第二级的每一个页表也有1024个表项,每一个表项对应一个物理页。页目录的表项简称PDE,页表的表项简称PTE.
    进行转化时,先是从由寄存器cr3指定的页目录中根据线性地址的高10位得到页表地址,然后在页表中根据线性地址的第12到21位得到物理页首地址,将这个首地址加上线性地址低12位,便得到了物理地址。
    分页机制是否生效的开关位于cr0的最高位PG位。如果PG=1,则分页机制生效。

    2.编写代码启动分页机制

    下面,我们在pmtest2.asmde 基础进行修改,将实验内存写入和读取的描述符、代码以及数据统统去掉,并添加这样一个函数SetupPaging,代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    PageDirBase		equ	200000h	; 页目录开始地址: 2M
    PageTblBase equ 201000h ; 页表开始地址: 2M+4K
    ...
    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
    ...
    SelectorPageDir equ LABEL_DESC_PAGE_DIR - LABEL_GDT
    SelectorPageTbl equ LABEL_DESC_PAGE_TBL - LABEL_GDT
    ...
    ; 启动分页机制 --------------------------------------------------------------
    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
    ; 分页机制启动完毕 ----------------------------------------------------------

可以看到,PageDirBase和PageTblBase是两个宏,指定了页目录表和页表在内存中的位置。页目录表位于地址处2MB,有1024个表项,占用4KB空间爱你,紧接着页目录表便是页表,位于地址2MB+4KB处。
为了罗技清晰和代码编写便捷,我们分别定义两个段,用来存放页目录表和页表,大小分别是4KB和4MB。
为了简单起见,我们就爱那个所有线性地址映射到相同的物理地址,于是线性地址和物理地址的关系符合下面的公式:
物理地址 = f(线性地址) = 线性地址
下面,我们看看PDE和PTE

3.PDE和PTB

PDE:
mark
PTE:
mark

  • 读写权限。此位与U/S位和寄存器cr0中的WP位相互作用。R/W=0表示只读;R/W=1表示刻度并可写。
  • U/S指定一个页或者一组页的特权级。此位与R/W位和寄存器cr0中的WP位相互作用。U/S=0表示系统级别,如果CPL为0、1、2那么他便是在此级别;U/S=1表示用户级别,如果CPL为3,那么他便是在此级别。
    如果cr0中WP位为0,那么即便用户级页面的R/W=0,系统级陈翔任然具备写权限;如果WP位为1,那么系统级程序也不能写入用户级只读页。
  • PWT用于控制对单个页或者页表的缓冲策略。
  • PCD用于控制对单个页湖综合页表的缓冲。
  • A指示页或页表是否被访问。
  • D指示页或页表是否被写入
  • PS决定页大小
  • PAT选择PAT条目
  • G指示全局页
    CPU会将最近常用的页目录和页表项保存在一个叫做TLB的缓冲区中。只有在TLB中找不到被请求页的转换信息时,才会到内存中去寻找。
    当页目录或页表项被更改时,操作系统应该马上使TLB中对应的条目无效,以便下次使用到此条目时让他获得更新。
    当cr0被加载时,所有TLB都会自动无效,除非页或页表条目的G位被设置。

    4.cr3

    cr0指向页目录表,它的结构如图:
    mark
    cr3又叫做PDBR。它的高20位将是页目录表首地址的高20位,页目录首地址的低12位会是0,也就是说页目录表会是4KB对齐的。类似的,PDE中的页表基址以及PTE的页基址也是如此。

    5.克勤克俭用内存

    前面,我们用来 4MB的空间来存放页表,并用它映射了4GB的内存空间,而我们的物理内存不见得有这么大,这显然是太浪费了。如果我们的内存总数只有16MB的话,知识页表就占有25%的内存空间爱你了。而实际上如果仅仅是对等映射的话,16MB的内存只要4个页表就够了。所以我们有必要知道内存有多大,然后根据内存大小确定多少页表是够用的。
    那么程序如何知道机器内存有多少内存呢?实际上方法不止一个,在此,我们仅介绍一种通用性比较器的,就是利用中断15h.
    在调用它之前,需要填充如下寄存器:
  • eax int 15h 可完成许多工作,主要由ax的值决定,我们想要获取内存信息,需要将ax赋值为0E820h。
  • ebx 放置着“后续值”,第一次调用时ebx必须为0.
  • es:di 指向一个地址范围描述符结构ARDS,BIOS将会填充此结构。
  • ecx es:di所指向的地址范围描述符结构的大小,以字节为单位。
  • edxd 0534D4150h(‘SMAP’) BIOS将会使用此标志,对调用者将要请求的系统映像信息进行校验,这些信息会被BIOS放置到es:di所指向的结构中。
    中断调用之后,结果存放于下列寄存器之中。
  • CF CF=0表示没有错误,否则存在错误。
  • eax 0534D4150h(‘SMAP’).
  • es:di 返回的地址房范围描述符结构指针,和输入值相同。
  • ecx BIOS填充在地址范围描述中的字节数量,被BIOS所返回的最小值是20字节。
  • ebx 这里放置着为等到下一个地址描述符所需要的后续值,这个值的实际形势依赖于具体的BIOS的实现,调用者不必关心他的具体形式,只需在下次迭代时将其原封不懂的放置在ebx中,就可以通过它获取下一个地址范围描述符。如果它的值为0,表示它是组后一个地址范围描述符。
    mark

    6.进一步体会分页机制

    在此之前,不知道你有没有注意股哦一个细节,如果你写一个程序,并改个名复制一份,然后同时调用,你会发现,从变量地址到寄存器的值,几乎全部都是一样的!这就是分页机制的功劳,下面我们就来摸摸你下这个效果。
    先执行某个线性地址处的模块,然后通过改变cr3来转换地址映射关系,再执行同一个线性地址处的模块,由于地址映射已经改变,所以两次得到的应该是不同的输出。
    映射关系转化前的情形如图:
    mark
    从上图很清楚的可以看到,LinearAddrDemo地址映射到ProcFoo打印出红色的字符串Foo,所以执行时我们应该可以看到红色的Foo。
    随后,我们改版地址映射关系,变化成如图所示:
    mark
    页目录表和页表的切换让LinearAddrDemo映射到ProcBar处,所以当我们再一次调用过程ProcPagingDemo时,程序将装异到ProcBar处执行,我们将看到红色的字符串Bar。
    mark

    中断和异常

    中断我们一直在用,最近的一次是通过int 15h得到了计算机内存信息。但是这都是在实模式下进行的,然后在保护模式下显示出来。
    这是因为在保护模式下,中断机制发生了很大变化,原来的中断向量表已经被IDT所代替,实模式下能用的BIOS中断在保护模式下已经不能用了。你可能没有听过IDT,它也是个描述符,叫做中断描述符表。IDT中的描述符可以是下面三种之一:
  • 中断门描述符
  • 陷阱门描述符
  • 任务门描述符
    IDT的作用是将每一个中断向量和一个描述符对应起来。
    下图是中断向量到中断处理程序的对应过程。
    mark
    联系调用门,我们知道,其实中断门和陷阱门的作用机理几乎是一样的,只不过使用调用门时使用call指令,而这里我们使用int指令。
    其中,IDT中的任务门在某些操作系统中根本就没有用到,所以我们不做过多关注。
    对比调用门我们知道,在中断门和陷阱门中BYTE4的低5位变成了保留位,而不再是Param Count。而且,表示TYPE的4位也将变为0XE(中断门或0XF(陷阱门。
    知道这些还不够,因为中断还涉及处理器与硬件的联系等。

    中断和异常机制

    中断和异常都是在程序执行过程中的强制转移,转移到相应的处理程序。中断通常在程序执行时因为硬件而随机发生,它们通常用来处理处理器外部的事件,比如外围设备的请求。当然软件也可以通过int n指令来产生中断。异常则通常在处理器执行指令过程中检测到错误时发生。
    他们通俗来讲,都是软件或者硬件发生了某种情形而通知处理器的行为。
    那么。处理器可以处理哪些问题,以及如何处理呢?下表给出了答案:
    markmark
    三种错误类型:
  • Fault 是一种可被更正的异常,而且一旦被更正,程序可以不失连续地继续执行。当一个fault发生时,处理器会吧产生fault的指令之前的状态保存起来。
  • Trap 是一种在发生trap的指令之后立即被报告的异常,它也允许程序或任务不失连续性的继续执行。异常处理程序的返回地址将会是产生trap的指令之后的那条指令。
  • Abort 是一种不总是报告精确异常发生未知的异常,他不允许陈翔或任务继续执行,而是用来报告严重错误的。

    外部中断

    中断有外部中断,由硬件产生的中断,另一种是由指令int n产生的中断。
    外部中断,分为不可屏蔽中断(NMI)和可屏蔽中断两种。分别由CPU的两根引脚NMI和INTR来接收。如图:
    mark
    可屏蔽中断与CPU的关系是通过对可编程中断控制器8259A建立起来的。8259A是中断机制中所有外围设备的一个代理,这个代理不但可以根据优先级在同时发生中断的设备中选择应该处理的请求,而且可以通过对其寄存器的设置来屏蔽或打开相应的中断。
    主8259A对应的端口地址为20h和21h,从8259A对应的端口地址是A0h和A1h。

    编程操作8259A

    对8259A的设置是通过向相应的端口写入特定的ICW(Initialization Command Word)来实现的,它的格式如下:
    mark
    下面是设置8259A的代码项目地址:
    下载地址
    密码:akis
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    ; Init8259A ---------------------------------------------------------------------------------------------
    Init8259A:
    mov al, 011h
    out 020h, al ; 主8259, ICW1.
    call io_delay

    out 0A0h, al ; 从8259, ICW1.
    call io_delay

    mov al, 020h ; IRQ0 对应中断向量 0x20
    out 021h, al ; 主8259, ICW2.
    call io_delay

    mov al, 028h ; IRQ8 对应中断向量 0x28
    out 0A1h, al ; 从8259, ICW2.
    call io_delay

    mov al, 004h ; IR2 对应从8259
    out 021h, al ; 主8259, ICW3.
    call io_delay

    mov al, 002h ; 对应主8259的 IR2
    out 0A1h, al ; 从8259, ICW3.
    call io_delay

    mov al, 001h
    out 021h, al ; 主8259, ICW4.
    call io_delay

    out 0A1h, al ; 从8259, ICW4.
    call io_delay

    mov al, 11111110b ; 仅仅开启定时器中断
    ;mov al, 11111111b ; 屏蔽主8259所有中断
    out 021h, al ; 主8259, OCW1.
    call io_delay

    mov al, 11111111b ; 屏蔽从8259所有中断
    out 0A1h, al ; 从8259, OCW1.
    call io_delay

    ret
    ; Init8259A ---------------------------------------------------------------------------------------------

这段代码分别往主、从两个8259A各写入了4个ICW。在往8259A写入ICW2时,我们看到IRQ0对应了中断向量号20h,于是,IRQ0~IRQ7就对应中断向量20h~27h;类似的还有其他的。
在代码后半部分,我们通过对端口21h和A1h的操作屏蔽了所有的外部中断,这一次写入的不再是ICW了,而是OCW(Operation Control Word)格式如下:
mark
可见,若想屏蔽某一个中断,将对应的那一位设为1即可。

建立IDT以及实现一个中断

对8259A操作完成后,就是建立IDT了。
为了方便操作,我们把IDT放进一个单独的段中。在pmtest9a.asm中可以看到,这个iDT不能再简单了,全部的255个描述符完全相同,都设置指向SelectorCode32:SpuriousHandler的中断门。SUpriousHandler也很简单,在屏幕的右上角打印红色的!,然后进入死循环。
我们可以修改下IDT,把80h号中断单独列出来,并新增加一个函数来处理这个中断:UserIntHandler,它和SuprioustHandler很类似,只是在函数末尾通过iretd指令返回,而不是进入四循环。代码如pmtest9c.asm,运行结果如下:
mark

时钟中断

可屏蔽中断与NMI的区别在于是否收到IF位的影响,而8259A的中断屏蔽寄存器(IMR)也影响着中断是否会被响应。所以,外部可屏蔽中断的发生就受到两个因素的影响,只有当IF位为1,并且IMR相应位为0时才会发生。
在代码pmtest9.asm的387行到392行可以看到,这个中断处理程序很简单,除了发送EOI的两行语句以及iretd,只有一条指令,就是把屏幕第0行、第70行的字符增一,变成ASCII中位于它后面的字符、由于第0行、第70行已被写入字符I,所以第一次中断发生时,那里会变成J,再次中断就变成K,以后每发生一次时钟中断,字符就会变动一次,就会看到不断变化中的字符。
mark

保护模式下的I/O

保护模式对I/O也做了限制,用户进程如果不被允许,是无法进行I/O操作的,这种限制通过两个方面来实现,IOPL和I/O许可位图。

  • IOPL
    她位于寄存器eflags的第12、13位,如图
    mark
    指令in、ins、out、outs、cli、sti只有在CPL<=IOPL时才执行。
    可以改变IOPL的指令只有popf和iretd,只有运行在ring0的程序才能将其改变。运行在低特权级下的程序无法改变IOPL.
    指令popf同样可以用来改变IF,只有CPL<=IOPL时,popf才可以成功将IF改变。
  • I/O许可位图
    之所以叫做位图,是因为它的每一位表示一个字节的端口地址是否可用,若为0表示可用,若为1表示不可用。由于每一个任务都可以有单独的TSS,所以每一个任务可以有它单独的I/O许可位图。
    I/O许可位图必须以0FFh结尾。若I/O许可位图基址大于或等于TSS段界限,就表示没有I/O许可位图,若CPL<=IOPL,则所有I/O指令都会引起异常。I/O许可位图的使用使得即时在同一特权级下不同的任务也可以有不同的I/O访问权限

    保护模式小结

    “保护模式”包含如下几个方面的含义:
  • 在GDT、LDT以及IDT中,每一个描述符都有自己的界限和属性等内容,是对描述符所描述对象的一种限定和保护
  • 分页机制中的PDE和PTE都含有R/W以及U/S位,提供页级保护
  • 页式存储的使用使得应用程序使用的是线性地址空间而不是物理地址,于是物理内存就被保护起来
  • 中断不再像实模式下一样使用,也提供特权检验等内容
  • I/O指令不再随便使用,于是端口被保护起龙
  • 在程序运行过程中,如果遇到不同特权级间的访问等情况,会对CPL、RPL、DPL、IOPL等内容进行非常严格的检验,同时可能伴随堆栈的切换,这都对不同层级的程序进行了保护。
您的支持将鼓励我继续创作!