五种模式说明
8086:实模式
80286:实模式、保护模式
80386:实模式、保护模式、虚拟8086模式
Athlon:实模式、保护模式、虚拟8086模式、64位模式、兼容模式
时至今日,即使最新的 CPU 也保持着这 5 种模式
补充:固件也是一种软件,无论是经典 BIOS 还是 UEFI 都是为了初始化计算机硬件而开发的软件,只不过它一般被烧写在主板的 ROM 或 FLASH 芯片里,而不是在硬盘上。其实显卡硬盘网卡等也都有自己的固件。
为了迎合时代发展,我们的 OS 只运行在 64 位模式下,虚拟 8086、兼容模式暂不考虑,保护模式仅供过渡。
模式切换
控制寄存器
参考 Intel 官方文档
CR0
、CR4
就是 CPU 的控制寄存器,模式切换就是修改它们的值。除此以外还有 CR2
、CR3
、CR8
三个,CR8
只用在 64 位模式。EFER 寄存器是在上述控制寄存器基础上新增的,不是所有 CPU 都有。
控制寄存器的位,就是某些功能的开关:0 为关,1 为开。
从实模式到保护模式,只需要把 CR0
的 PE 位修改为 1 即可。(在此之前需要定义好 GDT,才能在保护模式下访问内存)
修改方式为(参考维基百科):
1 | mov eax, cr0 |
PG 位代表内存分页开关,这是长模式的强制要求;而 CR4
的 PAE 位是物理内存拓展,它的一个用途就是与 CR0
的 PG 位合作选择内存分页方式。
EFER 的 LEM 位是长模式使能位。如果是纯 32 位的 CPU 不会支持长模式,也就没有 EFER。
从实模式到保护模式的切换步骤
- 定义 GDT
段描述符 GDT 的格式参考前面的内容。
Intel 规定,GDT 的第一个描述符必须是空描述符,没有为什么。它一共占据 8 个字节,所有的 64 位的每一位都是 0。这意味着它的起始地址是 0x00,而且大小也是 0。Intel 解释说它就相当于 NULL,用来初始化段选择子,解决了段选择子瞎指的问题。
除此之外,还至少需要有一个代码段描述符和一个数据段描述符。
- 告知 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 寄存器。
- 修改 PE 位
当 CR0 的 PE 位变成 1 的一刹那,CPU 也就切换到了保护模式。从此以后的地址访问模式就变了,所以还得清空流水线的现有指令。
- 清空流水线
通过 jmp 指令跳转到一个 32 位的地址就行了。
模式切换的代码实现
80386 下使用 64 位段描述符。
红框部分是 80286 时代的,80386 进入 32 位时代,为了向前兼容,前 48 位不能改,因此在后面追加了绿框的部分。
这还不是最终模式,毕竟 64 位模式也有 GDT,而 64 位模式下的内存地址是 64 位的。
Type 的描述如下:
G 表示粒度,为 0 时计算段长使用的单位是字节,也就是段长 = = 1MB。为 1 时计算段长使用的单位是 4KB, 也就是段长 = = 4GB。
D/B 是默认的操作数宽度,0 代表 16 位,1 代表 32 位,根据 CPU 模式设置。
S 为 0 代表是系统段,为 1 代表这是数据段或代码段
L 表示是否是 64 位的代码段,这个位只能用在长模式/IA32-e 模式下。
数据段和代码段的 GDT 分别如下:
这里我们直接把基地址设置为 0000,段界限设置到最大 FFFF,即4GB。采用平坦模式访问内存。
1 | LoadGDT: ; 利用了空的 GDT |
保护模式下显示字符
之前在实模式下,我们使用了两种方式显示字符,一种是写显存的方式,一种是使用中断。
在进入保护模式后,无法使用该中断了,要回归写显存的方式。
我们可以搞一个显存段,这个段的起始地址就是 0x000b8000,大小是 32KB,其定义如下:
1 | VideoDescriptor equ $-GDTStart |
新指令 cld,作用是把 Eflags 寄存器的 DF 位清零。
movs 指令是一个家族,repo 指令一般跟 movs 指令家族配合使用
movsw,以字节为单位移动字符串
(未完待续)