1.为什么要进入保护模式?
1.1在没有保护模式的情况下
在没有进入保护模式的情况下,也就是实模式,我们来看看一些的操作这会带来什么问题?
在讲解这些操作之前,我们最好理解这里的操作指的是什么,也就是计算机操作。我觉得最好的办法是用代码描述这种操作,而不是纯文字,因为用中文描述会感觉很抽象,而且计算机只能听懂代码。
我们来看看,在没有保护模式的情况下,实模式的一些代码操作:
mov ax, 0xb800 ; 文本模式显存段 mov ds, ax mov byte [0xb0], 'H' ; 向显存写入字符我们知道0xb8000处是显存段,上述代码向显存段写入了字符。在实模式下,CPU 不会阻止这段代码执行,哪怕它是一个普通用户程序。这就带来了问题:操作系统是管理硬件的唯一管理者,如果任何用户程序都能随意修改显存、改写硬件状态,那操作系统的管理就形同虚设了。用户程序想要操作硬件,应该向操作系统申请。
mov ax, 0x0000 ; 中断向量表所在段 mov ds, ax mov byte [0x30], 0x00 ; 破坏中断向量表内容实模式下,0x00000~0x003FF处存放的是中断向量表。上述代码直接改写了中断向量表的内容。如果此时发生对应的中断,CPU 会按照被破坏的地址去寻找中断服务程序,这极易导致系统崩溃死机。
1.2保护模式核心思想
分析上面的操作,本质问题就是代码可以随意访问内存的各个段,那么,保护模式怎么解决这个问题呢?
想象一下,有一天你去荒野求生了,发现河边有条草鱼,你肯定会兴奋的去抓它,但是如果你看见一只熊猫,你还会去抓它吗,尽管它很可爱,但本质原因是你知道它是保护动物这个信息,所以阻止了你这个行为。
所以,可以这么说,你做的每一步动作都是基于信息来决定你是否该执行的。就好像有人给你做了道牛欢喜的菜,在你不知道者是什么的时候你吃的很香,但是如果你知道它是什么的时候,你吃这个动作可能就不会执行了~
代码是一种CPU执行的动作,它可以有各种动作,有些动作会破坏内存中的某个段。那么,我们是不是可以给内存的每个段增加一些“属性信息”(比如:谁可以访问它?能写还是只能读?),在 CPU 准备执行代码操作的时候,由 CPU 硬件强制根据这些信息来判断是否允许执行此次操作。
这就是保护模式的核心思想。下篇文章我们来看看这些“信息”到底是什么,以及 CPU 是怎么根据它们来拦截操作的。
1.3描述符
1.3.1描述符分类
这些用于描述内存段属性信息的数据结构,在 x86 中就被称为描述符。描述符可以有多种,但无论何种描述符,都只占8个字节。
描述符可以分两大类:
存储器的段描述符:
- 用于描述一般的数据段,代码段。
系统描述符:
门描述符:描述与程序执行有关的逻辑结构
LDT 描述符:指向局部描述符表 LDT
TSS 描述符:指向任务状态段 TSS
那么,既然描述符都是8字节,那么该如何区分它们呢?
8字节大小,比特编号从 0 到 63 ,核心判断字段:
S 位(第 44 位,1 比特):描述符大类判断标志
S 位 = 0 → 该描述符是系统描述符
S 位 = 1 → 该描述符是存储器的段描述符
Type 字段(第 40~43 位,4 比特):描述符具体类型判断标志
若为系统描述符(S=0):Type 字段用于指定系统段的类型或门的具体类型
若为存储器段描述符(S=1):Type 字段用于指定是代码段还是数据段,以及相关属性
1.3.2存储器的段描述符结构
存储器的段描述符是最基本的描述符,所以我们开始先讲最基本的东西,那么,我们来看看存储器的段描述符结构吧,看看这个描述符具体描述了代码段,数据段的哪些信息。
我们知道了存储器段描述符S=1,Type 字段用于指定是代码段还是数据段,Type字段有四位,最高位是X位,据此分为数据段和代码段:
X=0,则该描述符描述的是数据段→
剩余 3 位为 E、W、A 位:
E 位(扩展方向位,expand)
E=1:向下扩展,从高地址向低地址扩展,通常是栈段
E=0:向上扩展,从低地址向高地址读写,属于普通数据段
假设一个段的描述符中基地址为0x00000000,段界限为0x7c00。当E=0,可以访问的范围是0x00000000~0x00007C00;当E=1,可以访问的范围是0x00007C01~0xFFFFFFFFF
W 位(可写位,writable)
W=0:段只读,写入会引发处理器异常中断
W=1:段可正常写入
A 位(已访问位,accessed)
A=0:描述符创建时,A 位清零
A=1:段被访问时,处理器自动将 A 位置 1
X=1,则该描述符描述的是代码段→
剩余 3 位为 C、R、A 位:
C 位(依从位)
C=0:非依从代码段,仅特权级相同的程序可直接转移执行
C=1:依从代码段,低特权级程序可直接转移执行;无特殊要求通常清零
此位仅作了解,当学到特权级保护后就知道了
R 位(可读位)
R=0:代码段不可读
R=1:代码段可读,可像只读存储器一样使用
A 位(已访问位,accessed)
A=0:描述符创建时,A 位清零
A=1:段被访问时,处理器自动将 A 位置 1
数据段和代码段描述符共同字段→
(简单了解即可,只有用到了才知道有什么用)
低双字0~15位+高双字16~19位:段界限
低双字16~31位+高双字0~7和24~32位:段基地址
高双字13~14位:DPL
对段的访问实施管控,限制可访问该段的程序
取值与特权级对应:00→0 级、01→1 级、10→2 级、11→3 级(0为最高特权级,3最低特权级)
高双字的位15位:P位
指示描述符对应的段是否可立即访问
高双字的位 20:AVL位
规则:处理器不使用此位,完全保留给操作系统自定义使用
高双字的位 21:L位
指示是否为 64 位代码段
32 位系统要求:此位固定置 0
高双字的位 22:D/B位
作用:指示段的默认操作位数
D/B=0:段按 16 位进行操作
D/B=1:段按 32 位进行操作
影响:决定向下扩展段的界线最大值(0xFFFF/0xFFFFFFFF),以及使用 SP 还是 ESP 寄存器
高双字的位 23:G位粒度位
核心作用:解释段界线的计数单位
G=0:段界线以字节为单位
G=1:段界线以4KB(0x1000 字节)为单位
实际界线计算公式(G=1 时):
实际段界线 = 描述符中的段界线 × 0x1000 + 0xFFF
1.4描述符表
1.4.1描述符表分类
这些描述符表得有个地方集中存放吧?不然CPU怎么找到呢。
x86 保护模式下,描述符集中存放在内存中,形成一个表格,叫做描述符表。描述符表主要包括 GDT、LDT 和 IDT。
其中 GDT 和 LDT 存放段描述符,IDT 存放门描述符。GDT 全局唯一,LDT 可以有多个。
1.4.2 全局描述符表GDT
那么,这么多张表,CPU怎么知道要用哪张表?拿GDT举例,处理器内部有一个 48 位的全局描述符表寄存器 GDTR,用于跟踪 GDT。如图所示。
那么,LDT和IDT是不是也有个寄存器跟踪,LDTR和IDTR?还真是。但这不是本文重点,就不讲解了。
1.4.3全局描述符表寄存器GDTR
那么,来看看GDTR的结构吧,看看它是怎么跟踪GDT的。
GDTR寄存器总共有48位,如图所示。
0~15位:GDT表的界限值,也就是这张表有多大,界限为 16 位 → GDT 最大64 KB,每个描述符 8 字节 → 最多可容纳8192个描述符。
16~47位:GDT表的起始线性地址,也就是这张表起始的位置再哪,线性地址即内存的连续地址,通常等于物理地址。
1.5保护模式相关工作
1.5.1如何进入保护模式
那么讲了这么多,该如何进入保护模式呢?其实很简单,只需要设置CR0寄存器即可。
CR0 是处理器内部的 32 位控制寄存器,包含一系列控制处理器操作模式和运行状态的标志位。
位 0(PE 位):Protection Enable(保护模式允许位)。将此位置 1,处理器即进入保护模式并按保护模式规则运行。
1.5.2进入保护模式之前的相关工作
设置 PE 位进入保护模式之前,必须保证:
已通过端口
0x92开启 A20地址线。执行
CLI指令禁止中断:保护模式下的中断机制与实模式完全不同,原有中断向量表不再适用,新的中断处理机制尚未建立,BIOS 中断也不能再用,若此时发生中断必然出错。
in al,0x92 ;南桥芯片内的端口 or al,0000_0010B out 0x92,al ;打开A20 cli ;保护模式下中断机制尚未建立,应 ;禁止中断 mov eax,cr0 or eax,1 mov cr0,eax ;设置PE位1.6保护模式内存访问方式
1.6.1通过描述符表的索引值找到描述符
进入保护模式后,那么CPU怎么访问某个内存段呢?既然每个内存段的信息都保存在描述符中,而这些描述符又一个个的保存在描述符表中,所以要访问某个内存段,应该通过这张表的索引值来找到对应的描述符。描述防护里有内存段的地址等属性信息,有了描述符,找到想找的内存段不是轻而易举了吗?就比如我拿到了你的身份证,找到你家的地址是不是很简单?
还是和前文一样的道理,这里说的内存访问,只是中文描述,这很抽象,我们将其转化为代码:
mov cx, 00000000000_01_000B mov ds, cx进入保护模式后,段寄存器传入的不再是逻辑地址,而是描述符选择子,选择子由三部分组成 ,结构如下:
其中位3到15位存的是描述符表的索引,CPU通过GDTR寄存器找到描述符表的基地址,然后再用描述符表索引值*8+GDT基地址(因为每个描述符是8字节),即可找到对应的描述符。
1.6.2将描述符存入高速缓存寄存器
找到了描述符,这个时候是不是要存起来了?那么,存放哪里呢?
32 位处理器为每个段寄存器增加了一个不可见的部分——描述符高速缓存器,用于存放:
段的线性基地址
段的界限值
段的访问属性
所谓不可见,说的是我们无法访问。
1.6.3内部流程
当一条指令将选择子加载到段选择器时,处理器自动执行以下内部过程:
mov cx, 00000000000_01_000B mov ds, cx将选择子中的索引号 × 8,得到该描述符在 GDT 中的字节偏移量
根据TI 位确定使用哪个描述符表(此处 TI=0,使用 GDT)
从GDTR中取出 GDT 的线性基地址
将 GDT 基地址 + 索引偏移量 = 目标描述符的线性地址
从内存中读取 8 字节描述符,将其解析的内容(基地址、界限、属性)加载到描述符高速缓存器中
至此,ds的描述符高速缓存器中存放了该内存段的描述符,下次访问该段时直接通过描述符高速缓存器中取出对应的描述符,例如以下代码:
mov bx,0x000A mov ax,0x000B mov [ds:bx],ax注意,只要不修改ds的值,下次内存访问时,CPU 就直接用之前缓存的描述符,不需要重新查表了,这是硬件加速的机制。