上文我们介绍了Intel处理器在实模式下的工作方式以及中断处理方法,但是因为在实模式下,任何软件都可以通过加载合适的段地址对整个内存空间进行读写,没有任何限制。过度的自由会带来过分的行为,于是出现了各种恶意软件去通过修改内存中系统占用的部分或者其他程序占用部分的内容,使得系统崩溃甚至瘫痪。
保护模式
80286时代Intel引入了被称作保护模式的cpu运行状态,但是80286提供的保护模式(下简称16位保护模式)对实模式下的代码兼容差,虽然可以访问16MB的内存,但是访问方法复杂,实模式与保护模式切换的困难,一旦进入保护模式,除非处理器复位,否则无法退出。16位保护模式开辟了保护模式的先河,但是保护模式真正发展起来的时间是80386时代,下面是80386的参数:
参数 | 详细信息 |
---|---|
架构 | 32位 |
时钟频率 | 12 MHz, 16 MHz, 20 MHz, 25 MHz, 33 MHz |
指令集 | x86(32位扩展) |
地址总线 | 32位,支持4 GB的物理内存地址空间 |
数据总线 | 32位 |
寄存器 | 32位通用寄存器(EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP),16位段寄存器(CS、DS、SS、ES、FS、GS),32位指针和索引寄存器 |
内存管理 | 支持分页(4 KB或4 MB页大小),提供虚拟内存管理 |
虚拟内存 | 支持,提供4 GB的虚拟地址空间 |
多任务处理 | 硬件级多任务支持,通过任务状态段(TSS)进行任务切换 |
保护模式 | 完全支持,包括段保护、分页、特权级别、I/O权限等 |
虚拟8086模式 | 允许在32位保护模式下运行8086实模式应用程序,同时提供保护机制 |
管道 | 无 |
缓存 | 无内建缓存(一级缓存和二级缓存由主板或外部芯片提供) |
封装 | PGA(Pin Grid Array)封装,132针脚 |
制造工艺 | 1.5微米 CMOS |
电源电压 | 5V |
指令周期 | 2个时钟周期(最短),复杂指令需要更多的时钟周期 |
引入的特性 | - 32位寄存器和总线 - 支持分页内存管理和虚拟内存 - 引入虚拟8086模式,支持在保护模式下运行实模式程序 - 提供硬件多任务支持 - 完整的保护模式支持,包括段保护和分页机制 - 增加了特权级别和I/O权限管理 |
可以发现80386是32位地址总线和数据总线的处理器,且其指令集也支持32位指令集,80386提供了虚拟8086模式,可以在不重新启动计算机的情况下运行之前实模式下的程序(很多人可能还记得windows 95上的MSDOS方式)。同时改进过的32位保护模式支持了分页、段保护、特权级、I/O保护等机制。
32位保护模式下的寻址方式
为了让程序只能操作自己所拥有的这部分内存,我们就要想办法限制其能力,32位保护模式始终使用分段的形式来进行寻址(即使开启了分页,也会使用分段),程序需要在段寄存器加载相应的段选择子,才能实现对内存的读写。
GDT表与段选择子
GDT表项结构
GDT表是一片在内存中的数据表,Intel手册上是这样介绍GDT表的:
When operating in protected mode, all memory accesses pass through either the global descriptor table (GDT) or an optional local descriptor table (LDT) as shown in Figure 2-1
作者译:在保护模式下操作时,所有内存访问都通过全局描述符表(GDT)或可选的本地描述符表,如图2-1所示
这里附上图2-1
上图来自Intel® 64 and IA-32 Architectures Software Developer’s Manual的图2-1
可以发现,任何地址的访问都会先依赖于GDT(注:LDT在日常使用的不多,这里不再赘述)。
我们先来说说GDT表的结构:
上图来自Intel® 64 and IA-32 Architectures Software Developer’s Manual的图3-8
GDT表项的长度为64位(2个双字),下面来详解各部分的作用
- 段长度(段界限,Segment Limit)有2部分,低16位在GDT表项的第0-15位,高4位在16-19位.
- 段的起点(段基地址,Base Address)在第16-39位,以及高8位(表中第0个双字的第16-31位到第1个双字的0-7位,以及高8位)。
- 40-43位(第一个双字的8-11位)是段类型,细分为X、E、W、A位,其中
X(43位) | E(42位) | W(41位) | A(40位) | 类型 | 功能 |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | 数据段 | 向上扩展(栈)、只读、未访问 |
0 | 0 | 0 | 1 | 数据段 | 向上扩展(栈)、只读、已访问 |
0 | 0 | 1 | 0 | 数据段 | 向上扩展(栈)、可写、未访问 |
0 | 0 | 1 | 1 | 数据段 | 向上扩展(栈)、可写、已访问 |
0 | 1 | 0 | 0 | 数据段 | 向下扩展(栈)、只读、未访问 |
0 | 1 | 0 | 1 | 数据段 | 向下扩展(栈)、只读、已访问 |
0 | 1 | 1 | 0 | 数据段 | 向下扩展(栈)、可写、未访问 |
0 | 1 | 1 | 1 | 数据段 | 向下扩展(栈)、可写、已访问 |
1 | 0 | 0 | 0 | 代码段 | 执行不可读、未访问 |
1 | 0 | 0 | 1 | 代码段 | 执行不可读、已访问 |
1 | 0 | 1 | 0 | 代码段 | 执行可读、未访问 |
1 | 0 | 1 | 1 | 代码段 | 执行可读、已访问 |
1 | 1 | 0 | 0 | 代码段 | 一致性代码段、不可读、未访问 |
1 | 1 | 0 | 1 | 代码段 | 一致性代码段、不可读、已访问 |
1 | 1 | 1 | 0 | 代码段 | 一致性代码段、可读、未访问 |
1 | 1 | 1 | 1 | 代码段 | 一致性代码段、可读、已访问 |
注意:A位由处理器自动置位,但应由软件复位。一致性(Conforming)代码段是32位保护模式中的一种特殊代码段,它允许程序在不改变当前特权级别的情况下执行代码。换句话说,较低特权级别的代码可以直接调用较高特权级别的一致性代码段,而不需要提升自己的权限。
- S位是该段的类型,置位(1)表示为代码段或数据段,复位(0)为系统段
- DPL位是该段的特权级,范围为0~3,其中0为最高特权级
- P位常用于计算机系统的虚拟内存(虚拟化硬盘空间作为内存),当置位(1)时,表面当前段在内存中,当复位(0)时,表明不在内存中,这给我们一种虚拟内存的思路,我们可以将不常用的内存段(考虑A为)内容写入硬盘,并清除该段内容仅保留段描述符并复位P,当有程序加载该段时,会发生#NP异常,异常处理程序将硬盘中的数据读回内存,这样可以让大型程序也可以运行在小内存的PC中。
- AVL位,系统软件使用,Intel对其无定义,通常复位即可。
- L位置位时表面当前段是64位段,复位时表面是32位段
- D/B位标识代码段的操作数位宽,32位段应该置位(1),16位保护模式的程序应该复位(0)
这里提一嘴:
在代码段中该位叫做D,置位时默认操作数和地址大小为32位。这意味着如果指令没有明确指定操作数或地址大小,处理器将假定它们为32位。复位时默认为16位。
在数据段中该位叫做B,置位时堆栈指针(ESP)为32位,栈的操作数也为32位。复位时堆栈指针(SP)为16位,栈的操作数也为16位。 - G位是颗粒度位,置位(1)时段长度等于段大小位*4KB,复位(0)时段长度等于段大小位的数值
GDT表的结构
因为GDT表是在实模式下填充数据的,故因放置在1MB内存空间下(当然可以在保护模式中重新写GDT表至1MB内存以上,但是得重新加载段选择子),GDT表的基地址以及大小被保存在一个叫做GDTR的特殊48位寄存器中。
GDT表结构如下:
字段名称 | 长度(位) | 描述 |
---|---|---|
段界限(Limit) | 16 | 表示GDT的大小,以字节为单位。段界限字段定义了GDT中最后一个字节的地址,实际大小为段界限 + 1。 |
基址(Base) | 32 | 表示GDT在内存中的起始地址,即GDT表项的基地址。 |
注意:第一个GDT表项(也称为“空描述符”或“Null Descriptor”)必须是全零的
加载GDT表
在实模式下写好GDT表后,我们使用lgdt
指令加载GDT表至GDTR寄存器,代码如下:
lgdt [GDT表的内存地址]
段选择子
在进入保护模式后,段寄存器依旧为16位,但是其内容与实模式内容不同,其内容为当前段的段选择子(可以理解为序号)
下面是段选择子的结构:
字段名称 | 位范围 | 长度(位) | 描述 |
---|---|---|---|
索引(Index) | 3 - 15 | 13 | 用于从GDT或LDT中选择特定的段描述符。它表示段描述符表中的偏移量,每个段描述符占用8个字节。 |
TI(表指示符) | 2 | 1 | 表示选择的段描述符是从GDT还是LDT中获取: 0 - 选择GDT中的段描述符 1 - 选择LDT中的段描述符 |
RPL(请求特权级) | 0 - 1 | 2 | 指定请求的特权级(Ring 0至Ring 3),用于访问控制和权限检查。 |
请注意:段索引中不应该是0,因为第一个GDT表项为空
注意:在加载一个段选择子后,cpu会进行如下操作
例如执行此指令:
mov ax, 0010H
mov ds, ax
cpu会解读0010H段选择子的内容,发现是GDT表项且请求特权级为00,随后cpu读取内部的GDTR寄存器获取GDT表的位置以及大小,若GDT表中包含第2项,则读取内存中GDT表项的第二项,随后加载该项内容CPU内部的DS段描述符缓存(高速缓存,隐藏缓存、段高速缓存),然后读取其段描述符的内容进行换算地址…进行一系列操作。
由于段描述符缓存的存在,直接修改GDT表项并不会影响段寄存器的工作,因此应该在GDT表项修改后重新让段加载一次段选择子。