五种模式说明

8086:实模式

80286:实模式、保护模式

80386:实模式、保护模式、虚拟8086模式

Athlon:实模式、保护模式、虚拟8086模式、64位模式、兼容模式

时至今日,即使最新的 CPU 也保持着这 5 种模式

补充:固件也是一种软件,无论是经典 BIOS 还是 UEFI 都是为了初始化计算机硬件而开发的软件,只不过它一般被烧写在主板的 ROM 或 FLASH 芯片里,而不是在硬盘上。其实显卡硬盘网卡等也都有自己的固件。

为了迎合时代发展,我们的 OS 只运行在 64 位模式下,虚拟 8086、兼容模式暂不考虑,保护模式仅供过渡。

模式切换

控制寄存器

参考 Intel 官方文档

Intel® 64 and IA-32 architectures software developer’s manual combined volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4

CR0CR4就是 CPU 的控制寄存器,模式切换就是修改它们的值。除此以外还有 CR2CR3CR8 三个,CR8 只用在 64 位模式。EFER 寄存器是在上述控制寄存器基础上新增的,不是所有 CPU 都有。

控制寄存器的位,就是某些功能的开关:0 为关,1 为开。

从实模式到保护模式,只需要把 CR0 的 PE 位修改为 1 即可。(在此之前需要定义好 GDT,才能在保护模式下访问内存)

修改方式为(参考维基百科):

1
2
3
mov eax, cr0
or eax, 1
mov cr0, eax

PG 位代表内存分页开关,这是长模式的强制要求;而 CR4 的 PAE 位是物理内存拓展,它的一个用途就是与 CR0 的 PG 位合作选择内存分页方式。

EFER 的 LEM 位是长模式使能位。如果是纯 32 位的 CPU 不会支持长模式,也就没有 EFER。

从实模式到保护模式的切换步骤

  1. 定义 GDT

段描述符 GDT 的格式参考前面的内容。

Intel 规定,GDT 的第一个描述符必须是空描述符,没有为什么。它一共占据 8 个字节,所有的 64 位的每一位都是 0。这意味着它的起始地址是 0x00,而且大小也是 0。Intel 解释说它就相当于 NULL,用来初始化段选择子,解决了段选择子瞎指的问题。

除此之外,还至少需要有一个代码段描述符和一个数据段描述符。

  1. 告知 CPU

定义好 GDT 后要如何通知 CPU 呢?

首先 CPU 要知道 GDT 的内存地址;其次 CPU 要知道 GDT 有多大,好为它腾出足够的内存空间。进入保护模式后,内存地址就是 32 位了,所以 GDT 基地址占 32 位;GDT 的界限占 16 位(因为保护模式是 80286 引入的,80286 的段描述符只有 48 位,与 80386 的 64 位段描述符不同,段界限只有 16 位),所以一共需要告诉 CPU 48 位的信息。

告知的方法,是通过 LGDT 指令,后面跟一个内存地址。LGDT 指令会获取从这个地址开始的 6 个字节共 48 位数据保存起来,保存的位置就是 GDTR 寄存器。

  1. 修改 PE 位

当 CR0 的 PE 位变成 1 的一刹那,CPU 也就切换到了保护模式。从此以后的地址访问模式就变了,所以还得清空流水线的现有指令。

  1. 清空流水线

通过 jmp 指令跳转到一个 32 位的地址就行了。

模式切换的代码实现

80386 下使用 64 位段描述符。

红框部分是 80286 时代的,80386 进入 32 位时代,为了向前兼容,前 48 位不能改,因此在后面追加了绿框的部分。

这还不是最终模式,毕竟 64 位模式也有 GDT,而 64 位模式下的内存地址是 64 位的。

Type 的描述如下:

G 表示粒度,为 0 时计算段长使用的单位是字节,也就是段长 = 2202^{20} = 1MB。为 1 时计算段长使用的单位是 4KB, 也就是段长 = 220×2122^{20} \times 2^{12} = 4GB。

D/B 是默认的操作数宽度,0 代表 16 位,1 代表 32 位,根据 CPU 模式设置。

S 为 0 代表是系统段,为 1 代表这是数据段或代码段

L 表示是否是 64 位的代码段,这个位只能用在长模式/IA32-e 模式下。

数据段和代码段的 GDT 分别如下:

这里我们直接把基地址设置为 0000,段界限设置到最大 FFFF,即4GB。采用平坦模式访问内存。

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
LoadGDT:    ; 利用了空的 GDT
mov word [GDTStart], GDTEnd-GDTStart
mov dword [GDTStart+2], GDTStart
LGDT [GDTStart]
EnableProtectBit:
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
jmp CodeDescriptor: dword ProtectModeLand
BITS 32 ; 告知编译器后面生成32位代码
ProtectModeLand: ;32位代码落地
GDTStart:
NullDescriptor equ $-GDTStart
dw 0
dw 0
db 0
db 0
db 0
db 0
db 0
DataDescriptor equ $-GDTStart
dw 0xfff
dw 0
db 0
db 0x93
db 0xcf
db 0
CodeDescriptor equ $-GDTStart
dw 0xfff
dw 0
db 0
db 0x9b
db 0xcf
db 0
GDTEnd:

保护模式下显示字符

之前在实模式下,我们使用了两种方式显示字符,一种是写显存的方式,一种是使用中断。

在进入保护模式后,无法使用该中断了,要回归写显存的方式。

我们可以搞一个显存段,这个段的起始地址就是 0x000b8000,大小是 32KB,其定义如下:

1
2
3
4
5
6
7
VideoDescriptor equ $-GDTStart
dw 0x7fff
dw 0x8000
db 0x0b
db 0x93
db 0xc0
db 0

新指令 cld,作用是把 Eflags 寄存器的 DF 位清零。

movs 指令是一个家族,repo 指令一般跟 movs 指令家族配合使用

movsw,以字节为单位移动字符串

(未完待续)