实模式

实模式又称实地址模式,实,即真实,这个真实分为两个方面,一个方面是运行真实的指令,对指令的动作不作区分,直接执行指令的真实功能,另一方面是发往内存的地址是真实的,对任何地址不加限制地发往内存。

实模式寄存器

指令的操作数,可以是寄存器、内存地址、常数,其实通常情况下是寄存器

实模式下访问内存

虽然有了寄存器,但是数据和指令都是存放在内存中的。通常情况下,需要把数据装载进寄存器中才能操作,还要有获取指令的动作,这些都要访问内存才行,而我们知道访问内存靠的是地址值。计算过程如下图.

所有的内存地址都是由段寄存器左移 4 位,再加上一个通用寄存器中的值或者常数形成地址,然后由这个地址去访问内存。这就是大名鼎鼎的分段内存管理模型。

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

实模式中断

中断即中止执行当前程序,转而跳转到另一个特定的地址上,去运行特定的代码。在实模式下它的实现过程是先保存 CS 和 IP 寄存器,然后装载新的 CS 和 IP 寄存器。

中断产生的两种方式:

第一种情况是,中断控制器给 CPU 发送了一个电子信号,CPU 会对这个信号作出应答。随后中断控制器会将中断号发送给 CPU,这是硬件中断

第二种情况就是 CPU 执行了 INT 指令,这个指令后面会跟随一个常数,这个常数即是软中断号。这种情况是软件中断

无论是硬件中断还是软件中断,都是 CPU 响应外部事件的一种方式。

为了实现中断,就需要在内存中放一个中断向量表,这个表的地址和长度由 CPU 的特定寄存器 IDTR 指向。实模式下,表中的一个条目由代码段地址和段内偏移组成,如下图所示。

有了中断号以后,CPU 就能根据 IDTR 寄存器中的信息,计算出中断向量中的条目,进而装载 CS(装入代码段基地址)、IP(装入代码段内偏移)寄存器,最终响应中断。

保护模式

内存一大,首先要解决的问题是寻址问题,因为 16 位的寄存器最多只能表示 216 个地址,所以 CPU 的寄存器和运算单元都要扩展成 32 位的。

虽然扩展 CPU 内部器件的位数解决了计算和寻址问题,但仍然没有解决前面那个实模式场景下的问题,导致前面场景出问题的原因有两点。第一,CPU 对任何指令不加区分地执行;第二,CPU 对访问内存的地址不加限制。

基于这些原因,CPU 实现了保护模式。

保护模式寄存器

保护模式相比于实模式,增加了一些控制寄存器和段寄存器,扩展通用寄存器的位宽,所有的通用寄存器都是 32 位的,还可以单独使用低 16 位,这个低 16 位又可以拆分成两个 8 位寄存器,如下表。

保护模式特权级

为了区分哪些指令(如 in、out、cli)和哪些资源(如寄存器、I/O 端口、内存地址)可以被访问,CPU 实现了特权级。

特权级分为 4 级,R0~R3,每个特权级执行指令的数量不同,R0 可以执行所有指令,R1、R2、R3 依次递减,它们只能执行上一级指令数量的子集。R0 拥有最大权力,可以访问低特权级的资源,反之则不行。

保护模式段描述符

目前为止,内存还是分段模型,要对内存进行保护,就可以转换成对段的保护。

一个段描述符有 64 位 8 字节数据,里面包含了段基地址、段长度、段权限、段类型(可以是系统段、代码段、数据段)、段是否可读写,可执行等。虽然数据分布有点乱,这是由于历史原因造成的。

由于 CPU 的扩展导致了 32 位的段基地址和段内偏移,还有一些其它信息,所以 16 位的段寄存器肯定放不下。放不下就要找内存借空间,然后把描述一个段的信息封装成特定格式的段描述符,放在内存中(注: 因为段寄存器只有16位,不能存放32位的段基地址:偏移地址+其他信息;于是将段信息封装成特定格式(段描述符,64位)后经过数据总线存入内存,形成段描述符表;该表的基地址和长度由 GDTR 寄存器指示,可通过GDTR 寄存器+CS:IP等段寄存器(存的段描述符的索引)得到段描述符所在的内存地址。保护模式最早是在80286出现的,80286还是16位cpu,但是地址线是24位,所以在保护模式下,段基址也设计成24位的。后面的80386是32位cpu,地址线也是32位的,段基址也是32位的,为了兼容,只能在286的基础上进行拓展。)

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

