cpu工作模式¶
以下主要来自 LMOS 大佬的操作系统课程中记录的笔记
按照 CPU 功能升级迭代的顺序,CPU 的工作模式有:
- 实模式
- 保护模式
- 长模式
实模式¶
实模式又称实地址模式:
- 运行真实的指令,不区分直接执行
- 发往内存的地址是真实的,对任何地址不加限制地发往内存
实模式寄存器¶

实模式下访问内存¶

可以发现所有的内存地址都是:
- 段寄存器左移 4 位
- 再加上一个通用寄存器中的值或者常数形成地址
- 最后由这个地址去访问内存
这就是分段内存管理模型。
代码段是由 CS 和 IP 确定的,而栈段是由 SS 和 SP 段确定的。
实模式下的汇编代码,共 16 位:
data SEGMENT ;定义一个数据段存放Hello World!
hello DB 'Hello World!$' ;注意要以$结束
data ENDS
code SEGMENT ;定义一个代码段存放程序指令
ASSUME CS:CODE,DS:DATA ;告诉汇编程序,DS指向数据段,CS指向代码段
start:
MOV AX,data ;将data段首地址赋值给AX
MOV DS,AX ;将AX赋值给DS,使DS指向data段
LEA DX,hello ;使DX指向hello首地址
MOV AH,09h ;给AH设置参数09H,AH是AX高8位,AL是AX低8位,其它类似
INT 21h ;执行DOS中断输出DS指向的DX指向的字符串hello
MOV AX,4C00h ;给AX设置参数4C00h
INT 21h ;调用4C00h号功能,结束程序
code ENDS
END start
实模式中断¶
实模式下的中断实现过程是先保存 CS 和 IP 寄存器,然后装载新的 CS 和 IP 寄存器。
中断的产生分为两种:
- 硬件中断(中断控制器给 CPU 发送了一个电子信号,CPU 会对这个信号作出应答。随后中断控制器会将中断号发送给 CPU)
- 软件中断( CPU 执行了
INT指令,这个指令后面会跟随一个常数,这个常数即是软中断号)
实现中断需要:
- 内存中放一个中断向量表
- 中断向量表的地址和长度由 CPU 的特定寄存器 IDTR 指向
实模式下,表中的一个条目由代码段地址和段内偏移组成。CPU 根据 IDTR 通过中断号计算中断向量条目,进而装载 CS(装入代码段基地址)、IP(装入代码段内偏移)寄存器,最终响应中断。
保护模式¶
为解决实模式中 CPU :
- 对任何指令都不加区分
- 对内存的访问都不加限制
基于上述问题 CPU 实现了保护模式。
同时为了解决软件规模增大带来的寻址问题,保护模式下的寄存器和运算单位都要扩展成 32 位。
保护模式寄存器¶

保护模式相比于实模式,增加了一些控制寄存器和段寄存器,扩展通用寄存器的位宽,所有的通用寄存器都是 32 位的,还可以单独使用低 16 位,这个低 16 位又可以拆分成两个 8 位寄存器。
保护模式特权级¶
为区分指令和资源可以被访问,CPU 实现了 特权级。

可以看到从外到内各权级之间的联系与区别。如 R0 拥有最大权力,可以访问低特权级的资源。
保护模式段描述符¶
保护模式下内存还是分段模型,要对内存进行保护,就可以转换成对段的保护。
- 段描述符
保护模式下 16 位的段寄存器放不下 32位的段基地址和段内偏移,通过借内存空间,把描述一个段的信息封装成特定格式的段描述符,放在内存中。

- 全局描述符
多个段描述符在内存中形成全局段描述符表,该表的基地址和长度由 CPU 和 GDTR 寄存器指示。

- 访问段
- 保护模式下段寄存器会存放段描述符索引(而不是段基地址)
访问内存地址时,段寄存器中的索引结合 GDTR 寄存器找到内存中的段描述符,再根据其中段信息判断是否能访问成功。
保护模式段选择子¶
保护模式下的段寄存器存放的有影子寄存器、段描述符索引、描述符表索引、权限级别组成的:
- 影子寄存器:硬件操作,高速缓存提升性能,64位适配段描述符数据
- 低三位的 TI 和 RPL:对齐段描述符的8字节,每个索引低3位都为0
- CPL(当前权限级别):通常由 CS 和 SS 中 RPL 组成,当 CPL 大于目标段 DPL 则禁止访问

