为了后面学习操作系统的需要,从今天开始我要研究x86的汇编了。所以我决定开始总结并连载x86的汇编系列,这是第一篇——初探保护模式。

我假定读者接触过16位的汇编语言,并理解汇编语言的基本概念、熟悉8086处理器采用的“段寄存器 * 16 + 偏移地址”的寻址方法。

我们从80386处理器入手。首先,到了80386时代,CPU有了三种运行模式,即实模式、保护模式和虚拟8086模式。

实模式指的是8086CPU的运行模式,不过这是后来提出的概念,在8086时代只有当时的运行模式,自然也就没有“实模式”这么个提法。如果世界上只有一种性别的人,也就没有男人,女人这种名称了。8086的汇编中,我们对于实模式的各种机制应该算是比较了解了,其大致包括实模式1MB的线性地址空间、内存寻址方法、寄存器、端口读写以及中断处理方法等内容。

不过到了80386时代,引进了一种沿用至今的CPU运行机制——保护模式(Protected Mode)。保护模式有一些新的特色,用来增强多工和系统稳定度,比如内存保护,分页系统,以及硬件支持的虚拟内存等。大部分现今基于 x86的操作系统都在保护模式下运行,包括Linux、FreeBSD、以及 微软 Windows 2.0 和之后版本 [都指32位操作系统] 。

虚拟8086模式用于在保护模式下运行原来实模式下的16位程序,我们不关心。

事实上,现在的64位处理器,拥有三种基本模式(保护模式、实模式、系统管理模式)和一种扩展模式(IA-32e模式(又分兼容模式和64位模式)) 详见这里

我们先来研究保护模式,学校目前基本还处于只讲8086实模式的时代。至于现代CPU的模式……我们有精力再来研究吧。声明下,我不是在吐槽我们的大学教育,真的。

80386首先扩展了8086的处理器(其实中间有个80286,不过这玩意感觉就是个过渡产品,我们不提了),原先的AX,BX,CX,DX,SI,DI,SP,BP从16位扩展(Extend)到了32位,并改名EAX,EBX,ECX,EDX,ESI,EDI,ESP,EBP,E就是Extend的意思。当然,保留了原先的16位寄存器的使用习惯,就像在8086下能用AH和AL访问AX的高低部分一样,不过EAX的低位部分能使用AX直接访问,高位却没有直接的方法,只能通过数据右移16位之后再访问。另外,CS,DS,ES,SS这几个16位段寄存器保留,再增加FS,GS两个段寄存器。另外还有其它很多新增加的寄存器,我们本着实用原则,到时候用到了我们再说。

我们知道,对CPU来讲,系统中的所有储存器中的储存单元都处于一个统一的逻辑储存器中,它的容量受CPU寻址能力的限制。这个逻辑储存器就是我们所说的线性地址空间。8086有20位地址线,拥有1MB的线性地址空间。而80386有32位地址线,拥有4GB的线性地址空间。但是80386依旧保留了8086采用的地址分段的方式,只是增加了一个折中的方案,即只分一个段,段基址0x00000000,段长0xFFFFFFFF(4GB),这样的话整个线性空间可以看作就一个段。这就是所谓的平坦模型(Flat Mode)。

我们以前就知道,线性地址不仅仅是内存地址,还有其它的存储器编址在里面。对于80386,在保护模式下如果开启分页,内存物理地址的访问不一定就是线性地址了,而是需要根据页映射转换到实际的物理地址去。我们暂时还谈不到分页,所以我们目前计算出的线性地址就是物理地址。

我们先来看保护模式下的内存是如何分段管理的。为了便于理解,我们从一个设计者的角度来研究这个问题,顺便试图按我的理解对一些机制的设计原因做一些阐释。

首先是对内存分段中每一个段的描述,内模式对于内存段并没有访问控制,任意的程序可以修改任意地址的变量,而保护模式需要对内存段的性质和允许的操作给出定义,以实现对特定内存段的访问检测和数据保护。考虑到各种属性和需要设置的操作,32位保护模式下对一个内存段的描述需要8个字节,其称之为段描述符(Segment Descriptor)。段描述符分为数据段描述符、指令段描述符和系统段描述符三种,大致相同,个体差异。

我们现在看一张这数据段8个字节的分解图吧,至于为什么是这样,以及每一个细节的含义请读者自行查阅Intel文档,毕竟我写的不是文档…

显然,寄存器不足以存放N多个内存段的描述符集合,所以这些描述符的集合(称之为描述符表)被放置在内存里了。在很多描述符表中,最重要的就是所谓的全局描述符表(Global Descriptor Table,GDT),它为整个软硬件系统服务。

一个问题解决了,但是又引出了的其他问题。问题一、这些描述符表放置在内存哪里?答案是没有固定的说法,可以任由程序员安排在任意合适的位置。那么问题二、既然没有指定固定位置,CPU如何知道全局描述符表在哪?答案是Intel干脆设置了一个48位的专用的全局描述符表寄存器(GDTR)来保存全局描述符表的信息。那这48位怎么分配呢?如图所示,0-15位表示GDT的边界位置(数值为表的长度-1,因为从0计算),16-47位这32位存放的就是GDT的基地址(恰似数组的首地址)。

既然用16位来表示表的长度,那么2的16次方就是65536字节,除以每一个描述符的8字节,那么最多能创建8192个描述符。

貌似说了这么多,我们一直还没提CPU的默认工作方式。80386CPU加电的时候自动进入实模式(实际上不是实模式,刚加电的时刻是一个奇葩的混沌模式,具体说明详见我的另外一篇文章《基于Intel 80×86 CPU的IBM PC及其兼容计算机的启动流程》)。既然CPU加电后就一直工作在实模式下了。那怎么进入保护模式呢?说来也简单,80386CPU内部有5个32位的控制寄存器(Control Register,CR),分别是CR0到CR3,以及CR8。用来表示CPU的一些状态,其中的CR0寄存器的PE位(Protection Enable,保护模式允许位),0号位,就表示了CPU的运行状态,0为实模式,1为保护模式。通过修改这个位就可以立即改变CPU的工作模式。

不过需要注意的是,一旦CR0寄存器的PE位被修改,CPU就立即按照保护模式去寻址了,所以这就要求我们必须在进入保护模式之前就在内存里放置好GDT,然后设置好GDTR寄存器。我们知道实模式下只有1MB的寻址空间,所以GDT就等于被限制在了这里。即便是再不乐意我们也没有办法,只得委屈就全的先安排在这里。不过进入保护模式之后我们就可以在4G的空间里设置并修改原来的GDTR了。

OK,现在有了描述符的数组了,也有了“数组指针”(GDTR)了,怎么表示我们要访问哪个段呢?还记得8086时代的段寄存器吧?不过此时它们改名字了,叫段选择器(段选择子)。此时的CS等寄存器不再保存段基址了,而是保存其指向段的索引信息,CPU会根据这些信息在内存中获取到段信息。

我们上一张图看看整个寻找和合成地址的过程吧:

大致的寻址我们就先说到这里,其实有很多细节我们先做了隐藏处理。那么在接下来的第二篇里面,我们会对从实模式到保护模式时候的细节再次进行阐述,并给出相关的汇编代码实现。

赏杯咖啡鼓励下~