段寄存器中不再存放段基地址,而是具体段描述符的索引,访问一个内存地址时,段寄存器中的索引首先会结合 GDTR 寄存器找到内存中的段描述符,再根据其中的段信息判断能不能访问成功。

保护模式段选择子

CS、DS、ES、SS、FS、GS 这些段寄存器,里面存放的就是一个内存段的描述符索引,其实它们是由影子寄存器、段描述符索引、描述符表索引、权限级别组成的。如下图所示。

上图中影子寄存器是靠硬件来操作的,对系统程序员不可见,是硬件为了减少性能损耗而设计的一个段描述符的高速缓存,不然每次内存访问都要去内存中查表,那性能损失是巨大的,影子寄存器也正好是 64 位,里面存放了 8 字节段描述符数据。(注:到影子寄存器是arm特有,x86和powerpc架构没有影子寄存器)

低三位之所以能放 TI 和 RPL,是因为段描述符 8 字节对齐,每个索引低 3 位都为 0,我们不用关注 LDT,只需要使用 GDT 全局描述符表,所以 TI 永远设为 0。

通常情况下,CS 和 SS 中 RPL 就组成了 CPL(当前权限级别),所以常常是 RPL=CPL,进而 CPL 就表示发起访问者要以什么权限去访问目标段,当 CPL 大于目标段 DPL 时,则 CPU 禁止访问,只有 CPL 小于等于目标段 DPL 时才能访问。

保护模式平坦模型

现代操作系统都会使用分页模型。但是 x86 CPU 并不能直接使用分页模型,而是要在分段模型的前提下,根据需要决定是否要开启分页。因为这是硬件的规定,程序员是无法改变的。但是可以简化设计,来使分段成为一种“虚设”,这就是保护模式的平坦模型。

CPU32 位的寄存器最多只能产生 4GB 大小的地址,而一个段长度也只能是 4GB,所以我们把所有段的基地址设为 0,段的长度设为 0xFFFFF,段长度的粒度设为 4KB,这样所有的段都指向同一个((段的长度 +1)* 粒度 - 1)字节大小的地址空间,即(0xFFFFF+1)*4K-1=4,294,967,296=UINT32_MAX,即整个地址空间。

 Hello OS 中段描述符表

GDT_START:
knull_dsc: dq 0
;第一个段描述符CPU硬件规定必须为0
kcode_dsc: dq 0x00cf9e000000ffff
# 转换成二进制代码:0000 0000 1100 1111 1001 1110 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
# 对照 <保护模式段描述符>比对含义
# 段基地址=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
kdata_dsc: dq 0x00cf92000000ffff # 转换成二进制代码: 0000 0000 1100 1111 1001 0010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
# 段基地址=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

段长度需要和 G 位配合,若 G 位为 1 则段长度等于 0xfffff 个 4KB。上面段描述符的 DPL=0,这说明需要最高权限即 CPL=0 才能访问。

保护模式中断

因为实模式下 CPU 不需要做权限检查(实模式下内核与用户程序处于同一特权级),所以它可以直接通过中断向量表中的值装载 CS:IP 寄存器就好了。

保护模式下的中断要权限检查,还有特权级的切换,所以就需要扩展中断向量表的信息,即每个中断用一个中断门描述符来表示,也可以简称为中断门,中断门描述符依然有自己的格式,如下图所示。

保护模式要实现中断,也必须在内存中有一个中断向量表,同样是由 IDTR 寄存器指向,只不过中断向量表中的条目变成了中断门描述符,如下图所示。

产生中断后,CPU 首先会检查中断号是否大于最后一个中断门描述符,x86 CPU 最大支持 256 个中断源(即中断号:0~255),然后检查描述符类型(是否是中断门或者陷阱门)、是否为系统描述符,是不是存在于内存中。

接着,检查中断门描述符中的段选择子指向的段描述符。