保护模式平坦模型¶
分段模型有很多权限,现代操作系统大多使用分页模型。
在 x86 CPU 中不能直接使用分页模型,需要在分段模型的前提下,根据需要决定是否要开启分页。因为硬件的规定,我们只能简化设计,来使分段成为一种“虚设”,即保护模式的平坦模型。
CPU 32 位的寄存器最多只能产生 4GB 大小的地址,而一个段长度也只能是 4GB,所以我们把:
- 所有段的基地址设为 0
- 段的长度设为 0xFFFFF
- 段长度的粒度设为 4KB
这样所有的段都指向同一个(0~4GB-1)字节大小的地址空间。
GDT_START:
knull_dsc: dq 0
;第一个段描述符CPU硬件规定必须为0
kcode_dsc: dq 0x00cf9e000000ffff
;段基地址=0,段长度=0xfffff
;G=1,D/B=1,L=0,AVL=0
;P=1,DPL=0,S=1
;T=1,C=1,R=1,A=0
;(段基址31:24) 0000 0000
;(G、D/B、L、AVL)1100
;(段长度19:16)1111
;(P、DPL、DPL、S、T、C、R、A)1001 1110
;(段基址23:0)0000 0000 0000 0000 0000 0000
;(段长度15:0)1111 1111 1111 1111
kdata_dsc: dq 0x00cf92000000ffff
;段基地址=0,段长度=0xfffff
;G=1,D/B=1,L=0,AVL=0
;P=1,DPL=0,S=1
;T=0,C=0,R=1,A=0
GDT_END:
GDT_PTR:
GDTLEN dw GDT_END-GDT_START-1
GDTBASE dd GDT_START
保护模式中断¶
- 实模式下 CPU 不需要做权限检查,所以可直接通过中断向量表中的值装载 CS:IP 寄存器
- 保护模式下的中断需要权限检查和特权级的切换,所以需要扩展中断向量表的信息,即每个中断用一个中断门描述符来表示(简称为中断门)

保护模式下中断实现与实模式大致相同,区别在于条目换成了中断门描述符。其产生中断的主要过程如下:
- 检查中断门描述符
- 检查中断门描述符中的段选择子指向的段描述符
- 权限检查:
- 若 CPL <= 中断门的 DPL(有权限能执行中断)
- 且 CPL >= 中断门中的段选择子所指向的段描述符的 DPL(能够进入相应的服务程序),就指向段描述符的 DPL(此时 CPL 等于 DPL)
- 若 CPL > 中断门中的段选择子指向的段描述符的 DPL,则进行栈切换(需要从 TSS 中加载具体权限的 SS、ESP,也要对 SS 中段选择子指向的段描述符进行检查)
- 检查后加载中断门描述符中:
- 目标代码段选择子到 CS 寄存器中
- 目标代码段偏移加载到 EIP 寄存器中

切换到保护模式¶
x86 CPU 在第一次加电和每次 reset 后,都会自动进入实模式,若要进入保护模式,则需要通过代码实现切换,其步骤如下:
- 准备全局段描述符表
GDT_START:
knull_dsc: dq 0
kcode_dsc: dq 0x00cf9e000000ffff
kdata_dsc: dq 0x00cf92000000ffff
GDT_END:
GDT_PTR:
GDTLEN dw GDT_END-GDT_START-1
GDTBASE dd GDT_START
- 加载设置 GDTR 寄存器,使之指向全局段描述符表
- 设置 CR0 寄存器,开启保护模式
;开启 PE
mov eax, cr0
bts eax, 0
;CR0.PE =1
;bts指令的意思是 bit test and set 位测试并设置
;判断 eax 与0,若 eax == 0,bts 会将 CF = 1,并将eax置位(即设置为1)
;然后把 CR0 的最低位设为1之后,即开启了保护模式
mov cr0, eax
- 进行长跳转,加载 CS 段寄存器,即段选择子
长跳转的目的在于无法直接或间接 mov 一个数据到 CS 寄存器中,因为刚刚开启保护模式时,CS 的影子寄存器还是实模式下的值,所以需要告诉 CPU 加载新的段信息。
这里填入CS的值是0x8,根据段选择子的格式定义:
0x8: INDEX TI CPL 0000 0000 1 00 0
- INDEX代表GDT中的索引
- TI代表使用GDTR中的GDT
- CPL代表处于特权级,即GDT表中的第1个(index从0开始)描述符
_32bits_mode作为段内偏移,然后得 GDT第一个1段描述符 + _32bits_mode的内存地址 = 段基址 + 段内偏移地址
- 进入保护模式
- CPU 发现了 CRO 寄存器第 0 位的值是 1
- 按 GDTR 的指示找到全局描述符表
- 然后根据索引值 8,进行一系列合法检查
- 把新的段描述符信息加载到 CS 影子寄存器
长模式¶
长模式又名 AMD64(该标准是 AMD 公司最早定义的),它使 CPU 在现有的基础上有了 64 位的处理能力(完成 64 位数据运算和寻址 64 位的地址空间)。
长模式寄存器¶
长模式相比于保护模式,增加了一些通用寄存器,并扩展通用寄存器的位宽,所有的通用寄存器都是 64 位,还可以单独使用低 32 位。

