操作系统实践之路——三、硬件(1.CPU工作模式)

前言

​ 今天来学习 CPU 的工作模式,硬件中最重要的就是 CPU,它就是执行程序的核心部件。按照 CPU 功能升级迭代的顺序,CPU 的工作模式有实模式、保护模式、长模式,这几种工作模式下 CPU 执行程序的方式截然不同,下面来讨论这几种工作模式。

一、实模式

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

1.1实模式寄存器

​ 由于CPU是根据指令完成相应的功能,举个例子:ADD AX,CX;这条指令完成加法操作,AX、CX 为 ADD 指令的操作数,可以理解为 ADD 函数的两个参数,其功能就是把 AX、CX 中的数据相加。指令的操作数,可以是寄存器、内存地址、常数,其实通常情况下是寄存器,AX、CX 就是 x86 CPU 中的寄存器。

​ 看看 x86 CPU 在实模式下的寄存器。表中每个寄存器都是 16 位的。
在这里插入图片描述

1.2实模式下访问内存

​ 我们都知道,数据和指令都是存放在内存中,所以我们要访问内存,而访问内存靠的是地址值。

​ 那这个地址值是如何计算呢?如下图所示:

在这里插入图片描述

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

代码段地址+左移4位+ IP = 取指
数据段+左移4位+ 通用寄存器值 = 数据地址
栈段SS+左移4位 + SP = 栈地址

​ 注意:代码段是由 CS 和 IP 确定的,而栈段是由 SS 和 SP 段确定的!!

1.3实模式中断

​ 中断就是中止执行当前的程序,转到另一个特定的地址上,去运行特定的代码。那么实模式下它的中断是如何产生的呢?

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

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

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

在这里插入图片描述

​ 有了中断号后,CPU 就能根据 IDTR 寄存器中的信息,计算出中断向量中的条目,进而装载 CS(装入代码段基地址)、IP(装入代码段内偏移)寄存器,最终响应中断。
即:中断号+ IDTR 寄存器(指向中断表的地址和长度) —> 根据中断号,找到中断表中的对应条目 —> 解析出中断函数基地址填充CS、中断函数偏移填充IP —> 响应中断。

二、保护模式

​ 随着软件的规模不断增加,需要更高的计算量和更大的内存容量。而内存一大,首先就要解决的问题就是寻址问题。原本实模式下的16位寄存器最大只能表示2的16次方个地址,所以CPU的寄存器和运算单位都要扩展成32位的。

​ 虽然CPU内部器件的位数解决了计算和寻址的问题,但依旧是没有解决实模式场景下的问题:①CPU对任何指令不加区分的执行;②CPU对访问内存的地址不加限制。

​ 基于以上的原因,CPU实现了保护模式。

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

2.1保护模式寄存器

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

在这里插入图片描述

2.2保护模式特权级

​ 为了区分哪些指令和哪些资源可以被访问,CPU实现了特权级。

​ 下图中,从外到内,既能体现权力的大小,又能体现各特权级对资源控制访问的多少,还能体现各特权级之间的包含关系。R0~R3,每个特权级执行的指令的数量不用。R0 拥有最大权力,可以访问低特权级的资源,反之则不行。

在这里插入图片描述

2.3保护模式段描述符

​ 由于CPU的扩展导致了 32 位的段基地址和段内偏移,还有一些其它信息,所以 16 位的段寄存器肯定放不下。放不下就要找内存借空间,然后把描述一个段的信息封装成特定格式的段描述符,放在内存中,其格式如下。一个段描述符有 64 位 8 字节数据,里面包含了段基地址、段长度、段权限、段类型(可以是系统段、代码段、数据段)、段是否可读写,可执行等。

在这里插入图片描述

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

在这里插入图片描述

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

​ 通过内存中存放64位段描述符实现特权划分,段地址寻址。
CS | DS | SS (段描述符索引)+ GDTR(指向全局段描述符表基地址) – > 找到段描述符 --> 解析代码段还是数据段,地址,访问权限

2.4保护模式段选择子

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

在这里插入图片描述

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

CPL:当前权限级别(CS 和 SS 中 RPL 就组成了 CPL)

RPL:请求权限级别

DPL:描述符权限级别

