天天看点

保护模式汇编系列之一 - 初探保护模式

为了后面学习操作系统的需要,从今天开始我要研究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会根据这些信息在内存中获取到段信息。

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

保护模式汇编系列之一 - 初探保护模式

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

继续阅读