以常用的x86 CPU为例。按照CPU的迭代,CPU的工作模式有实模式、保护模式、长模式。

其实就是对应于16位、32位、64位。

实模式

实模式的实是真实,体现在两个方面:

  1. 运行真实的指令,对指令的动作不加以区分,直接执行指令真实的功能。
  2. 操作的地址的真实的物理地址。

实模式寄存器

实模式下,寄存器是16位的。

寄存器描述
AX、BX、CX、DX、SI、DI、BP通用寄存器,可以存放地址和数据,参数计算
IP程序指针寄存器,指向下一条指令的地址
SP栈指针寄存器,始终指向栈顶
CS、DS、ES、SS段寄存器,存放一个内存段的基地址
FLAGSCPU标志寄存器,存放CPU执行运算指令产生的状态位

实模式下可寻址1M,也就是 ,需要20位的地址总线。但是寄存器只有16位,为了寻址20位的地址,默认将低4位置为0,段寄存器存放高16位的地址作为基地址。一个段的大小最多只有即64KB。内存地址的计算方法如下:

如图:

Note

代码段是由CS和IP确定的 栈段是由SS和SP确定的

实模式中断

中断就是暂停当前程序,去运行指定的中断处理程序,结束后再返回继续执行的过程。

实模式下执行中断就是先保存当前的CS和IP,然后加载新的CS和IP执行程序。

中断有软件中断和硬件中断两种:

  • 硬件中断中,中断控制器先向CPU发送一个中断信号,CPU应答后,中断控制器再将中断号发送过去。
  • 软件中断中,CPU执行INT指令,指令后面跟着一个常数,即软中断号。

CPU就通过硬件中断或者软件中断获得中断号,然后根据中断向量表获取到对应中断处理程序的CS和IP。将中断处理程序的CS和IP加载,就可以跳转到中断处理程序的位置开始执行了。

而中断向量表就是一个内存中的中断向量的数组,每一个中断向量都包含其对应的CS和IP。中断向量表的信息是通过IDTR寄存器保存的。

保护模式

保护模式从实模式的16位变为了32位,也同样解决了实模式的两个问题:

  1. 指令不加以区分执行。
  2. 内存访问不受限制。

保护模式引入了特权级和段描述符来解决了这两个问题。 首先查看保护模式寄存器的变化。

保护模式寄存器

保护模式扩展了通用寄存器的位数,增加了一些控制寄存器和段寄存器。这些寄存器都是32位的,但是与之对应的16位寄存器仍然可以使用。

寄存器描述
EAX、EBX、ECX、EDX、ESI、EDI、EBP32位通用寄存器,可以存放地址和数据,参数计算
EIP32位程序指针寄存器,指向下一条指令的地址
ESP32位栈指针寄存器,始终指向栈顶
CS、DS、ES、SS、FS、GS16位段寄存器,存放一个内存段的基地址
EFLAGSCPU标志寄存器,存放CPU执行运算指令产生的状态位
CR0、CR1、CR2、CR332位CPU控制寄存器,控制CPU的功能特性,例如开启保护模式

保护模式特权级

保护模式将指令引入的特权级,对指令和资源进行了区分。

特权级分为4级,R0~R3,特权等级依次递减。R0能够执行所有的指令集,而后的特权级只能执行前一等级的子集。

R0的权限最大,可以访问低权限的资源,反之则不能。

保护模式段描述符

特权级对指令进行了区分,但是对内存的保护是通过特权级和段描述符一同实现的,对内存保护转为对段的保护。

保护模式下段基地址和偏移地址都扩展为了32位,但是段寄存器仍然是16位的,肯定放不下。 所以引入了段描述符和段描述符表,与实模式的中断的实现方式非常类似。

将内存中段的相关信息存放在段描述符中,而段描述符表就是段描述符的数组。因此我们段寄存器存放着段描述符的索引而不是段基地址。段描述符表的相关信息通过GDTR寄存器存储 如图所示,通过GDTR和段寄存器可以找到对应的段描述符,从而获取到段描述符中的段的相关信息。

下面介绍段描述符的结构。段描述符一个64位8字节的数据,包含着段基地址、段长度、段权限、段类型、段是否可读写,可执行等。划分比较乱,这是由于历史原因造成的。如图:

保护模式段选择子

上前提到段寄存器中存放着段描述符的索引。但不仅仅是段描述符的索引,还有影子寄存器、段描述符索引、描述符表索引、权限级别。

  • 影子寄存器。是由硬件操作的,对系统程序员不可见的,为段描述符设计的高速缓存。为了减少每次去内存中查找段描述符的损耗,使用影子寄存器直接将对应的段描述符缓存起来。
  • 段描述符索引的低三位用来存放TI,RPL,这是因为段描述符索引指的是段描述符在段描述符表中的偏移位置,而段描述符的大小是8字节,因此段描述符索引8字节对齐,低三位全部为0,所以我们可以利用这三位来存放TI和RPL。
  • 通常CS和SS中RPL组成了CPL(当前权限级别),只有CPL小于目标端访问级别DPL的时候,级CPL权限高的时候才能访问目标段。

保护模式平坦模型

分段模型有很多缺陷。 现代操作系统使用的都是分页模型。但是x86 CPU并不能直接使用分页模型,而是在分段模型的前提下,根据需要决定是否开启分页

32位的CPU最多寻址4GB,一个段长度最大也是4GB,我们把所有的段的段描述符的段基地址设为0,段长度设置为0xFFFFF,段长度的粒度为4KB,这样所有的段都指向同一个(0~4GB-1)的段空间。