长模式段描述符¶
与保护模式绝大多数特性相同,如特权级和权限检查。
长模式下的段描述符格式如下:

在长模式下,弱化段模式管理,CPU 不再对段基址和段长度进行检查(地址检查交给 MMU),只对 DPL 进行相关的检查。同样也有全局段描述符,且由 CPU 的 GDTR 寄存器指向。
ex64_GDT:
null_dsc: dq 0
;第一个段描述符CPU硬件规定必须为0
c64_dsc:dq 0x0020980000000000 ;64位代码段
;无效位填0
;D/B=0,L=1,AVL=0
;P=1,DPL=0,S=1
;T=1,C=0,R=0,A=0
d64_dsc:dq 0x0000920000000000 ;64位数据段
;无效位填0
;P=1,DPL=0,S=1
;T=0,C/E=0,R/W=1,A=0
eGdtLen equ $ - null_dsc ;GDT长度
eGdtPtr:dw eGdtLen - 1 ;GDT界限
dq ex64_GDT
段描述符的 DPL=0,这说明需要最高权限即 CPL=0 才能访问。 若是数据段的话,G、D/B、L 位都是无效的。
长模式中断¶
长模式的中断与保护模式的中断过程大致相同,主要区别在于中断门描述符格式为适应64位进行了修改和扩展:
- 在原有基础上增加 8 字节,用于存放目标段偏移的高 32 位值(支持 64 位寻址)
- 目标代码段选择子对应的代码段描述符必须是 64 位的代码段
- IST 是 64 位 TSS 中的 IST 指针

切换到长模式¶
切换到长模式的步骤如下:
- 准备长模式全局段描述符表
ex64_GDT:
null_dsc: dq 0
;第一个段描述符CPU硬件规定必须为0
c64_dsc:dq 0x0020980000000000 ;64位代码段
d64_dsc:dq 0x0000920000000000 ;64位数据段
eGdtLen equ $ - null_dsc ;GDT长度
eGdtPtr:dw eGdtLen - 1 ;GDT界限
dq ex64_GDT
- 准备 MMU 页表开启分页
mov eax, cr4
bts eax, 5 ;CR4.PAE = 1
mov cr4, eax ;开启 PAE
mov eax, PAGE_TLB_BADR ;页表物理地址
mov cr3, eax
长模式下内存地址空间的保护交给了 MMU,MMU 依赖页表对地址进行转换,页表有特定的格式存放在内存中,其地址由 CPU 的 CR3 寄存器指向
- 加载 GDTR 寄存器,使之指向全局段描述表
- 开启长模式同时开启保护模式和分页模式
;开启 64位长模式
mov ecx, IA32_EFER
rdmsr
bts eax, 8 ;IA32_EFER.LME =1
wrmsr
;开启 保护模式和分页模式
mov eax, cr0
bts eax, 0 ;CR0.PE =1
bts eax, 31
mov cr0, eax
在实现长模式时定义了 MSR 寄存器,需要用专用的指令 rdmsr、wrmsr 进行读写,IA32_EFER 寄存器的地址为 0xC0000080,它的第 8 位决定了是否开启长模式。
- 进行跳转,加载 CS 段寄存器,刷新其影子寄存器
参考¶
https://time.geekbang.org/column/intro/100078401?tab=catalog