CPU保护模式

我们知道,操作系统是对计算机各种硬件资源进行管理的,通过提供各种API,方便各种应用使用计算机硬件完成各自功能。因此,开发操作系统需要受硬件实现限制,也可以理解为按照硬件接口约定调用硬件能力。比如启动扇区的最后两个字节必须是0xAA 0x55,这就是一种约定,不这样做就没法正常启动。对于386指令集的CPU,这样的硬件接口约定和指导汇总就是《Intel 80386 程序员参考手册》

随着CPU功能的日益强大,这种硬件上的约定越来越多,其中很多还是特定历史原因造成的,或为了保持兼容性而设计的。这就导致我们需要了解的细节很多,而且不会很直观。保护模式就是实现操作系统过程中将会遇到的第一座大山,因为我们要借助CPU硬件能力实现。

实模式&保护模式

我们前面访问内存是采用的[CS:IP]=CS<<4+IP的方式就是实模式下的内存寻址方式。Intel 8086 CPU只有16位,到了80386时代(286存在时间很短),CPU进入了32位,这时再也不能采用之前8086 16位CPU的寻址方式了。同时,Intel在硬件设计中加入了很多内存访问检查等保护机制。为了与之前的CPU特性区分,Intel将8086对应的16位运行模式称为实模式,将386及之后的x86CPU对应的32位运行模式称为保护模式

因为x86CPU是兼容之前8086CPU的,而且x86CPU启动后默认运行在实模式下。这也是为什么我们前面的代码放在最新的x86CPU上也可以运行的很happy的原因。

保护模式寻址方式

保护模式下,CPU分段寻址方式与实模式不再相同。操作系统将内存进行分段,并将分段信息以数组的形式存储在内存中。数组的每一项对应一个分段信息,称为段描述符(Segment Descriptor),这个数组被称之为段描述符表。全局有一个总的段描述符表,称为全局段描述符表(GDT),当然,也有局部段描述符表(LDT)

可以想想,所有的内存寻址都需要通过GDT表进行,为了加速内存寻址,CPU提供了GDTR寄存器用于存储GDT对应的内存起始地址。同时,CPU提供了LGDT指令用于将GDT的内存起始地址加载到GDTR寄存器中。

有了全局段描述符表GDT,段寄存器中的值就不再像实模式下那样,是段的起始地址了。而是用来筛选出GDT表中对应的某一项的,称之为段选择子(Segment selector)

这样,保护模式下的寻址方式大概如下:

  1. 操作系统开发者在内存中按格式创建出GDT
  2. 操作系统启动后通过LGDT指令将内存中GDT的起始地址加载到GDTR
  3. 寻址时,[CS:IP]中,段寄存器的值被CPU理解为段选择子,通过CPU某些运算检查,从GDTR中找出某一项段描述符
  4. 段描述符通过CPU某些运算检查,得到段基址,加上偏移地址,得到最终地址结果

怎么样,被这些概念绕晕了吧? ^_^

保护

也许你会好奇,第3步的段选择子不应该就是一个数组下标吗?第4步中的段描述符不就是一个内存起始地址吗?如果让我来设计CPU,我也会这么干。但历史上,从8086到80386发展过程中,也许发生了很多因为内存访问导致的bug,导致Intel决定,在CPU中设计一套内存访问控制,权限检查机制。

因此,第3步段选择子除了数组下表的功能外,还存储了一些其它用于内存访问检查的信息,第4步段描述符除了存储内存起始地址,也存储了其它对应信息。这些信息在内存访问时,CPU会按照设计规则对齐进行检查,防止非法访问。这也就是保护模式中所谓的保护的意思。

数据结构

段描述符

段描述符

段选择子

段选择子

GDTR寄存器

GDTR

从实模式到保护模式

从实模式切换到保护模式,要进行如下3步操作,这些操作都是Intel CPU要求的。

  • 打开A20 Gate
  • 加载GDT
  • 将CR0的PE位置1

A20 Gate历史背景

8086下,CPU是16位的,地址总线20位,对应1M内存。当程序访问大于1M的地址时,系统会贴心的对地址按1M求模。这个技术称为wrap-around

到了保护模式,我们需要访问大于1M的内存,需要关闭wrap-around机制。而这个机制,因为历史原因,被键盘控制器上的第21根地址线(A20 Gate)控制。当打开A20 Gate时,才能突破1M的内存限制。

创建并记载GDT

我们将内存空间划分为3个段。根据规定,第1个段必须为空。因为GDT中4项如下:

;0描述符
    dd    0x00000000
    dd    0x00000000
;1描述符(4GB代码段描述符)
    dd    0x0000ffff
    dd    0x00cf9800
;2描述符(4GB数据段描述符)
    dd    0x0000ffff
    dd    0x00cf9200
;3描述符(28Kb的视频段描述符)
    dd    0x80000007
    dd    0x00c0920b

lgdt_value:
    dw $-gdt-1    ;高16位表示表的最后一个字节的偏移(表的大小-1) 
    dd gdt        ;低32位表示起始位置(GDT的物理地址)

跳转至32位模式

protect_mode:
;进入32位
    lgdt [lgdt_value]
    in al, 0x92
    or al, 0000_0010b
    out 0x92, al
    cli
    
    mov eax, cr0
    or eax, 1
    mov cr0, eax
    
    jmp dword SELECTOR_CODE:main

[bits 32]
;正式进入32位

main:

测试

64位视频段描述符,我们展开后如下图:

对比段描述符结构,知道视频段对应的段基址为0xb8000,正好为显卡黑白模式的起始地址。根据段选择子的数据结构定义,可以定义视频段选择子为3<<3,这样就可以向显存里写入字符进行测试了。

SELECTOR_VIDEO    equ    0x0003<<3

mov ax, SELECTOR_VIDEO
mov gs, ax
mov byte [gs:0xa0],'3'
mov byte [gs:0xa2],'2'
mov byte [gs:0xa4],'m'
mov byte [gs:0xa6],'o'
mov byte [gs:0xa8],'d'

小结