保护模式中断

保护模式下的中断要权限检查和特权级的切换,所以比起实模式,要对中断向量表进行扩展。每个中断用中断门描述符来表示,也可以简称为中断门。

同样,在内存中有一个中断向量表为中断门描述符的数组,中断描述符表,使用IDTR寄存器指向 中断产生后会进行如下处理:

  1. 检查中断号是否大于最后一个中断门描述符。x86最大支持256个中断源。
  2. 检查描述符类型,是中断门还是陷阱门。
  3. 是否是系统描述符。
  4. 是否在内存中。
  5. 检查中断门描述符中的段选择子指向的段描述符。
  6. 如果CPL等于DPL,则统计权限不进行栈切换,否则进行栈的切换。如果进行栈切换,还要从TSS加载对应权限的SS、ESP。
  7. 检查完毕之后将中断门描述符的目标代码段选择子和目标代码偏移加载到CS和EIP中。

保护模式的切换

x86 CPU加电之后会自动进入实模式,要进入保护模式,需要程序员自己编写代码进行切换。

  1. 准备全局段描述符
GDT_START:
knull_dsc: dq 0 ;第一个段描述符硬件规定必须位0
kcode_dsc: dq 0x00cf9e000000ffff
kdata_dsc: dq 0x00cf92000000ffff
GDT_END:
GDT_PTR:
GDTLEN dw GDT_END - GDT_START -1
GDTBASE dd GDT_START
  1. 设置GDTR寄存器,让其指向全局段描述符表。GDTR是一个48位的寄存器
lgdt [GDT_PTR]
  1. 设置CR0寄存器,开启保护模式。CR0寄存器是保护模式引入的一个控制寄存器。
mov eax, cr0
bts eax, 0   ;cr0.PE=1
mov cr0, eax
  1. 进行长跳转,加载CS寄存器,即段选择子。
jmp dword 0x8 :_32bits_mode ;_32bits_mode为32位代码标号即段偏移

为什么要进行长跳转,这是因为我们无法直接或间接 mov 一个数据到 CS 寄存器中,因为刚刚开启保护模式时,CS 的影子寄存器还是实模式下的值,所以需要告诉 CPU 加载新的段信息。

接下来,CPU发现CR0的第0位为1,就是根据GDTR找到全局描述符表,然后根据索引值8,把新的段描述符加载到CS影子寄存器。根据前面的代码,很明显,这里索引值8对应的就是代码段。

长模式

长模式最早由AMD公司定义,能够处理64位的数据运算,也能寻址64位的地址空间。

长模式寄存器

长模式相较于保护模式,增加了一些通用寄存器,并将通用寄存器的位数扩展到64位。

寄存器描述
RAX、RBX、RCX、RDX、RSI、RDI、RBP64位通用寄存器,可以存放地址和数据,参数计算
RIP64位程序指针寄存器,指向下一条指令的地址
RSP64位栈指针寄存器,始终指向栈顶
CS、DS、ES、SS、FS、GS16位段寄存器,存放一个内存段的基地址
RFLAGSCPU标志寄存器,存放CPU执行运算指令产生的状态位
CR0、CR1、CR2、CR332位CPU控制寄存器,控制CPU的功能特性,例如开启保护模式

长模式段描述符

长模式依然具备保护模式下的绝大部分功能,如特权级和权限检查。这里只介绍一下长模式和保护模式的差异。 如图: 长模式下,默认所有段的段基地址都为0,段长度为64位。因此不再对段基地址和段长度进行检查。只对DPL进行相关检查,流程与保护模式一致。

当描述符中的 L=1,D/B=0 时,就是 64 位代码段,DPL 还是 0~3 的特权级。

长模式下仍然在内存中有段描述符组成的数组,段描述符表,也仍然使用GDTR寄存器指向

长模式中断

在保护模式下的中断门描述符的段内偏移就有32位,在64位的长模式下段内偏移也有64位,所以对中断门描述符也有所扩展。 中断门描述符由64位扩展为了128位,即16字节。低8字节与保护模式保持兼容,缺少的32位偏移地址由高8字节的低32位保存,其余位保留,如图: 其余部分如中断描述符表和中断的响应都与保护模式一致。

切换到长模式

可以由实模式直接切换到保护模式,也可以由保护模式切换到长模式。 步骤如下:

  1. 准备长模式的全局描述符表
 
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
  1. 准备长模式下的 MMU 页表,这个是为了开启分页模式,切换到长模式必须要开启分页。这是因为长模式下不对段基地址和段长度检查,对内存的保护交给了MMU,MMU依赖页表进行转换。而页表由CR3指向。
mov eax, cr4
bts eax, 5   ;CR4.PAE = 1
mov cr4, eax ;开启 PAE
mov eax, PAGE_TLB_BADR ;页表物理地址
mov cr3, eax
  1. 设置GDTR寄存器,使之指向全局段描述符表。
lgdt [eGdtPtr]
  1. 开启长模式,同时开启保护模式和分页模式。在实现长模式时定义了 MSR 寄存器,需要用专用的指令 rdmsr、wrmsr 进行读写,IA32_EFER 寄存器的地址为 0xC0000080,它的第 8 位决定了是否开启长模式。
;开启 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 
  1. 进行跳转,加载CS寄存器,刷新其影子寄存器。
jmp 08:entry64 ;entry64为程序标号即64位偏移地址

长模式弱化段管理模式,只保留了权限检查,将地址的检查交由了MMU