最后做权限检查,如果 CPL 小于等于中断门的 DPL,并且 CPL 大于等于中断门中的段选择子所指向的段描述符的 DPL,就指向段描述符的 DPL。(注:CPL 小于等于中断门的 DPL,说明有权限能执行中断。 CPL 大于等于所指向的段描述符的 DPL,如果不提升 CPL,就会导致没有权限调到该内存,所以指向段描述符的 DPL。)

进一步的,CPL 等于中断门中的段选择子指向段描述符的 DPL,则为同级权限不进行栈切换,否则进行栈切换。如果进行栈切换,还需要从 TSS 中加载具体权限的 SS、ESP,当然也要对 SS 中段选择子指向的段描述符进行检查。

做完这一系列检查之后,CPU 才会加载中断门描述符中目标代码段选择子到 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 寄存器,使之指向全局段描述符表。

lgdt [GDT_PTR]

第三步,设置 CR0 寄存器,开启保护模式。(注: CR0-CR3 32位CPU控制寄存器,控制CPU的功能控制特性)


# 开启 PE
mov eax, cr0
bts eax, 0 # CR0.PE =1 ,bts指令的意思是bit test and set 位测试并设置,BTS 指令先将指定位的值存储到 CF 标志中然后设置该位。BTS eax , 0 是将操作数的第0位放入CF然后置位,因此虽然eax操作后为1,但CF为0
mov cr0, eax

第四步,进行长跳转,加载 CS 段寄存器,即段选择子。

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

这里填入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的内存地址 = 段基址 + 段内偏移地址。中间的dword限定符表示要访问完整的32位地址。

之所以要进行长跳转,这是因为我们无法直接或间接 mov 一个数据到 CS 寄存器中,因为刚刚开启保护模式时,CS 的影子寄存器还是实模式下的值,所以需要告诉 CPU 加载新的段信息。新架构下的影子寄存器只会在段寄存器接收到新的段选择子的时候才会更新。这里,因为我们修改了段描述符,所以需要重载刷新。

接下来,CPU 发现了 CR0 寄存器第 0 位的值是 1,就会按 GDTR 的指示找到全局描述符表,然后根据索引值 8,把新的段描述符信息加载到 CS 影子寄存器,当然这里的前提是进行一系列合法的检查。

到此为止,CPU 真正进入了保护模式,CPU 也有了 32 位的处理能力。

长模式

长模式又名 AMD64,因为这个标准是 AMD 公司最早定义的,它使 CPU 在现有的基础上有了 64 位的处理能力,既能完成 64 位的数据运算,也能寻址 64 位的地址空间。

长模式寄存器

长模式相比于保护模式,增加了一些通用寄存器,并扩展通用寄存器的位宽,所有的通用寄存器都是 64 位,还可以单独使用低 32 位。这个低 32 位可以拆分成一个低 16 位寄存器,低 16 位又可以拆分成两个 8 位寄存器,如下表。

长模式段描述符

长模式依然具备保护模式绝大多数特性,如特权级和权限检查。

长模式下的段描述的格式如下:

在长模式下,CPU 不再对段基址和段长度进行检查,只对 DPL 进行相关的检查,这个检查流程和保护模式下一样。

当描述符中的 L=1,D/B=0 时,就是 64 位代码段,DPL 还是 0~3 的特权级。然后有多个段描述在内存中形成一个全局段描述符表,同样由 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

段长度和段基址都是无效的填充为 0,CPU 不做检查。但是上面段描述符的 DPL=0,这说明需要最高权限即 CPL=0 才能访问。若是数据段的话,G、D/B、L 位都是无效的。

长模式中断

保护模式下为了实现对中断进行权限检查,实现了中断门描述符,在中断门描述符中存放了对应的段选择子和其段内偏移,还有 DPL 权限,如果权限检查通过,则用对应的段选择子和其段内偏移装载 CS:EIP 寄存器。

长模式支持 64 位内存寻址,所以要对中断门描述符进行修改和扩展,下面我们就来看看长模式下的中断门描述符的格式,如下图所示。

首先为了支持 64 位寻址中断门描述符在原有基础上增加 8 字节,用于存放目标段偏移的高 32 位值。其次,目标代码段选择子对应的代码段描述符必须是 64 位的代码段。最后其中的 IST 是 64 位 TSS 中的 IST 指针

长模式也同样在内存中有一个中断门描述符表,只不过表中的条目(如上图所示)是 16 字节大小,最多支持 256 个中断源,对中断的响应和相关权限的检查和保护模式一样。