CPL是你的权限,RPL是你要请求的权限级别,一般是CPL=RPL,也可以CPL<RPL,毕竟就四个权限级别,但是如果 CPL > DPL,那么CPU就禁止你访问了,权限不够。

2.5保护模式平坦模型

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

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

​ 来看看前面Hello OS 中段描述符表,如下所示:

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
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

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

2.6保护模式中断

​ 实模式下CPU不需要做权限检查,所以它可以直接通过中断向量表中的值装载CS:IP 寄存器就好了。

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

在这里插入图片描述

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

在这里插入图片描述

过程

​ 中断号+ IDTR 寄存器(指向中断表) —> 根据中断号,找到中断表指向的内存中的中断门描述符 —> 中断门和中断描述符中段选择子的权限检查–> 中断门描述符中目标代码段选择子填充CS, 目标代码段偏移填充EIP —> 响应中断。

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

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

最后权限检查,如果 CPL 小于等于中断门的 DPL,并且 CPL 大于等于中断门中的段选择子所指向的段描述符的 DPL,就指向段描述符的 DPL。

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

​ 做完这一系列检查之后,CPU 才会加载中断门描述符中目标代码段选择子到 CS 寄存器中,把目标代码段偏移加载到 EIP 寄存器中。

2.7切换到保护模式

​ 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 寄存器,开启保护模式。

;开启 PE
mov eax, cr0
bts eax, 0                      ; CR0.PE =1
mov cr0, eax         

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

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

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

三、长模式

​ 长模式又名 AMD64,因为这个标准是 AMD 公司最早定义的,它使 CPU 在现有的基础上有了 64 位的处理能力,既能完成 64 位的数据运算,也能寻址 64 位的地址空间。这在大型计算机上犹为重要,因为它们的物理内存通常有几百 GB。

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

3.1长模式寄存器

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

在这里插入图片描述

3.2长模式段描述符

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

​ 长模式下段描述的格式如下图所示:

在这里插入图片描述

​ 在长模式下,CPU 不再对段基址和段长度进行检查,段长度和段基址都是无效的填充为 0,只对 DPL 进行相关的检查,这个检查流程和保护模式下一样。

​ 当描述符中的 L=1,D/B=0 时,就是 64 位代码段,DPL 还是 0~3 的特权级。然后有多个段描述在内存中形成一个全局段描述符表,同样由 CPU 的 GDTR 寄存器指向。

3.3长模式中断

​ 看看长模式下的中断门描述符的格式,如下图所示:

在这里插入图片描述

​ 首先为了支持 64 位寻址中断门描述符在原有基础上增加 8 字节,用于存放目标段偏移的高 32 位值。

​ 其次,目标代码段选择子对应的代码段描述符必须是 64 位的代码段。最后其中的 IST 是 64 位 TSS 中的 IST 指针,因为我们不使用这个特性,所以不作详细介绍。

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

3.4切换到长模式

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

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

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位偏移地址

4总结

​ 1. X86 CPU的位数不断提高。从16位到32位再到64位,每一次的进步都尽量去兼容之前的CPU架构。前一代的寄存器尽量保留,不够用就拓展新的。而寄存器的长度升级后,其低位可以兼容上一代的寄存器。

​ 2.CPU的安全性提升。从实模式【可随意执行全部CPU指令,随意访问读写内存空间】,到保护模式【将指令划分为ring0~ring3,只有CPU和操作系统允许才可以调用CPU指令和访问内存】,而64位的长模式在安全方面和32位的保护模式并没有本质区别。

​ 3.访问内存的地址变大了。从实模式到保护模式,访问内存时,需要访问的地址变大了,需要控制的内容变多了,于是引入了段描述符,所有的段描述符组成了描述符表,包括唯一的全局描述符GDT和多个局部描述符号LDT。GDT是操作系统特供,要重点关注。保护模式下CPU寻址的时候,要通过段寄存器+GDTR(指向全局段描述符表基地址)寄存器定位到的内存中的描述符,判断是否允许访问。然后,再根据段描述符中地址进行访问。

参考资料

以上内容是我学习彭东老师的《操作系统实战45讲》后所进行的一个笔记记录,如有错误,还请各位大佬多多指教。

我主要参考了以下资料,十分感谢:

操作系统实战45讲——彭东老师

  • 13
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值