x86 CPU工作的三种模式
x86 CPU工作的三种模式
x86 CPU有三种工作模式:实模式、保护模式、长模式
内容硬核,涉及到很多OS的知识,如有错误,请通知我!
实模式
实模式概念
实模式:实地址模式
他有两个特点:
- 运行真实的指令(即任何指令都可以直接执行)
- 执行的内存地址是实际的物理内存地址
实模式下的寄存器
寄存器此处简单罗列一下,这里涉及到很多汇编的知识,先混个眼熟,之后用到了,我会细说
寄存器按类型分为三种:通用寄存器、控制寄存器、段寄存器
通用寄存器:(分三类)
- 数据寄存器: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、索引的具体过程(重点,细品)
- 收到中断信号,取中断类型码:其来源可能是CPU内部、也可能是IO设备发来的
- 内部的中断:CPU内部可以直接得到中断号;
- 外部的中断:那么数据总线会传入其中断号;
- 保存当前的CPU状态(比如FLAGS寄存器)
- 保存当前的CS、IP(将CS、IP入栈即可)
- 通过IDTR寄存器+中断号去查中断表
- 设置CS为代码段基地址,IP为代码段内偏移
- 执行对应CS+IP的程序
- 执行完成后,出栈,再返回执行原来的指令
保护模式
实模式存在的问题
设想如此的一段代码:
1 | int main(){ |
它会做些什么?关闭中断,进入死循环,不停的将内存地址置零
简单来说,这段程序清空了内存,实模式下竟然可以运行这样的代码,显然是不合理的,因此提出了保护模式
实模式下存在的问题有:
- 使用16位的寄存器,寻址范围小
- 任何指令都可以执行
- 可以访问任何内存地址
因此保护模式要实现:
- 扩展寻址范围(扩展寄存器位数)
- 区分的执行指令(特权级)
- 限制可以访问的内存范围(段描述符)
保护模式的寄存器
为了解决寻址范围小的问题,将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,权利依次降低,他们的权利范围如图:

可以用两位表示这四个权级
1 | 00 R0 |
因此,越小表示权利越大
(注意: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 | GDT_START: |
可见,这段代码中kcode_dsc
与kdata_dsc
两个段的起始地址与段长度都相同
且均设G=1
代表段长度的粒度为4KB,均设DPL为R0级别才可以访问
(可以结合前面段描述符的图来看)
保护模式的中断
与实模式的中断处理的区别?中断门
保护模式提出了特权级及其切换,所以需要检查特权,因此有了新的结构中断门描述符(中断门)
中断门的结构:类似于段描述符
中断向量表条目换成中断门
处理中断的步骤【重点】
- 收到中断信号,取中断类型码:其来源可能是CPU内部、也可能是IO设备发来的
- 内部的中断:CPU内部可以直接得到中断号;
- 外部的中断:那么数据总线会传入其中断号;
- 保存当前的CPU状态(比如FLAGS寄存器)
- 保存当前的CS、EIP(将CS、IP入栈即可)
- 通过IDTR寄存器+中断号去查中断表
- 【新】检查中断号是否超过范围(即大于最后一个中断门,x86允许256个中断源)
- 【新】检查描述符类型(是中断门还是陷阱门、是否是系统描述符、是否在内存中)
- 【新】检查中断门描述符中的段选择子指向的段描述(即找到中断处理程序)
- 【新,重点】权限检查:如果
CPL<=中断门DPL && CPL >=中断门段选择子DPL
就设置CS为代码段基地址,EIP为代码段内偏移 - 执行对应CS+EIP的程序(这里还涉及到通过GDTR寄存器,再去查GDT全局段描述符表,然后再找对应的段)
- 执行完成后,出栈,再返回执行原来的指令
以上是我的理解方式,下面放一个不错的理解,建议结合食用:
保护模式中断发生步骤:
- 中断发生后,根据中断号码,对比cpu IDTR寄存器指示的中断门描述符表,找出中断对应的中断门描述符表
- 中断门描述符表中,找出中断门对应的DPL判断权限, 目标代码偏移地址,目标代码段选择子。
- 根据目标代码段选择子中的段描述符索引,查找GDTR寄存器(指向全局段描述符表)指示的全局段描述符表,找出择子指向目标代码的段描述符,目标代码段RPL(进行权限判断)
- 根据段描述符,找出对应的中断程序代码段地址。
- 根据以上步骤将目标代码段地址及偏移地址 装载到CS:EIP 寄存器 其中权限对比
cpl >=中断段选择符DPL
,保证中断服务处理程序权限大于触发中断的应用程序,禁止中断调用用户程序,防止恶意用户程序,而又不妨碍用户态和内核态产生中断;cpl<= 中断门描述符DPL
,确保应用程序有足够的权限引起中断,防止用户态程序调用特殊中断。
权限检查为什么要
CPL<=中断门DPL && CPL >=中断门段选择子DPL
?
CPL<=中断门DPL
:只有权比门大,才能让门打开(确保应用程序有足够的权限引起中断,防止用户态程序调用特殊中断)CPL >=中断门段选择子DPL
:表示引起中断的程序的权利不能比中断处理程序的权利大(防止恶意用户程序,而又不妨碍用户态和内核态产生中断)
实模式切换到保护模式
总共需要4步:
准备全局描述符表GDT
1
2
3
4
5
6
7
8GDT_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寄存器,指向GDT
1
lgdt [GDT_PTR]
设置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)进行长跳转
1
jmp dword 0x8 :_32bits_mode ;_32bits_mode为32位代码标号即段偏移
此刻,我们的OS就进入了保护模式!
为什么要进行长跳转?
因为我们无法直接或间接 mov 一个数据到 CS 寄存器中,因为刚刚开启保护模式时,CS 的影子寄存器(影子寄存器是硬件,程序员没办法设置)还是实模式下的值,所以需要告诉 CPU 加载新的段信息
为什么要设置为0x8?
1 | 段描述符索引 TI(第三位) CPL(最后两位) |
即表示以R0的权限访问0x1
的值(即GDT第一个描述符)
长模式
长模式,又名 AMD64 模式,最早由 AMD 公司制定
为什么要进入长模式
在保护模式中,我们的位数32位,这显然不能满足我们的使用(尤其是今日,你在用多大的内存呢)
所以长模式,就进行了进一步的扩展,长模式在保护模式的基础上,把寄存器扩展到 64 位同时增加了一些寄存器
使 CPU 具有了能处理 64 位数据和寻址 64 位的内存地址空间的能力
长模式的段描述符
长模式的段描述符去掉了段基址、段长度、Type内的一些字段
注意:
G
无效了L
可以设置为是否为64位模式(数据段无效)- 段长度和段基址都是无效的填充为 0,CPU 不做检查
长模式的中断门描述符
扩展到64位,使用高32位存储代码段偏移的高位,其他内容与保护模式相同
切换到长模式
切换到长模式可以从实模式直接切换,也可以从保护模式切换
准备全局描述符
1
2
3
4
5
6
7
8ex64_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)
1
2
3
4
5mov eax, cr4
bts eax, 5 ;CR4.PAE = 1
mov cr4, eax ;开启 PAE
mov eax, PAGE_TLB_BADR ;页表物理地址
mov cr3, eax加载 GDTR 寄存器,使之指向全局段描述表
1
lgdt [eGdtPtr]
开启长模式,要同时开启保护模式和分页模式
(此处还涉及到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跳转,加载CS段寄存器,刷新影子寄存器
1
jmp 08:entry64 ;entry64为程序标号即64位偏移地址
长模式总结
结构方面:扩展到64位,增加了寄存器
长模式的改变:弱化段模式管理,只保留了权限级别的检查,忽略了段基址和段长度,而地址的检查则交给了 MMU
参考资料
- 《CSAPP》
- 《汇编语言》王爽著
- 极客时间《操作系统实战45讲》
- 《x86汇编语言 从实模式到保护模式》