切换到长模式

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

第一步,准备长模式全局段描述符表。


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 页表,这个是为了开启分页模式,切换到长模式必须要开启分页。长模式下已经不对段基址和段长度进行检查了,那么内存地址空间就得不到保护了。

长模式下内存地址空间的保护交给了 MMU,MMU 依赖页表对地址进行转换,页表有特定的格式存放在内存中,其地址由 CPU 的 CR3 寄存器指向。


mov eax, cr4
bts eax, 5 ;CR4.PAE = 1
mov cr4, eax ;开启 PAE
mov eax, PAGE_TLB_BADR ;页表物理地址
mov cr3, eax

第三步,加载GDTR寄存器,指向全局段描述表:

lgdt [eGdtPtr]

第四步:开启长模式,要同时开启保护模式和分页模式,在实现长模式时定义了 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

第五步:进行跳转,加载 CS 段寄存器,刷新其影子寄存器。

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

切换到长模式和切换保护模式的流程差不多,只是需要准备的段描述符有所区别,还有就是要注意同时开启保护模式和分页模式。

小结

  1. 实模式,早期 CPU 是为了支持单道程序运行而实现的,单道程序能掌控计算机所有的资源,早期的软件规模不大,内存资源也很少,所以实模式极其简单,仅支持 16 位地址空间,分段的内存模型,对指令不加限制地运行,对内存没有保护隔离作用。

  2. 保护模式,随着多道程序的出现,就需要操作系统了。内存需求量不断增加,所以 CPU 实现了保护模式以支持这些需求。

    保护模式包含特权级,对指令及其访问的资源进行控制,对内存段与段之间的访问进行严格检查,没有权限的绝不放行,对中断的响应也要进行严格的权限检查,扩展了 CPU 寄存器位宽,使之能够寻址 32 位的内存地址空间和处理 32 位的数据,从而 CPU 的性能大大提高。

  3. 长模式,又名 AMD64 模式,最早由 AMD 公司制定。由于软件对 CPU 性能需求永无止境,所以长模式在保护模式的基础上,把寄存器扩展到 64 位同时增加了一些寄存器,使 CPU 具有了能处理 64 位数据和寻址 64 位的内存地址空间的能力。

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

参考资料

彭东-操作系统实战45讲

