x86 CPU工作的三种模式

引言:X86CPU工作的三种模式,十分硬核!啃了整整两天

x86 CPU工作的三种模式

x86 CPU有三种工作模式:实模式、保护模式、长模式

内容硬核,涉及到很多OS的知识,如有错误,请通知我!

实模式

实模式概念

实模式:实地址模式

他有两个特点:

  1. 运行真实的指令(即任何指令都可以直接执行)
  2. 执行的内存地址是实际的物理内存地址

实模式下的寄存器

寄存器此处简单罗列一下,这里涉及到很多汇编的知识,先混个眼熟,之后用到了,我会细说

寄存器按类型分为三种:通用寄存器、控制寄存器、段寄存器

  • 通用寄存器:(分三类)

    • 数据寄存器:AX、BX、CX、DX
    • 指针寄存器:SP、BP
    • 变址寄存器:SI、DI
  • 控制寄存器:IP(IP与PC是一个意思)、FLAGS

  • 段寄存器:CS、DS、ES、SS

注意:实模式下的段寄存器存放内存段的基地址

取指令

取指令涉及到的关键寄存器:CS 与 IP

  • CS:代码段寄存器
  • IP:IP就是PC,指向下一个指令的地址

(注意:涉及到代码段寄存器与普通寄存器的操作,都需要将段寄存器左移4位,然后与IP寄存器的值相加

取指令

为什么要设计为段寄存器左移4位,然后与IP寄存器相加这种形式呢?

​ 因为当时硬件设计者,设计了20条地址线,但是一个寄存器是16位的,一个寄存器是不能遍历2^16的地址,所以需要左移4位2^16<<4就能访问到所有的地址了

(离谱,为什么硬件设计者要这么搞?难为软件工程师)

访问内存数据

实模式下的中断

中断也是一块大的知识点,可以看此篇补充

不同的书籍对于中断的具体描述不太一样,这里给出一种理解方式(CSAPP是另一种方式)

中断的分类

中断分为软件中断与硬件中断:

  • 软件中断:比如系统调用,指系统内部出现的调用
  • 硬件中断:(根据产生的原因,分为内外中断)
    • 内中断:指CPU内部出现的中断信号,比如除零、缺页等
    • 外中断:指外部IO设备发到CPU的中断信号,比如键盘输入
      • 可屏蔽异常(CPU可以不予理会,继续执行)
      • 不可屏蔽异常

中断的处理过程

首先我们先搞清几个问题:

1、如何让CPU执行我们需要的程序?

​ CPU会执行CS+IP(或称为PC)对应的指令,想让CPU执行我们需要的指令,只需要设置CS+IP指向我们编写的程序即可

2、如何处理中断?

其实很简单:注意两件事情即可

  • 处理中断:让CPU执行中断处理程序即可(即让CS+IP为中断处理程序)

(中断处理程序是我们编写的一段处理各种中断的代码,对于不同的中断,我们需要有不同的解决办法)

  • 处理中断之前:如果处理完成中断,我们还要返回当前处理的指令位置,所以我们需要保存一下当前的CS与IP

3、如何找到中断处理程序?

设计了以下结构:

  • 中断号:表示不同的中断(其实代表了偏移地址
  • 中断表(IDT,Interrupt Descriptor Table):存放不同中断处理程序的入口地址(一个表目由两个字节组成,段地址与偏移(如下图))
  • IDTR寄存器(IDTR):记录中断表的起始地址与长度

4、索引的具体过程(重点,细品)

  1. 收到中断信号,取中断类型码:其来源可能是CPU内部、也可能是IO设备发来的
    • 内部的中断:CPU内部可以直接得到中断号;
    • 外部的中断:那么数据总线会传入其中断号;
  2. 保存当前的CPU状态(比如FLAGS寄存器)
  3. 保存当前的CS、IP(将CS、IP入栈即可)
  4. 通过IDTR寄存器+中断号去查中断表
  5. 设置CS为代码段基地址IP为代码段内偏移
  6. 执行对应CS+IP的程序
  7. 执行完成后,出栈,再返回执行原来的指令

中断索引过程

保护模式

实模式存在的问题

设想如此的一段代码:

1
2
3
4
5
6
7
8
9
10
int main(){
int* addr = (int*)0;
cli(); //关中断
while(1)
{
*addr = 0;
addr++;
}
return 0;
}

它会做些什么?关闭中断,进入死循环,不停的将内存地址置零

简单来说,这段程序清空了内存,实模式下竟然可以运行这样的代码,显然是不合理的,因此提出了保护模式


实模式下存在的问题有:

  • 使用16位的寄存器,寻址范围小
  • 任何指令都可以执行
  • 可以访问任何内存地址

因此保护模式要实现:

  1. 扩展寻址范围(扩展寄存器位数)
  2. 区分的执行指令(特权级)
  3. 限制可以访问的内存范围(段描述符)

保护模式的寄存器

为了解决寻址范围小的问题,将16位寄存器改为了32位寄存器

(名字直接加了个E表示32位,注意:没有扩展段寄存器

  • 通用寄存器:(分三类)

    • 数据寄存器:EAX、EBX、ECX、EDX
    • 指针寄存器:ESP、EBP
    • 变址寄存器:ESI、EDI
  • 控制寄存器:

    • 扩展为32位:EIP、EFLAGS
    • 新加入:CR0、CR1、CR2、CR3(也是32位,控制CPU的功能控制特性,比如开启保护模式就用到CR0寄存器等等)
  • 段寄存器(注意:没有扩展段寄存器,因此仍然是16位的):

    • 原有:CS、DS、ES、SS
    • 新加入:FS、GS

注意:保护模式下的段寄存器虽然位数没有变化,但是存放的内容由一个简单的基地址,转变为内存段的描述符索引

特权级

为了实现区分的执行指令,CPU实现了特权级

总共分为4中权级别,从R0-R3,权利依次降低,他们的权利范围如图:

CPU特权级示意图

可以用两位表示这四个权级

1
2
3
4
00 R0
01 R1
10 R2
11 R3

因此,越小表示权利越大

(注意:Linux系统只实现了R0与R3,即内核态与用户态

段描述符

为了限制内存的访问范围,设置了段描述符

注意:目前我们仍然是分段模型

什么是段描述符?

段描述符:即描述一个段的有关信息

段描述符存放在什么地方?

存放在内存中。

(由于CPU扩展,所以此时的段基地址与段内偏移第一世故32位,但是段寄存器没有扩展,因此,只能将信息存放在内存中)

段描述符的结构如下:

  • 重点要注意DPL(Descriptor Privilege Level),实现了特权级,之后会细说

  • 我标记了一下位数,可以看出来,段描述符的布局很乱(历史原因)

段描述符的结构

全局段描述符表

很多段描述符就构成了段描述符表

​ 访问时,根据GDTR寄存器(类似于IDTR寄存器)结合代码段寄存器,找到段描述符,然后根据段描述符再去找对应的段

如图:

全局段描述符

注意:这个过程中代码段不是简简单单的存放偏移(或者说不是简单存放段描述符的索引,而是存放段选择子

段选择子

代码段不是简单的存储一个段描述符的偏移,其也是一个复杂的结构,如图

段选择子

  • 影子寄存器:arm架构有的一个硬件部分,是一个段描述符的高速缓存,可以减少性能损耗(其对程序员不可见,因此无需特别了解)

  • 段描述符索引:占了13位

    为什么段描述符只占了13位?这样能存下偏移吗?

    ​ 本来应该就是16位,但是由于八字节对齐,所以最低三位均为000,所以最低三位可以用来做其他事情,这里就用2位表示RPL,1位表示TI

  • RPL(Request Privilege Level):注意这个结构,之后会详细介绍

RPL、DPL、CPL进行权限校验

经过上面,我们知道:

段描述符内有DPL,段寄存器的段选择子有RPL

在CS与SS中的RPL就组成了CPL(Current Privilege Level),而一般情况下,这两个值是相同的(RPL = CPL)

【重点】因此 CPL 就表示发起访问者要以什么权限去访问目标段

  • 当 CPL > DPL :CPU 禁止访问

  • 当 CPL <= DPL:可以访问。

总结:

使用当前的存放在CS、SS中的RPL作为CPL,与将要操作的段的DPL进行比较;

小于等于表明当前权力大,可以访问;

大于说明权利不足,禁止访问;

平坦模式

为什么要使用平坦模型?

  • 分段模型有缺陷:

    注意,现在我们还是处于分段模式,这个模式有很多缺陷,所以现在基本都在使用分页模型

但是硬件规定:x86 CPU 并不能直接使用分页模型,而是要在分段模型的前提下,根据需要决定是否要开启分页

因此设计了平坦模型

什么是平坦模型?

一句话:平坦模型是将全部的4GB内存整体作为一个大段来处理,而不是分成小的区块

这种模型下:

  • 所有段都是4GB
  • 段基址都是0x0000 0000
  • 段长度都是0xffff ffff
  • 依然存在着段接线和数据访问的检查(但不会产生违例的情况,因为所有段的基址和长度都一样,可以访问任意地方)

如何实现平坦模型?

  • 设置基地址为0,设置段长度为0xfffff(因为段长度只有20bit位,所以只有5个f)

  • 设置粒度为4kb,即设置G=1

这样,就实现了平坦模型


此处例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GDT_START:
knull_dsc: dq 0
;第一个段描述符CPU硬件规定必须为0
kcode_dsc: dq 0x00cf9e000000ffff
;0x00cf9e000000ffff相当于:
;1、段基地址=0,段长度=0xfffff
;2、G=1,D/B=1,L=0,AVL=0
;3、P=1,DPL=0,S=1
;4、T=1,C=1,R=1,A=0
kdata_dsc: dq 0x00cf92000000ffff
;0x00cf92000000ffff相当于:
;1、段基地址=0,段长度=0xfffff
;2、G=1,D/B=1,L=0,AVL=0
;3、P=1,DPL=0,S=1
;4、T=0,C=0,R=1,A=0
GDT_END:

GDT_PTR:
GDTLEN dw GDT_END-GDT_START-1
GDTBASE dd GDT_START

可见,这段代码中kcode_dsckdata_dsc两个段的起始地址与段长度都相同

且均设G=1代表段长度的粒度为4KB,均设DPL为R0级别才可以访问

(可以结合前面段描述符的图来看)

保护模式的中断

与实模式的中断处理的区别?中断门

保护模式提出了特权级及其切换,所以需要检查特权,因此有了新的结构中断门描述符中断门

中断门的结构:类似于段描述符

中断门的结构

中断向量表条目换成中断门

中断向量表

处理中断的步骤【重点】

  1. 收到中断信号,取中断类型码:其来源可能是CPU内部、也可能是IO设备发来的
    • 内部的中断:CPU内部可以直接得到中断号;
    • 外部的中断:那么数据总线会传入其中断号;
  2. 保存当前的CPU状态(比如FLAGS寄存器)
  3. 保存当前的CS、EIP(将CS、IP入栈即可)
  4. 通过IDTR寄存器+中断号去查中断表
  5. 【新】检查中断号是否超过范围(即大于最后一个中断门,x86允许256个中断源)
  6. 【新】检查描述符类型(是中断门还是陷阱门、是否是系统描述符、是否在内存中)
  7. 【新】检查中断门描述符中的段选择子指向的段描述(即找到中断处理程序)
  8. 【新,重点】权限检查:如果CPL<=中断门DPL && CPL >=中断门段选择子DPL就设置CS为代码段基地址EIP为代码段内偏移
  9. 执行对应CS+EIP的程序(这里还涉及到通过GDTR寄存器,再去查GDT全局段描述符表,然后再找对应的段)
  10. 执行完成后,出栈,再返回执行原来的指令

以上是我的理解方式,下面放一个不错的理解,建议结合食用

保护模式中断发生步骤:

  1. 中断发生后,根据中断号码,对比cpu IDTR寄存器指示的中断门描述符表,找出中断对应的中断门描述符表
  2. 中断门描述符表中,找出中断门对应的DPL判断权限, 目标代码偏移地址,目标代码段选择子。
  3. 根据目标代码段选择子中的段描述符索引,查找GDTR寄存器(指向全局段描述符表)指示的全局段描述符表,找出择子指向目标代码的段描述符,目标代码段RPL(进行权限判断)
  4. 根据段描述符,找出对应的中断程序代码段地址。
  5. 根据以上步骤将目标代码段地址及偏移地址 装载到CS:EIP 寄存器 其中权限对比
    1. cpl >=中断段选择符DPL,保证中断服务处理程序权限大于触发中断的应用程序,禁止中断调用用户程序,防止恶意用户程序,而又不妨碍用户态和内核态产生中断;
    2. cpl<= 中断门描述符DPL ,确保应用程序有足够的权限引起中断,防止用户态程序调用特殊中断。

权限检查为什么要CPL<=中断门DPL && CPL >=中断门段选择子DPL

  • CPL<=中断门DPL:只有权比门大,才能让门打开(确保应用程序有足够的权限引起中断,防止用户态程序调用特殊中断)
  • CPL >=中断门段选择子DPL:表示引起中断的程序的权利不能比中断处理程序的权利大(防止恶意用户程序,而又不妨碍用户态和内核态产生中断)

实模式切换到保护模式

总共需要4步:

  1. 准备全局描述符表GDT

    1
    2
    3
    4
    5
    6
    7
    8
    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
  2. 设置GDTR寄存器,指向GDT

    1
    lgdt [GDT_PTR]
  3. 设置CR0寄存器(开启保护模式)

    1
    2
    3
    4
    ;开启 PE
    mov eax, cr0
    bts eax, 0 ;CR0.PE =1
    mov cr0, eax ;CR0的最低位为

    bts指令的意思是bit test and set 位测试并设置,在此处的作用是:判断eax与0,若eax == 0:bts会将CF = 1,并将eax置位(置位的意思就是设置为1)

  4. 进行长跳转

    1
    jmp dword 0x8 :_32bits_mode ;_32bits_mode为32位代码标号即段偏移

此刻,我们的OS就进入了保护模式!


为什么要进行长跳转?

​ 因为我们无法直接或间接 mov 一个数据到 CS 寄存器中,因为刚刚开启保护模式时,CS 的影子寄存器(影子寄存器是硬件,程序员没办法设置)还是实模式下的值,所以需要告诉 CPU 加载新的段信息

为什么要设置为0x8?

1
2
段描述符索引    TI(第三位) CPL(最后两位)
0000 0000 0000 1000

段选择子

即表示以R0的权限访问0x1的值(即GDT第一个描述符)

长模式

长模式,又名 AMD64 模式,最早由 AMD 公司制定

为什么要进入长模式

在保护模式中,我们的位数32位,这显然不能满足我们的使用(尤其是今日,你在用多大的内存呢)

所以长模式,就进行了进一步的扩展,长模式在保护模式的基础上,把寄存器扩展到 64 位同时增加了一些寄存器

使 CPU 具有了能处理 64 位数据和寻址 64 位的内存地址空间的能力

长模式的段描述符

长模式的段描述符去掉了段基址、段长度、Type内的一些字段

长模式下的段描述符

注意:

  • G无效了
  • L可以设置为是否为64位模式(数据段无效)
  • 段长度和段基址都是无效的填充为 0,CPU 不做检查

长模式的中断门描述符

扩展到64位,使用高32位存储代码段偏移的高位,其他内容与保护模式相同

长模式中断描述符

切换到长模式

切换到长模式可以从实模式直接切换,也可以从保护模式切换

  1. 准备全局描述符

    1
    2
    3
    4
    5
    6
    7
    8
    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
  2. 准备MMU(内存管理单元)页表(长模式必须开启分页,长模式下内存地址空间的保护交给了 MMU)

    1
    2
    3
    4
    5
    mov eax, cr4
    bts eax, 5 ;CR4.PAE = 1
    mov cr4, eax ;开启 PAE
    mov eax, PAGE_TLB_BADR ;页表物理地址
    mov cr3, eax
  3. 加载 GDTR 寄存器,使之指向全局段描述表

    1
    lgdt [eGdtPtr]
  4. 开启长模式,要同时开启保护模式和分页模式

    (此处还涉及到MSR寄存器,rdmsr、wrmsr是操作msr寄存器专门的指令,IA32_EFER寄存器的第八位决定是否开启长模式)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ;开启 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
  5. 跳转,加载CS段寄存器,刷新影子寄存器

    1
    jmp 08:entry64 ;entry64为程序标号即64位偏移地址

长模式总结

结构方面:扩展到64位,增加了寄存器

长模式的改变:弱化段模式管理只保留了权限级别的检查,忽略了段基址和段长度,而地址的检查则交给了 MMU

参考资料

  1. 《CSAPP》
  2. 《汇编语言》王爽著
  3. 极客时间《操作系统实战45讲》
  4. 《x86汇编语言 从实模式到保护模式》