保护模式
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
及之后的x86
CPU对应的32位运行模式称为保护模式
。
因为x86
CPU是兼容之前8086
CPU的,而且x86
CPU启动后默认运行在实模式
下。这也是为什么我们前面的代码放在最新的x86
CPU上也可以运行的很happy的原因。
保护模式寻址方式
保护模式下,CPU分段寻址方式与实模式不再相同。操作系统将内存进行分段,并将分段信息以数组的形式存储在内存中。数组的每一项对应一个分段信息,称为段描述符(Segment Descriptor)
,这个数组被称之为段描述符表
。全局有一个总的段描述符表,称为全局段描述符表(GDT)
,当然,也有局部段描述符表(LDT)
。
可以想想,所有的内存寻址都需要通过GDT
表进行,为了加速内存寻址,CPU提供了GDTR寄存器
用于存储GDT
对应的内存起始地址。同时,CPU提供了LGDT指令
用于将GDT的内存起始地址加载到GDTR
寄存器中。
有了全局段描述符表GDT
,段寄存器中的值就不再像实模式下那样,是段的起始地址了。而是用来筛选出GDT
表中对应的某一项的,称之为段选择子(Segment selector)
。
这样,保护模式下的寻址方式大概如下:
- 操作系统开发者在内存中按格式创建出
GDT
- 操作系统启动后通过
LGDT
指令将内存中GDT
的起始地址加载到GDTR
中 - 寻址时,
[CS:IP]
中,段寄存器的值被CPU理解为段选择子
,通过CPU某些运算检查,从GDTR
中找出某一项段描述符
段描述符
通过CPU某些运算检查,得到段基址
,加上偏移地址,得到最终地址结果
怎么样,被这些概念绕晕了吧? ^_^
保护
也许你会好奇,第3步的段选择子
不应该就是一个数组下标吗?第4步中的段描述符
不就是一个内存起始地址吗?如果让我来设计CPU,我也会这么干。但历史上,从8086到80386发展过程中,也许发生了很多因为内存访问导致的bug,导致Intel决定,在CPU中设计一套内存访问控制,权限检查机制。
因此,第3步段选择子
除了数组下表的功能外,还存储了一些其它用于内存访问检查的信息,第4步段描述符
除了存储内存起始地址,也存储了其它对应信息。这些信息在内存访问时,CPU会按照设计规则对齐进行检查,防止非法访问。这也就是保护模式中所谓的保护的意思。
数据结构
段描述符
段选择子
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'
小结