操作系统实战45讲笔记- 05 CPU工作模式:程序执行的三种模式的更多相关文章

  1. MySQL实战45讲笔记一

    MySQL的基本架构大体可以分为server层和存储引擎层,逻辑架构图如下: Server层除了图中显示的,还包括所有的内置函数(包括日期.时间.数学和加密函数等),存储过程.触发器.视图等跨存储引擎 ...

  2. Mysql实战45讲 05讲深入浅出索引(下)极客时间 读书笔记

    极客时间 Mysql实战45讲 04讲深入浅出索引(下)极客时间 笔记体会: 回表:回到主键索引树搜索的过程,称为回表覆盖索引:某索引已经覆盖了查询需求,称为覆盖索引,例如:select ID fro ...

  3. 极客时间 Mysql实战45讲 07讲行锁功过:怎么减少行锁对性能的影响笔记 极客时间

    极客时间 Mysql实战45讲 07讲行锁功过:怎么减少行锁对性能的影响笔记 极客时间极客时间 Mysql实战45讲 07讲行锁功过:怎么减少行锁对性能的影响笔记 极客时间 笔记体会: 方案一,事务相 ...

  4. Mysql实战45讲 04讲深入浅出索引(上)读书笔记 极客时间

    极客时间 Mysql实战45讲 04讲深入浅出索引 极客时间(上)读书笔记  笔记体悟 1.索引的作用:提高数据查询效率2.常见索引模型:哈希表.有序数组.搜索树3.哈希表:键 - 值(key - v ...

  5. 深挖计算机基础:MySQL实战45讲学习笔记

    参考极客时间专栏<MySQL实战45讲>学习笔记 一.基础篇(8讲) MySQL实战45讲学习笔记:第一讲 MySQL实战45讲学习笔记:第二讲 MySQL实战45讲学习笔记:第三讲 My ...

  6. 《MySQL实战45讲》(8-15)笔记

    MySQL实战45讲 目录 MySQL实战45讲 第八节: 事务到底是隔离的还是不隔离的? 在MySQL里,有两个"视图"的概念: "快照"在MVCC里是怎么工 ...

  7. Mysql实战45讲 06讲全局锁和表锁:给表加个字段怎么有这么多阻碍 极客时间 读书笔记

    Mysql实战45讲 极客时间 读书笔记 Mysql实战45讲 极客时间 读书笔记 笔记体会: 根据加锁范围:MySQL里面的锁可以分为:全局锁.表级锁.行级锁 一.全局锁:对整个数据库实例加锁.My ...

  8. MySQL实战45讲学习笔记:第三十九讲

    一.本节概况 MySQL实战45讲学习笔记:自增主键为什么不是连续的?(第39讲) 在第 4 篇文章中,我们提到过自增主键,由于自增主键可以让主键索引尽量地保持递增顺序插入,避免了页分裂,因此索引更紧 ...

  9. 《MySQL实战45讲》(1-7)笔记

    <MySQL实战45讲>笔记 目录 <MySQL实战45讲>笔记 第一节: 基础架构:一条SQL查询语句是如何执行的? 连接器 查询缓存 分析器 优化器 执行器 第二节:日志系 ...

  10. 《MySQL实战45讲》个人笔记-基础篇

    拜读了林晓斌大佬的<MySQL实战45讲>,特意做个知识点总结,以便后期回忆. 01.基础架构:一条SQL查询语句是如何执行的? Server 层包括连接器.查询缓存.分析器.优化器.执行 ...

随机推荐

  1. Postgresql架构体系解析

    一.PostgreSQL物理架构 postgresql的物理架构非常简单,它由共享内存.一系列后台进程和数据文件组成. 二.Shared Memory 共享内存是服务器服务器为数据库缓存和事务日志缓存 ...

  2. Qt-FFmpeg开发-视频播放【软解码 + OpenGL显示RGB图像】(3)

    Qt-FFmpeg开发-视频播放[软解码 + OpenGL显示RGB图像] 目录 Qt-FFmpeg开发-视频播放[软解码 + OpenGL显示RGB图像] 1.概述 2.实现效果 3.FFmpeg软 ...

  3. Django中models的字段

    常见的field类型: 1.AutoField 自增字段,它是一个根据ID自增长的IntegerField字段,通常不用自己设置,如果没有设置主键,django会自动添加它为主键字段 2.CharFi ...

  4. 洛谷P4726 【模板】多项式指数函数(多项式 exp)

    题目 https://www.luogu.com.cn/problem/P4726 思路 (略) 是个板题,但是包含了很多多项式的基础板子,适合用来练手. 据说递归版的好写(好抄),但是我猜测和fft ...

  5. CF1738E Balance Addicts

    个人思路: \(sum_i\) 表示前 \(i\) 个数的前缀和,推一下式子可知是要选若干对 \(l_i,r_i\),使得 \(l_1 < l_2 <\dots < l_k \le ...

  6. 设置点击事件时Unable to preventDefault inside passive event listener due to target being treated as passive

    问题 当使用fastClick.js设置点击事件时,控制台报错: [Intervention] Unable to preventDefault inside passive event listen ...

  7. 使用Java API操作Neo4j

    使用IntelliJ IDEA (Jet Brains) 创建java工程 把下载好的neo4j包中的lib文件下的所有jar文件复制到工程Neo4j_Project的lib文件夹下(若无lib文件夹 ...

  8. C++中链表报错member access within null pointer of type 'ListNode'

    报错原因:指针有指向空节点的可能,所以报错,C++中链表的使用比较严格 解决方法:在给指针确定指向节点之前,先判断此节点是否为空节点

  9. DotNetCore2.1使用GitLab通过阿里云自动构建镜像上传阿里云仓库在Docker运行

    操作步骤: 1.安装GitLab并添加项目(此处省略安装过程) 2.获取GitLab的Access Tokens 3.创建空的DotNetCore2.1 Api项目 4.项目添加Docker支持,文件 ...

  10. golang运算符

    1.算术运算符 算术运算符有:+,-,*,/,%,++,-- 1.1 加号(+) 表示正数 数字相加 字符串拼接 package main import ( "fmt" ) fun ...