32位保护模式概览
概览
- 基本工作模式
- 线性地址
- 寄存器扩展
- 寻址扩展
- 模式反转
- 指令扩展
- 全局描述符表、特权级、分页等
基本工作模式
Intel 32 位处理器架构简称 IA-32,尽管 8086 是 16 位的处理器,但它也是 32 位架构内的一部分。16 位到 32 位,不单单是地址线和数据线的扩展 ,实际上还有更多的部分,包括高速缓存、流水线、浮点处理部件、多处理器(核)管理、多媒体扩展、乱序执行、分支预测、虚拟化、温度和电源管理等。
80286 首次提出保护模式的概念。80286 的地址线扩充到 24 位,能够访问 16 MB的内存,但即使这样 ,其仍然遵守内存分段模型,即使用 [段地址+偏址] 方式来访问内存,这是整个 IA-32 的基因 。在保护模式下,段寄存器中存放的不再是段地址,而是段选择子,段选择子映射到段描述符,段描述符中记录了段基址和段界限以及各种权限。 同时,访问内存也不再需要将段地址向左移动四位,而是直接将段地址和偏移地址相加 。这样一来,段地址就无须位于 16 字节对齐的地方,而可以在 16 MB内存的任何角落。然而,80286 的偏移地址仍被限制在 64KB ,对段长度的限制阻碍了 80286 的应用,这是其最大的败笔。
虽然段地址可以不在 16 字节对齐的地方,但对齐有利于提高 CPU 寻址的效率。实际操作时,仍然会对齐。
80386 是划时代的,它的地址线和寄存器扩充到了 32 位,能够访问 4GB 的内存,其偏移地址也能达到 4GB!但如前文所说,分段是 IA-32 的基因,80386 也使用 [段地址+偏址] 方式来访问内存 。在 32 位模式下,处理器要求在加载程序时,先定义该程序所拥有的段(段描述符),然后才允许使用这些段。定义段时,除了基地址外,还附加了段界限、特权级别、类型等属性,当程序访问一个段时,处理器将用固件实施各种检查工作,以防止对内存的违规访问。80386 及其后续的 32 位处理器都兼容实模式,而且在刚加电启动时,这些处理器都位于实模式下,只有经过一番设置后才会进入保护模式 。
需要强调的是,实模式并不是说 32 位的 CPU 退化成了 16 位的 CPU,即使是在实模式下,其也能够使用 32 位 CPU 的资源 。也就是说,32 位 CPU 在 16 位实模式下,其本质仍然是 32 位 CPU,仍具备操作 32 位寄存器和 32 位操作数的能力!另外,不存在 32 位实模式(只能说32位 CPU 下的实模式)。
可以这么说:实模式一般指 8086 工作模式,而 32 位 CPU 的实模式是 8086 工作模式的扩展。
线性地址
IA-32 支持多任务,在多任务环境下,操作系统会给每个任务分配内存空间。在分段模型下,内存的分配是不定长的,程序大时,就分配一大块内存;程序小时,就分配一小块。时间长了,内存空间就会碎片化,就有可能出现一种情况:内存空间是有的,但都是小块,无法分配给某个任务。为了解决这个问题,IA-32 处理器支持分页功能, 分页功能将物理内存空间划分成逻辑上的页。页的大小是固定的,一般为 4KB,通过使用页,可以 简化内存管理 。当页功能开启时,段部件产生的地址就不再是物理地址了,而是线性地址(Linear Address),线性地址还要经页部件转换后,才是物理地址。
段的管理是由处理器的段部件负责进行的,段部件将段地址和偏移地址相加,得到访问内存的地址。不开启分页时,段部件产生的地址就是物理地址 。当页功能开启时,段部件产生的地址就不再是物理地址了,而是线性地址(Linear Address),线性地址还要经页部件转换后,才是物理地址 。
注意,线性地址是用来描述任务的地址空间的一种概念 ,IA-32 处理器上的每个任务都拥有 4GB 的虚拟内存空间 ,这是一段长 4GB 的平坦空间(平坦模型),就像一段平直的线段,因此叫线性地址空间。详细内容将在后续博客中讨论。
平坦模型一定在分页机制下运行,分页机制是内存平坦模型的基础 。
寄存器扩展
- 16 位下的 8 个通用寄存器被扩展到 32 位:eax,ebx,ecx,edx,esp,ebp,edi,esi;就如前文所说,即使在实模式下也可以使用这些 32 位寄存器。注意:1)指令的源操作数和目的操作数必须有相同的长度;2)32 位通用寄存器的高 16 位不可独立使用 ,低 16 位保持对 16 位处理器的兼容。
- 标志寄存器 FLAGS 由 16 位扩展到 32 位 EFLAGS,低 16 位与之前保持一致,见汇编入门一文;高 16 位将在后续文章阐述。
- 段寄存器仍为 16 位,但其中不再存放段基址,而是存放 16 位的段选择子。增添了两个新段寄存器:FS,GS 。
- 每个段寄存器都包含一个 64 位的不可见部分,称为描述符高速缓存寄存器 ,其中装着被整理后的段描述符。引入该寄存器的原因是:1)段描述符在内存中,获取较慢;2)段描述符中,段基址被分成 3 部分,段界限被分成两部分,CPU 无法直接使用,需要整理放入寄存器后才能供CPU使用。
寻址扩展
实模式的内存寻址方式:
32 位 CPU 的寻址方式:
可见,32 位保护模式允许所有通用寄存器作为 基址寄存器 ,除 ESP 之外的所有通用寄存器也可以作为 变址寄存器 ,同时变址寄存器还能够乘上固定的比例因子。注意,实模式下不能使用 SP 作为基址寄存器,而 32 位保护模式下可以使用 ESP 作为基址寄存器 。
模式反转
IA-32 的指令格式:
前缀 | 操作码 | 寻址方式和操作类型 | 立即数 | 偏移量 |
---|
以上格式起源于 16 位处理器,32 位处理器在此基础上扩展了数据的宽度,其他基本保持不变。虽然 32 位采用的是和 16 位相同的指令格式,但寻址方式和寄存器的定义却是另起炉灶的 。考虑如下机器代码:
1 | 8B 50 02 |
其对应的 16 位指令为:
1 | mov dx,[bx+si+0x02] |
其对应的 32 位指令位:
1 | mov edx,[eax+0x02] |
所以说,相同的机器代码在两种模式下可能对应着不同的指令 。而我们在前文中已经反复强调,在32位 CPU 的实模式中仍然可以使用 32 位寄存器,那么问题来了,对于机器代码 8B 50 02
,CPU 怎么知道将它翻译成 16 位指令还是 32 位指令呢?为解决此问题,编写程序时必须使用 bits 关键字指定运行环境:
1 | #有无[]都可 |
如果不指定,则默认为 [bits 16] 。指定环境后,如果某条指令在 A 模式下使用了 B 模式下的寄存器,则该指令的机器码就会加上前缀 0x66
,来临时反转当前模式。比如在 16 位模式下,inc ax
的机器码为:0x40
,而 inc eax
的指令则为 66 40
。因此,前缀 0x66
具有临时反转当前模式的作用 。
值得一提的是,对于 mov 段寄存器,通用寄存器
这样的指令,在 16 位和 32 位模式下的机器码完全相同。如下:
1 | [bits 16] |
这两条指令的机器码都为 8E D8
。为什么这样设计?因为有前缀会使处理器多花一个时钟来处理,而这样的指令很频繁,而且牵涉到内存段的访问,所以也很重要,因此它们被设计得相同。
指令扩展
-
对于 shl,shr 指令,在实模式下使用 cl 寄存器存储移动的位数,在 32 位保护模式下同样如此。
-
mul bx
,实模式下,另一个乘数在 ax 中,积的高位放在 dx 中,低位放在 ax 中。在 32 位下,积直接放在 eax 中。
mul ebx
,另一个乘数放在 eax 中,积的高位放在 edx,低位放在 eax 中。
div bx
,实模式下,被除数高位在 dx 中,低位在 ax 中,商存放在 ax,余数放在 dx 中。32 位下相同。
div ebx
,被除数高位放在 edx,低位放在 eax,商放在 eax,余数放在 edx。 -
对于 push 指令:
-
立即数:
无论是实模式还是保护模式,压入 16 位,则栈指针减 2;压入 32 位,则栈指针减 4 。压入 8 位数据则按当前模式默认大小压入 ,见下。1
2
3
4#实模式下:
push byte 0x1 #sp-=2
push word 0x1 #sp-=2
push dword 0x1 #sp-=41
2
3
4#保护模式下:
push byte 0x1 #esp-=4
push word 0x1 #esp-=2
push dword 0x1 #esp-=4 -
段寄存器:
按当前模式的默认操作数大小压入:1
2
3
4#实模式下:
push es #sp-=2
#保护模式下:
push es #sp-=4 -
通用寄存器和内存:
无论在保护模式还是实模式,如果压入 16 位,则栈指针减 2;如果压入 32 位,则栈指针减 4;
-
保护模式的其他特性如全局描述符表、特权级、分页等内容较多,也是保护模式的精髓,后续将单独记录。
文章参考:《操作系统真相还原》《x86实模式到保护模式》分段、分页和平坦模型