文章目录
前言
今天来学习 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讲——彭东老师