无标题文档

整理说明

要深入理解32位编程的奥秘,必须理解保护模式.这是李彦昌的保护模式教程,基本是清华出版社80X86汇编语言程序设计的保护模式部分的电子版,但修改了原书的一些错误,也加入了李先生自己的一些体会.很好的资料,保护模式的资料本来就少,中文的的资料更少,谨以此与Asm爱好者共享.

Hume[Afo]

主页:hume.longcity.net

2001年10月22日


保护模式教程

一.保护方式简介

80386有三种工作方式:实模式,保护模式和虚拟8086模式。本文介绍保护方式下的80386及相关的程序设计 内容。实模式下的80386寄存器，寻址方式和指令等基本概念，除特别说明外在保护方式下仍然保持。
尽管实方式下80386的功能要大大超过其先前的处理器(8086/8088,80186,80286)，但只有在保护方式下， 80386才能真正发挥更大的作用。在保护方式下，全部32条地址线有效，可寻址高达4G字节的物理地址空 间;扩充的存储器分段管理机制和可选的存储器分页管理机制，不仅为存储器共享和保护提供了硬件支持 ，而且为实现虚拟存储器提供了硬件支持;支持多任务，能够快速地进行任务切换和保护任务环境;4个特 权级和完善的特权检查机制，既能实现资源共享又能保证代码和数据的安全和保密及任务的隔离;支持虚 拟8086方式，便于执行8086程序。

<一>存储管理机制

为了对存储器中的程序及数据实现保护和共享提供硬件支持，为了对实现虚拟存储器提供硬件支持，在 保护方式下,80386不仅采用扩充的存储器分段管理机制，而且提供可选的存储器分页管理机制。这些存 储管理机制由80386存储管理部件MMU实现。

1.目标

80386有32根地址线，在保护方式下，它们都能发挥作用，所以可寻址的物理地址空间高达4G字节。在 以80386及其以上处理器为CPU的PC兼容机系统中，把地址在1M以下的内存称为常规内存，把地址在1M 以上的内存称为扩展内存。
80386还要对实现虚拟存储器提供支持。虽然与8086可寻址的1M字节物理地址空间相比，80386可寻址的 物理地址空间可谓很大，但实际的微机系统不可能安装如此达的物理内存。所以，为了运行大型程序和 真正实现多任务，必须采用虚拟存储器。虚拟存储器是一种软硬件结合的技术，用于提供比在计算机系 统中实际可以使用的物理主存储器大得多的存储空间。这样，程序员在编写程序时不用考虑计算机中物 理存储器的实际容量。
80386还要对存放在存储器中的代码及数据的共享和保护提供支持。任务甲和任务乙并存，任务甲和任务 乙必须隔离，以免相互影响。但它们又可能要共享部分代码和数据。所以，80386既要支持任务隔离，又 要支持可共享代码和数据的共享，还要支持特权保护。

2.地址空间和地址转换

保护方式下的虚拟存储器由大小可变的存储块构成，这样的存储块称为段。80386采用称为描述符的数据 来描述段的位置、大小和使用情况。虚拟存储器的地址(逻辑地址)由指示描述符的选择子和段内偏移两部 分构成，这样的地址集合称为虚拟地址空间。80386支持的虚拟地址空间可达64T字节。程序员编写程序时 使用的存储地址空间是虚拟地址空间，所以，他们可认为有足够大的存储空间可供使用。
显然，只有在物理存储器中的程序才能运行，只有在物理存储器中的数据才能访问。因此，虚拟地址空间 必须映射到物理地址空间，二维的虚拟地址必须转化成一维的物理地址。由于物理地址空间远小于虚拟地 址空间，所以只有虚拟地址空间中的部分可以映射到物理地址空间。由于物理存储器的大小要远小于物理 地址空间，所以只有上述部分中的部分才能真正映射到物理存储器。
每一个任务有一个虚拟地址空间。为了避免多个并行任务的多个虚拟地址空间直接映射到同一个物理地址 空间，采用线性地址空间隔离虚拟地址空间和物理地址空间。线性地址空间由一维的线性地址构成，线性 地址空间和物理地址空间对等。线性地址32位长，线性地址空间容量为4G字节。
80386分两步实现虚拟地址空间到物理地址空间到物理地址空间的映射，也就是分两步实现虚拟地址到物 理地址的转换，但第二步是可选的。下图是地址映射转换的示意图。
通过描述符表和描述符，分段管理机制实现虚拟地址空间到线性地址空间的映射，实现把二维的虚拟地 址转换为一维的线性地址。这一步总是存在的。
分页管理机制把线性地址空间和物理地址空间分别划分为大小相同的块，这样的块称为页。通过在线性地 址空间的页与物理地址空间的页建立之间建立的映射表，分页管理机制实现线性地址空间到物理地址空间 的映射，实现线性地址到物理地址的转换。分页管理机制是可选的，在不采用分页管理机制时，线性地址 空间就等同于物理地址空间，线性地址就等于物理地址。
分段管理机制所使用的可变大小的块，时分段管理机制比较适宜处理复杂系统的逻辑分段。存储块的大小 可以根据适当的逻辑含义进行定义，而不用考虑固定大小的页所强加的人为限制。每个段可作为独立的单 位处理，以简化段的保护及共享。分页机制使用的固定大小的块最适合于管理物理存储器，无论是管理内 存还是外存都同样有效。分页管理机制能够有效地支持实现虚拟存储器。
段及分页这两种机制是两种不同的转换机制，是整个地址转换函数的不同的转换级。虽然两种机制都利用 存储在主存储器中的转换表，但这些表具有独立的结构。事实上，段表存储在线性地址空间，而页表存储 在物理地址空间。因此，段转换表可由分页机制重新进行定位而不需段机制的参与。段转换机制把虚拟地 址转换为线性地址，并在线性地址中访问段转换机制的表格，而不会觉察分页机制已把线性地址转换为物 理地址。类似地，分页机制对于程序产生的地址所使用的虚拟地址空间一无所知。分页机制只是直接地把 线性地址转换为物理地址，并且在物理地址中访问转换表格，并不知道虚拟地址空间的存在，甚至不知道 段转换机制的存在。

3.虚拟存储器概念

虚拟存储器是一种设计技术，用于提供比在计算机系统中实际可以使用的物理主存储器大得多的存储空 间。使用者会产生一种错觉，好象在程序中可以使用非常大的物理存储空间。使用虚拟存储器的好处是： 一个程序可以很容易地在物理存储器容量大不一样的、配置范围很广的计算机上运行；编程人员使用虚拟 存储器可以写出比任何实际配置的物理存储器都大得多的程序。虚拟存储器由存储管理机制及一个大容量 的快速硬盘存储器支持。在程序运行的任何时刻，只把虚拟地址空间的一小部分映射到主存储器，其余部 分则存储在磁盘上。因为只有存储在主存储器中的部分虚拟存储器可由处理器使用，这种虚拟存储技术将 依赖程序内部访问存储器的局部化特性，在程序执行中只需整个虚拟存储器中的少量存储内容在主存储器 中驻留。而当访问存储器的范围发生变化时，有必要把虚拟存储器的某些部分从磁盘调入主存储器，虚拟 存储器的另外的部分，也能从主存储器传送回磁盘上。
地址转换机制以两种方式支持虚拟存储器。
第一，把实际驻留在主存储器中的那部分虚拟存储器标记为无效，并建立起虚拟存储器驻留部分的虚拟-- 物理映射关系，把驻留部分的相应虚拟存储器地址，转换为对应物理存储器的地址。如果程序访问的虚拟 地址对应于虚拟存储器未驻留的部分，将由于无效映射信息而引起异常。操作系统通过把未驻留部分从磁 盘上读入到主存储器中，来处理这种异常，并根据需要更新地址转换表。在引起异常的原因排除以后，异 常处理程序完成异常事件的处理，并返回原来的程序恢复执行。在后面的文章中将会看到，从异常处理程 序返回后，这时要重新执行一次原来引起异常的指令，而该指令在后一次执行时自然会成功地完成。
第二，地址转换机制通过收集驻留在主存储器中的虚拟存储器部分的使用统计信息来支持虚拟存储器，这 些使用统计信息，在主存储器空间紧缺时，帮助操作系统决定可以将哪些部分传送回磁盘。

<二>保护机制

为了支持多任务，对各任务实施保护是必需的。从80286开始，处理器就具备了保护机制。保护机制能有 效地实现不同任务之间的保护和同一任务内的保护。

1.不同任务之间的保护

保护的一个重要方面是应用程序之间的保护。通过把每个任务放置在不同的虚拟地址空间的方法来实现任 务与任务的隔离，达到应用程序之间保护的目的。虚拟地址到物理地址的映射函数在每个任务中进行定 义，随着任务切换，映射函数也切换。任务A的虚拟地址空间映射到物理地址空间的某个区域，而任务B的 虚拟地址空间映射到物理地址空间的另外区域，彼此独立，互不相干。因此，两个不同的任务，尽管虚拟 存储单元地址相同，但实际的物理存储单元地址可以不同。
每个任务各有一组独立的映射表，即具有不同的地址转换函数。在80386上，每个任务都有自己的段表及 页表。当处理器进行切换并执行新的任务时，这种任务切换的一个重要部分，就是为新任务切换任务的转 换表。为了使操作系统与所有的应用程序相隔离，可以把操作系统存储在一个单一的任务中。然而，我们 即将看到，在一个任务内操作的保护机制，更适合于保护操作系统，使其不被应用程序破坏。这种机制， 使操作系统由所有任务共享，并且可在每一任务中对其进行访问，而且仍然保护了操作系统，使其不被应 用程序破坏。这种保护操作系统的方法，是把操作系统存储在虚拟地址空间的一个公共区域，然后，再使 每一任务按此区域分配一个同样的虚拟地址空间，并进行同样的虚拟--物理地址映射。各个任务公用的这 部分虚拟地址空间，被称为全局地址空间。
仅由一个任务占有的虚拟地址空间部分，即不被任何其它任务共享的虚拟地址部分，称为局部地址空间。 局部地址空间包含的代码和数据，是任务私有的，需要与系统中的其它任务相隔离。
再每个任务中有不同的局部地址空间。因此，两个不同的任务中，对同一虚拟地址的访问，实际上转换为 不同的物理地址。这就使操作系统对每个任务的存储器，可以赋予相同的虚拟地址，仍然保证任务的隔 离。另一方面，对全局地址空间中同一虚拟地址的访问，在所有任务中都转换为同样的物理地址，从而支 持公共的代码及数据的共享，例如对操作系统的共享。

2.同一任务内的保护

在一个任务之内，定义有四种执行特权级别，用于限制对任务中的段进行访问。按照包含在段中的数据的 重要性和代码的可信程度，给段指定特权级别。把最高的特权级别分配给最重要的数据段和最可信任的代 码段。具有最高特权级别的数据，只能由最可信任的代码访问。给不重要的数据段和一般代码段分配较低 的特权级别。具有最低特权级别的数据，可被具有任何特权级别的代码访问。
特权级别用数字0、1、2和3表示，数字0表示最高特权级别，而数字3表示最低特权级别，即数字较大的级 别具有较低的特权。为了避免模糊和混淆，在比较特权级别时，不使用“大于”或“小于”这样的术语， 而使用“里面”或“内层”这样的术语表示较高特权级，级别的数字较小；使用“外面”或“外层”这样 的术语表示较低特权级别，级别的数字较大。0级为最内层的特权级别，3级为最外层的特权级别，按这样 的表示方法，四种特权级的层次关系如下图(图中右边的数字为特权级)所示。
每一特权级都有各自独立的程序堆栈，以避免与共享栈区有关的保护问题。当一个程序从一个特权级切换 到另一个特权级执行时，程序使用的堆栈，从原特权级的栈段改变为新特权级的栈段。对于堆栈段寄存器 SS来说，描述符特权级(DPL)必须等于当前代码段的特权级(CPL)。从一个特权级切换到另一特权级的方法 将在控制转移方法一文中描述。
每个存储器段都与一个特权级别相联系。特权级别限制是指，只有足够级别的程序，才可对相应的段进行 访问。在任何时候，一个任务总是在四个特权级之一下运行，任务在特定时刻的特权级称为当前特权级 (Current Privilege level)，标记为CPL，即当前运行程序的特权级。每当一个程序试图访问一个段时， 就把CPL与要访问的段的特权级进行比较，以决定是否允许这一访问。对给定CPL执行的程序，允许访问同 一级别或外层级别的数据段。如上图所示，CodeK可访问同级的数据段DataK，也可访问外层的DataOS、 DataAP1及DataAP2等。如果试图访问内层级别的数据段则是非法的，并引起异常。如上图所示，CodeOS可 访问同级的DataOS，也可访问外层的DataAP1和DataAP2等，但不能访问内层的DataK。
虽然应用程序都在最外层，但由于各个不同的应用程序存储在不同的虚拟地址空间中，所以各应用程序被 隔离保护。如上图所示，最外层的CodeAP1只能访问DataAP1，不可能访问同级的另一应用程序的DataAP2； 同样，CodeAP2只能访问DataAP2，不可能访问DataAP1。
这实际上是组合保护。应用程序1和操作系统构成任务A，应用程序2和操作系统构成任务B。操作系统被任 务A和任务B共享，在任务A和任务B的两个不同的虚拟地址空间中，操作系统占用虚拟地址空间相同的部分。
特权级的典型用法是，把操作系统的核心部分放在0级，操作系统的其余部分放在1级，而应用程序放在3 级，留下的2级供中间软件使用。对特权级进行这样的安排，使得在0级的操作系统核心有权访问任务中的 所有存储段；而在3级的应用程序只能访问程序本身的存储段，这些存储段也是在3级(注意，Windows 9X 操作系统只使用了0级和3级，以便于移植到精简指令集的计算机上，如RS4000等，这些处理器一般只有两 个特权级，即系统级和用户级)。

参考资料 | 书 名 | 出 版 社 | 作 者
 | 《保护方式下的80386及其编程》 | 清华大学出版社 | 周明德主编
 | 《80X86汇编语言程序设计教程》 | 清华大学出版社 | 扬季文主编


二.分段管理机制

本文介绍保护方式下的段定义以及由段选择子及段内偏移构成的二维虚拟地址如何被转换为 一维线性地址。

<一>段定义和虚拟地址到线性地址的转换

段是实现虚拟地址到线性地址转换机制的基础。在保护方式下，每个段由如下三个参数进行 定义：段基地址(Base Address)、段界限(Limit)和段属性(Attributes)。
段基地址规定线性地址空间中段的开始地址。在80386保护方式下，段基地址长32位。因为 基地址长度与寻址地址的长度相同，所以任何一个段都可以从32位线性地址空间中的任何一 个字节开始，而不象实方式下规定的边界必须被16整除。
段界限规定段的大小。在80386保护模式下，段界限用20位表示，而且段界限可以是以字节 为单位或以4K字节为单位。段属性中有一位对此进行定义，把该位成为粒度位，用符号G标 记。G=0表示段界限以字节位位单位，于是20位的界限可表示的范围是1字节至1M字节，增量 为1字节；G=1表示段界限以4K字节为单位，于是20位的界限可表示的范围是4K字节至4G字节 ，增量为4K字节。当段界限以4K字节为单位时，实际的段界限LIMIT可通过下面的公式从20 位段界限Limit计算出来：
LIMIT=limit*4K+0FFFH=(Limit SHL 12)+0FFFH
所以当粒度为1时，段的界限实际上就扩展成32位。由此可见，在80386保护模式下，段的长 度可大大超过64K字节。
基地址和界限定义了段所映射的线性地址的范围。基地址Base是线性地址对应于段内偏移为 0的虚拟地址，段内偏移为X的虚拟地址对应Base+X的线性地址。段内从偏移0到Limit范围内 的虚拟地址对应于从Base到Base+Limit范围内的线性地址。
下图表示一个段如何从虚拟地址空间定位到线性地址空间。图中BaseA等代表段基地址， LimitA等代表段界限。另外，段C接在段A之后，也即BaseC=BaseA+LimitA。
例如：设段A的基地址等于00012345H，段界限等于5678H，并且段界限以字节为单位(G=0)， 那么段A对应线性地址空间中从00012345H-000179BDH的区域。如果段界限以4K字节为单位 (G=1)，那么段A对应线性地址空间中从00012345H-0568B344H(=00012345H+5678000H+0FFFH) 的区域。
通过增加段界限，可以使段的容量得到扩展。这对于那些要在内存中扩展容量的普通数据段 很有效，但对堆栈段情况就不是这样。因为堆栈底在高地址端，随着压栈操作的进行，堆栈 向低地址方向扩展。为了适应普通数据段和堆栈数据段在两个相反方向上的扩展，数据段的 段属性中安排了一个扩展方向位，标记为ED。ED=0表示向高端扩展，ED=1表示向低端扩展。 一般只有堆栈数据段才使用向低端扩展的属性(堆栈段也可使用向上扩展的段)，这是因为， 向下扩展的段是为以下两个目的而设计的：
第一，堆栈段被定义为独特段，即DS和SS包含不同的选择器。
第二，一个堆栈段是靠将它复制到一个更大的段来扩充自己(而不是靠将现存的页增加到它 的段上)。不打算用这种方法实现堆栈的设计者不需要定义向下扩展的段。
需要注意的是，只有数据段的段属性中才有扩展方向属性位ED，也就是说只有数据段(堆栈 段作为特殊的数据段)才有向上扩展和向下扩展之分，其它段都是自然的向上扩展。
数据段的扩展方向和段界限一起决定了数据段内偏移的有效范围。当段最大为1M字节时，在 向高端扩展的段内，从0到Limit的偏移是合法有效的偏移，而从Limit+1到1M-1的偏移是非 法无效的偏移；在向低端扩展的段内，情形刚好相反，从0到Limit的偏移是非法无效的偏移 ，而从Limit+1到1M-1的偏移是合法有效的偏移，注意边界值Limit对应地址的有效性。段最 大为4G时，情形类似。由此可见，如果一个段是向下扩展的，则所有的偏移必须大于限长， 因为其限长是指下限，其基地址从高地址出开始。反之，若一个段是向上扩展的，则所有偏 移必须小于等于限长，因为其限长是指上限，基地址从低地址处开始。通过使用段环绕，可 以把向下扩展段定义到任何线性地址且可定义为任何大小。
在每次把虚拟地址转换为线性地址的过程中，要对偏移进行检查。如果偏移不在有效的范围 内，那么就引起异常。
段属性规定段的主要特性。例如上面已经提到的段粒度G就是段属性的一部分。在对段进行 各种访问时，将对访问是否合法进行检查，主要依据是段属性。例如：如果向一个只读段进 行写入操作，那么不仅不能写入，而且会引起异常。在下面会详细说明各个段熟属性位的定 义和作用。

<二>存储段描述符

用于表示上述定义段的三个参数的数据结构称为描述符。每个描述符长8个字节。在保护方 式下，每一个段都有一个相应的描述符来描述。按描述符所描述的对象来划分，描述符可分 为如下三类：存储段描述符、系统段描述符、门描述符(控制描述符)。下面先介绍存储段 描述符。

1.存储段描述符的格式

存储段是存放可由程序直接进行访问的代码和数据的段。存储段描述符描述存储段，所以存 储段描述符也被称为代码和数据段描述符。存储段描述符的格式如下表所示。表中上面一排 是对描述符8个字节的使用的说明，最低地址字节(假设地址为m)在最右边，其余字节依次向 左，直到最高字节(地址为m+7)。下一排是对属性域各位的说明。

存储段 描述符 | m+7 | m+6 | m+5 | m+4 | m+3 | m+2 | m+1 | m+0
 | Base(31...24) | Attributes |  | Segment Base(23...0) |  |  | Segment Limite(15...0)

存储段 描述符 属 性 | Byte m+6 |  |  |  |  |  |  |  | Byte m+5
 | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0 | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0
 | G | D | 0 | AVL | Limit(19...16) |  |  |  | P | DPL |  | DT1 | TYPE

从上表可知，长32位的段基地址(段开始地址)被安排在描述符的两个域中，其位0—位23 安排在描述符内的第2—第4字节中，其位24—位31被安排在描述符内的第7字节中。长20 位的段界限也被安排在描述符的两个域中，其位0—位15被安排在描述符内的第0—第1字 节中，其位16—位19被安排在描述符内的第6字节的低4位中。
使用两个域存放段基地址和段界限的原因与80286有关。在80286保护方式下，段基地址只 有24位长，而段界限只有16位长。80286存储段描述符尽管也是8字节长，但实际只使用低 6字节，高2字节必须置为0。80386存储段描述符这样的安排，可使得80286的存储段描述 符的格式在80386下继续有效。
80386描述符中的段属性也被安排在两个域中。下面对其定义及意义作说明。
(1)P位称为存在(Present)位。P=1表示描述符对地址转换是有效的，或者说该描述符所描 述的段存在，即在内存中；P=0表示描述符对地址转换无效，即该段不存在。使用该描述 符进行内存访问时会引起异常。
(2)DPL表示描述符特权级(Descriptor Privilege level)，共2位。它规定了所描述段的 特权级，用于特权检查，以决定对该段能否访问。
(3)DT位说明描述符的类型。对于存储段描述符而言，DT=1，以区别与系统段描述符和门 描述符(DT=0)。
(4)TYPE说明存储段描述符所描述的存储段的具体属性。
其中的位0指示描述符是否被访问过(Accessed)，用符号A标记。A=0表示尚未被访问，A=1 表示段已被访问。当把描述符的相应选择子装入到段寄存器时，80386把该位置为1，表明 描述符已被访问。操作系统可测试访问位，已确定描述符是否被访问过。
其中的位3指示所描述的段是代码段还是数据段，用符号E标记。E=0表示段为数据段，相 应的描述符也就是数据段(包括堆栈段)描述符。数据段是不可执行的，但总是可读的。 E=1表示段是可执行段，即代码段，相应的描述符就是代码段描述符。代码段总是不可写 的，若需要对代码段进行写入操作，则必须使用别名技术，即用一个可写的数据段描述符 来描述该代码段，然后对此数据段进行写入。
在数据段描述符中(E=0的情况)，TYPE中的位1指示所描述的数据段是否可写，用W标记。 W=0表示对应的数据段不可写。反之，W=1表示数据段是可写的。注意，数据段总是可读 的。TYPE中的位2是ED位，指示所描述的数据段的扩展方向。ED=0表示数据段向高端扩展 ，也即段内偏移必须小于等于段界限。ED=1表示数据段向低扩展，段内偏移必须大于段 界限。
在代码段描述符中(E=1的情况)，TYPE中的位1指示所描述的代码段是否可读，用符号R标 记。R=0表示对应的代码段不可读，只能执行。R=1表示对应的代码段可读可执行。注意 代码段总是不可写的，若需要对代码段进行写入操作，则必须使用别名技术。在代码段 中，TYPE中的位2指示所描述的代码段是否是一致代码段，用C标记。C=0表示对应的代码 段不是一致代码段(普通代码段)，C=1表示对应的代码段是一致代码段。关于一致代码段 的说明，后面的文章将会详细介绍。
存储段描述符中的TYPE字段所说明的属性可归纳为下表：

数据段 类 型 | 类型值 | 说 明
 | 0 | 只读
 | 1 | 只读、已访问
 | 2 | 读/写
 | 3 | 读/写、已访问
 | 4 | 只读、向下扩展
 | 5 | 只读、向下扩展、已访问
 | 6 | 读/写、向下扩展
 | 7 | 读/写、向下扩展、已访问

代码段 类 型 | 类型值 | 说 明
 | 8 | 只执行
 | 9 | 只执行、已访问
 | A | 执行/读
 | B | 执行/读、已访问
 | C | 只执行、一致码段
 | D | 只执行、一致码段、已访问
 | E | 执行/读、一致码段
 | F | 执行/读、一致码段、已访问

(5)G为就是段界限粒度(Granularity)位。G=0表示界限粒度为字节；G=1表示界限粒度为4K 字节。注意，界限粒度只对段界限有效，对段基地址无效，段基地址总是以字节为单位。
(6)D位是一个很特殊的位，在描述可执行段、向下扩展数据段或由SS寄存器寻址的段(通常 是堆栈段)的三种描述符中的意义各不相同。
在描述可执行段的描述符中，D位决定了指令使用的地址及操作数所默认的大小。D=1表示 默认情况下指令使用32位地址及32位或8位操作数，这样的代码段也称为32位代码段；D=0 表示默认情况下，使用16位地址及16位或8位操作数，这样的代码段也称为16位代码段，它 与80286兼容。可以使用地址大小前缀和操作数大小前缀分别改变默认的地址或操作数的大 小。
在向下扩展数据段的描述符中，D位决定段的上部边界。D=1表示段的上部界限为4G；D=0表 示段的上部界限为64K，这是为了与80286兼容。
在描述由SS寄存器寻址的段描述符中，D位决定隐式的堆栈访问指令(如PUSH和POP指令)使 用何种堆栈指针寄存器。D=1表示使用32位堆栈指针寄存器ESP；D=0表示使用16位堆栈指针 寄存器SP，这与80286兼容。
(7)AVL位是软件可利用位。80386对该位的使用未左规定，Intel公司也保证今后开发生 产的处理器只要与80386兼容，就不会对该位的使用做任何定义或规定。
此外，描述符内第6字节中的位5必须置为0，可以理解成是为以后的处理器保留的。

2.存储段描述符的结构类型表示

根据存储段描述符的结构，可定义如下的汇编语言描述符结构类型：
    DESC       STRUC
    LIMITL     DW      0 ;段界限低16位
    BASEL      DW      0 ;基地址低16位
    BASEM      DB      0 ;基地址中间8位
    ATTRIB     DB      0 ;段属性
    LIMITH     DB      0 ;段界限的高4位(包括段属性的高4位)
    BASEH      DB      0 ;基地址的高8位
    DESC       ENDS
利用结构类型DESC能方便地在程序中说明存储段描述符。例如：下面的描述符DATAS描述一 个可读写的有效(存在的)数据段，基地址是100000H，以字节为单位的界限是0FFFFH，描述 符特权级DPL=3。
    DATAS DESC <0FFFFH,,10H,0F2H,,>
再如：下述描述符CODEA描述一个只可执行的有效的32位代码段，基地址是12345678H，以 4K字节位单位的段界限值是10H(以字节位单位的界限是10FFFH)，描述符特权级DPL=0。
    CODEA DESC <10H,5678H,34H,98H,0C0H,12H>

<三>全局和局部描述符表

一个任务会涉及多个段，每个任务需要一个描述符来描述，为了便于组织管理，80386把 描述符组织成线性表。由描述符组成的线性表称为描述符表。在80386中有三种类型的描 述符表：全局描述符表GDT(Global Descriptor Table)、局部描述符表LDT(Local Descriptor Table)和中断描述符表IDT(Interrupt Descriptor Table)。在整个系统中， 全局描述符表GDT和中断描述符表IDT只有一张，局部描述符表可以有若干张，每个任务 可以有一张。
例如，下列描述符表有6个描述符构成：
    DESCTAB LABEL BYTE
    DESC1   DESC  <1234H,5678H,34H,92H,,>
    DESC1   DESC  <1234H,5678H,34H,93H,,>
    DESC1   DESC  <5678H,1234H,56H,98H,,>
    DESC1   DESC  <5678H,1234H,56H,99H,,>
    DESC1   DESC  <0FFFFH,,10H,16H,,>
    DESC1   DESC  <0FFFFH,,10H,90H,,>
每个描述符表本身形成一个特殊的数据段。这样的特殊数据段最多可包含有8K(8192)个描 述符.
关于中断描述符表IDT在以后的文章中介绍。
每个任务的局部描述符表LDT含有该任务自己的代码段、数据段和堆栈段的描述符，也包含 该任务所使用的一些门描述符，如任务门和调用门描述符等。随着任务的切换，系统当前 的局部描述符表LDT也随之切换。
全局描述符表GDT含有每一个任务都可能或可以访问的段的描述符，通常包含描述操作系统 所使用的代码段、数据段和堆栈段的描述符，也包含多种特殊数据段描述符，如各个用于 描述任务LDT的特殊数据段等。在任务切换时，并不切换GDT。
通过LDT可以使各个任务私有的各个段与其它任务相隔离，从而达到受保护的目的。通 过GDT可以使各任务都需要使用的段能够被共享。下图给出了任务A和任务B所涉及的有关段 既隔离受保护，又合用共享的情况。通过任务A的局部描述符表LDTA和任务B的局部描述符 表LDTB，把任务A所私有的代码段CodeA及数据段DataA与任务B所私有的代码段CodeB和数据 段DataB及DataB2隔离，但任务A和任务B通过全局描述符表GDT共享代码段CodeK及CodeOS和 数据段DataK及DataOS。
一个任务可使用的整个虚拟地址空间分为相等的两半，一半空间的描述符在全局描述符表 中，另一半空间的描述符在局部描述符表中。由于全局和局部描述符表都可以包含多 达8192个描述符，而每个描述符所描述的段的最大值可达4G字节，因此最大的虚拟地址空 间可为：
4GB*8192*2=64MMB=64TB

<四>段选择子

在实模式下，逻辑地址空间中存储单元的地址由段值和段内偏移两部分组成。在保护方式 下，虚拟地址空间(相当于逻辑地址空间)中存储单元的地址由段选择子和段内偏移两部分 组成。与实模式相比，段选择子代替了段值。
段选择子长16位，其格式如下表所示。从表中可见，段选择子的高13位是描述符索 引(Index)。所谓描述符索引是指描述符在描述符表中的序号。段选择子的第2位是引用描 述符表指示位，标记为TI(Table Indicator)，TI=0指示从全局描述符表GDT中读取描述符 ；TI=1指示从局部描述符表LDT中读取描述符。

选择子 结 构 | BIT15 | BIT14 | BIT13 | BIT12 | BIT11 | BIT10 | BIT9 | BIT8 | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0
 | 描述符索引 |  |  |  |  |  |  |  |  |  |  |  |  | TI | RPL

选择子确定描述符，描述符确定段基地址，段基地址与偏移之和就是线性地址。所以， 虚拟地址空间中的由选择子和偏移两部分构成的二维虚拟地址，就是这样确定了线性地 址空间中的一维线性地址。
选择子的最低两位是请求特权级RPL(Requested Privilege Level)，用于特权检查。 RPL字段的用法如下：
每当程序试图访问一个段时，要把当前特权级与所访问段的特权级进行比较，以确定是 否允许程序对该段的访问。使用选择子的RPL字段，将改变特权级的测试规则。在这种 情况下，与所访问段的特权级比较的特权级不是CPL，而是CPU与RPL中更外层的特权级。 CPL存放在CS寄存器的RPL字段内，每当一个代码段选择子装入CS寄存器中时，处理器自 动地把CPL存放到CS的RPL字段。
由于选择子中的描述符索引字段用13位表示，所以可区分8192个描述符。这也就是描述 符表最多包含8192个描述符的原因。由于每个描述符长8字节，根据上表所示选择子的 格式，屏蔽选择子低3位后所得的值就是选择子所指定的描述符在描述符表中的偏移， 这可认为是安排选择子高13位作为描述符索引的原因。
有一个特殊的选择子称为空(Null)选择子，它的Index=0，TI=0，而RPL字段可以为任意 值。空选择子有特定的用途，当用空选择子进行存储访问时会引起异常。空选择子是特 别定义的，它不对应于全局描述符表GDT中的第0个描述符，因此处理器中的第0个描述 符总不被处理器访问，一般把它置成全0。但当TI=1时，Index为0的选择子不是空选择 子，它指定了当前任务局部描述符表LDT中的第0个描述符。

<五>段描述符高速缓冲寄存器

在实模式下，段寄存器含有段值，为访问存储器形成物理地址时，处理器引用相应的某 个段寄存器并将其值乘以16，形成20位的段基地址。在保护模式下，段寄存器含有段选 择子，如上所述，为了访问存储器形成线性地址时，处理器要使用选择子所指定的描述 符中的基地址等信息。为了避免在每次存储器访问时，都要访问描述符表而获得对应的 段描述符，从80286开始每个段寄存器都配有一个高速缓冲寄存器，称之为段描述符高 速缓冲寄存器或描述符投影寄存器，对程序员而言它是不可见的。每当把一个选择子装 入到某个段寄存器时，处理器自动从描述符表中取出相应的描述符，把描述符中的信息 保存到对应的高速缓冲寄存器中。此后对该段访问时，处理器都使用对应高速缓冲寄存 器中的描述符信息，而不用再从描述符表中取描述符。
各段描述符高速缓冲寄存器之内容如下表所示。其中，32位段基地址直接取自描述符， 32位的段界限取自描述符中20位的段界限，并根据描述符属性中的粒度位转换成以字节 为单位。其它十个特性根据描述符中的属性而定，“Y”表示“是”，“N”表示“否” ，“R”表示必须可读，“W”表示必须可写，“P”表示必须存在，“D”表示根据描述 符中属性而定。

段描 述符 高速 缓冲 寄存 器的 内容 | 段寄存器 | 段基地址 | 段界限 | 段属性
 |  |  |  | 存在性 | 特权级 | 已存取 | 粒度 | 扩展方向 | 可读性 | 可写性 | 可执行 | 堆栈大小 | 一致特权
 | CS | 32位基地址 | 32位段界限 | P | D | D | D | D | D | N | Y | - | D
 | SS | 32位基地址 | 32位段界限 | P | D | D | D | D | R | W | N | D | -
 | DS | 32位基地址 | 32位段界限 | P | D | D | D | D | D | D | N | - | -
 | ES | 32位基地址 | 32位段界限 | P | D | D | D | D | D | D | N | - | -
 | FS | 32位基地址 | 32位段界限 | P | D | D | D | D | D | D | N | - | -
 | GS | 32位基地址 | 32位段界限 | P | D | D | D | D | D | D | N | - | -

段描述符高速缓冲寄存器再处理器内，所以可对其进行快速访问。绝大多数情况下， 对存储器的访问是在对应选择子装入到段寄存器之后进行的，所以，使用段描述符高 速缓冲寄存器可以得到很好的执行性能。
段描述符高速缓冲寄存器之内保存的描述符信息将一直保存到重新把选择子装载到段 寄存器时再更新。程序员尽管不可见段描述符高速缓冲寄存器，但必须注意到它的存 在和它的上述更新时机。例如，在改变了描述符表中的某个当前段的描述符后，也要 更新对应的段描述符高速缓冲寄存器的内容，即使段选择子未作改变，这可通过重新 装载段寄存器实现。


三.控制寄存器和系统地址寄存器

80386控制寄存器和系统地址寄存器如下表所示。它们用于控制工作方式，控制分段管理机制及 分页管理机制的实施。

控 制 寄存器 | CRx | BIT31 | BIT30—BIT12 | BIT11—BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0
 | CR0 | PG | 0000000000000000 |  | ET | TS | EM | MP | PE
 | CR1 | 保留
 | CR2 | 页故障线性地址
 | CR3 | 页目录表物理页码 |  | 000000000000

 | BIT47—BIT16 | BIT15—BIT0
全局描述符表寄存器GDTR | 基地址 | 界限
中断描述符表寄存器IDTR | 基地址 | 界限

 | BIT15—BIT0
局部描述符表寄存器LDTR | 选择子
任务状态段寄存器TR | 选择子

BIT31—BIT0 | BIT31—BIT0 | BIT11—BIT0
基地址 | 界限 | 属性
基地址 | 界限 | 属性

<一>控制寄存器

从上表可见，80386有四个32位的控制寄存器，分别命名位CR0、CR1、CR2和CR3。但CR1被保留， 供今后开发的处理器使用，在80386中不能使用CR1，否则会引起无效指令操作异常。CR0包括指 示处理器工作方式的控制位，包含启用和禁止分页管理机制的控制位，包含控制浮点协处理器 操作的控制位。CR2及CR3由分页管理机制使用。CR0中的位5—位30及CR3中的位0至位11是保留 位，这些位不能是随意值，必须为0。
控制寄存器CR0的低16位等同于80286的机器状态字MSW。

1.保护控制位

控制寄存器CR0中的位0用PE标记，位31用PG标记，这两个位控制分段和分页管理机制的操作， 所以把它们称为保护控制位。PE控制分段管理机制。PE=0，处理器运行于实模式；PE=1，处理 器运行于保护方式。PG控制分页管理机制。PG=0，禁用分页管理机制，此时分段管理机制产生 的线性地址直接作为物理地址使用；PG=1，启用分页管理机制，此时线性地址经分页管理机制 转换位物理地址。关于分页管理机制的具体介绍在后面的文章中进行。
下表列出了通过使用PE和PG位选择的处理器工作方式。由于只有在保护方式下才可启用分页机 制，所以尽管两个位分别为0和1共可以有四种组合，但只有三种组合方式有效。PE=0且PG=1是 无效组合，因此，用PG为1且PE为0的值装入CR0寄存器将引起通用保护异常。
需要注意的是，PG位的改变将使系统启用或禁用分页机制，因而只有当所执行的程序的 代码和至少有一部分数据在线性地址空间和物理地址空间具有相同的地址的情况下，才 能改变PG位。

PG和PE 位与处 理器工 作模式 | PG | PE | 处理器工作方式
 | 0 | 0 | 实模式
 | 0 | 1 | 保护模式，禁用分页机制
 | 1 | 0 | 非法组合
 | 1 | 1 | 保护方式，启用分页机制

2.协处理器控制位

控制寄存器CR0中的位1—位4分别标记为MP(算术存在位)、EM(模拟位)、TS(任务切换位) 和ET(扩展类型位)，它们控制浮点协处理器的操作。
当处理器复位时，ET位被初始化，以指示系统中数字协处理器的类型。如果系统中存在 80387协处理器，那么ET位置1；如果系统中存在80287协处理器或者不存在协处理器，那 么ET位清0。
EM位控制浮点指令的执行是用软件模拟，还是由硬件执行。EM=0时，硬件控制浮点指令 传送到协处理器；EM=1时，浮点指令由软件模拟。
TS位用于加快任务的切换，通过在必要时才进行协处理器切换的方法实现这一目的。每 当进行任务切换时，处理器把TS置1。TS=1时，浮点指令将产生设备不可用(DNA)异常。 MP位控制WAIT指令在TS=1时，是否产生DNA异常。MP=1和TS=1时，WAIT产生异常；MP=0时 ，WAIT指令忽略TS条件，不产生异常。

3.CR2和CR3

控制寄存器CR2和CR3由分页管理机制使用。
CR2用于发生页异常时报告出错信息。当发生页异常时，处理器把引起页异常的线性地址 保存在CR2中。操作系统中的页异常处理程序可以检查CR2的内容，从而查出线性地址空 间中的哪一页引起本次异常。
CR3用于保存页目录表的其始物理地址。由于目录是页对齐的，所以仅高20位有效，低12 位保留未用。向CR3中装入一个新值时，低12位必须为0；但从CR3中取值时，低12位被忽 略。每当用MOV指令重置CR3的值时，会导致分页机制高速缓冲区的内容无效，用此方法， 可以在启用分页机制之前，即把PG位置1之前，预先刷新分页机制的高速缓存。CR3寄存 器即使在CR0寄存器的PG位或PE位为0时也可装入，如在实模式下也可设置CR3，以便进行 分页机制的初始化。在任务切换时，CR3要被改变，但是如果新任务中CR3的值与原任务 中CR3的值相同，那么处理器不刷新分页高速缓存，以便当任务共享也表时有较快的执行 速度。

<二>系统地址寄存器

全局描述符表GDT、局部描述符表LDT和中断描述符表IDT等都是保护方式下非常重要的特 殊段，它们包含有为段机制所用的重要表格。为了方便快速地定位这些段，处理器采用 一些特殊的寄存器保存这些段的基地址和段界限。我们把这些特殊的寄存器称为系统地 址寄存器。

1.全局描述符表寄存器GDTR

如本文开始处的表格所示，GDTR长48位，其中高32位为基地址，低16位为界限。由于GDT 不能有GDT本身之内的描述符进行描述定义，所以处理器采用GDTR为GDT这一特殊的系统 段提供一个伪描述符。GDTR给定了GDT，如下图所示。
GDTR中的段界限以字节为单位。由于段选择子中只有13位作为描述符索引，而每个描述 符长8个字节，所以用16位的界限足够。通常，对于含有N个描述符的描述符表的段界限 设为8*N-1。
利用结构类型可定义伪描述符如下：
    PDESC STRUC
    LIMIT DW 0
    BASE  DD 0
    PDESC ENDS

2.局部描述符表寄存器LDTR

局部描述符表寄存器LDTR规定当前任务使用的局部描述符表LDT。如本文开始处的表格 所示，LDTR类似于段寄存器，由程序员可见的16位的寄存器和程序员不可见的高速缓冲 寄存器组成。实际上，每个任务的局部描述符表LDT作为系统的一个特殊段，由一个描 述符描述。而用于描述符LDT的描述符存放在GDT中。在初始化或任务切换过程中，把描 述符对应任务LDT的描述符的选择子装入LDTR，处理器根据装入LDTR可见部分的选择子 ，从GDT中取出对应的描述符，并把LDT的基地址、界限和属性等信息保存到LDTR的不可 见的高速缓冲寄存器中。随后对LDT的访问，就可根据保存在高速缓冲寄存器中的有关 信息进行合法性检查。
LDTR寄存器包含当前任务的LDT的选择子。所以，装入到LDTR的选择子必须确定一个位 于GDT中的类型为LDT的系统段描述符，也即选择子中的TI位必须是0，而且描述符中的 类型字段所表示的类型必须为LDT。
可以用一个空选择子装入LDTR，这表示当前任务没有LDT。在这种情况下，所有装入到 段寄存器的选择子都必须指示GDT中的描述符，也即当前任务涉及的段均由GDT中的描 述符来描述。如果再把一个TI位为1的选择子装入到段寄存器，将引起异常。

3.中断描述符表寄存器IDTR

中断描述符表寄存器IDTR指向中断描述符表IDT。如本文开始处的表格所示，IDTR长48 位，其中32位的基地址规定IDT的基地址，16位的界限规定IDT的段界限。由于80386只 支持256个中断/异常，所以IDT表最大长度是2K，以字节位单位的段界限为7FFH。IDTR 指示IDT的方式与GDTR指示GDT的方式相同。

4.任务状态段寄存器TR

任务状态段寄存器TR包含指示描述当前任务的任务状态段的描述符选择子，从而规定 了当前任务的状态段。任务状态段的格式在后面的文章中介绍。如本文开始处的表格 所示，TR也有程序员可见和不可见两部分。当把任务状态段的选择子装入到TR可见部 分时，处理器自动把选择子所索引的描述符中的段基地址等信息保存到不可见的高速 缓冲寄存器中。在此之后，对当前任务状态段的访问可快速方便地进行。装入到TR的 选择子不能为空，必须索引位于GDT中的描述符，且描述符的类型必须是TSS。


四.实模式与保护模式切换实例

本文介绍两个实现实模式与保护模式切换的实例，通过他们说明如何实现实模式与保护模式的切换， 也说明保护模式下的80386及其编程。 这里下载本文源代码。

<一>演示实模式和保护模式切换的实例(实例一)

实例一的逻辑功能是，以十六进制数的形式显示从内存地址110000H开始的256个字节的值。本实例指 定该内存区域的目的仅仅是想说明切换到保护模式的必要性，因为在实模式下不能访问该指定内存区 域，只有在保护模式下才能访问到该指定区域。
本实例的具体实现步骤是：(1)作切换到保护方式的准备；(2)切换到保护方式；(3)把指定内存区域 的内容传送到位于常规内存的缓冲区中；(4)切换回实模式；(5)显示缓冲区内容。

1.包含文件

386保护模式汇编语言程序用到的包含文件如下所示，该包含文件在后面的程序中还要用到。
;名称:386SCD.INC
;功能:符号常量等的定义
;----------------------------------------------------------------------------
;IFNDEF         __386SCD_INC
;__386SCD_INC   EQU     1
;----------------------------------------------------------------------------
.386P
;----------------------------------------------------------------------------
;打开A20地址线
;----------------------------------------------------------------------------
EnableA20       MACRO
                push    ax
                in      al,92h
                or      al,00000010b
                out     92h,al
                pop     ax
                ENDM
;----------------------------------------------------------------------------
;关闭A20地址线
;----------------------------------------------------------------------------
DisableA20      MACRO
                push    ax
                in      al,92h
                and     al,11111101b
                out     92h,al
                pop     ax
                ENDM
;----------------------------------------------------------------------------
;16位偏移的段间直接转移指令的宏定义(在16位代码段中使用)
;----------------------------------------------------------------------------
JUMP16          MACRO   Selector,Offset
                DB      0eah     ;操作码
                DW      Offset   ;16位偏移量
                DW      Selector ;段值或段选择子
                ENDM
;----------------------------------------------------------------------------
;32位偏移的段间直接转移指令的宏定义(在32位代码段中使用)
;----------------------------------------------------------------------------
COMMENT <JUMP32>
JUMP32          MACRO   Selector,Offset
                DB      0eah     ;操作码
                DD      OFFSET
                DW      Selector ;段值或段选择子
                ENDM
<JUMP32>
;-------------------------------------------------
JUMP32          MACRO   Selector,Offset
                DB      0eah     ;操作码
                DW      OFFSET
                DW      0
                DW      Selector ;段值或段选择子
                ENDM
;----------------------------------------------------------------------------
;16位偏移的段间调用指令的宏定义(在16位代码段中使用)
;----------------------------------------------------------------------------
CALL16          MACRO   Selector,Offset
                DB      9ah      ;操作码
                DW      Offset   ;16位偏移量
                DW      Selector ;段值或段选择子
                ENDM
;----------------------------------------------------------------------------
;32位偏移的段间调用指令的宏定义(在32位代码段中使用)
;----------------------------------------------------------------------------
COMMENT <CALL32>
CALL32          MACRO   Selector,Offset
                DB      9ah      ;操作码
                DD      Offset
                DW      Selector ;段值或段选择子
                ENDM
<CALL32>
;-------------------------------------------------
CALL32          MACRO   Selector,Offset
                DB      9ah      ;操作码
                DW      Offset
                DW      0
                DW      Selector ;段值或段选择子
                ENDM
;----------------------------------------------------------------------------
;存储段描述符结构类型定义
;----------------------------------------------------------------------------
Desc            STRUC
LimitL          DW      0 ;段界限(BIT0-15)
BaseL           DW      0 ;段基地址(BIT0-15)
BaseM           DB      0 ;段基地址(BIT16-23)
Attributes      DB      0 ;段属性
LimitH          DB      0 ;段界限(BIT16-19)(含段属性的高4位)
BaseH           DB      0 ;段基地址(BIT24-31)
Desc            ENDS
;----------------------------------------------------------------------------
;门描述符结构类型定义
;----------------------------------------------------------------------------
Gate            STRUC
OffsetL         DW      0 ;32位偏移的低16位
Selector        DW      0 ;选择子
DCount          DB      0 ;双字计数
GType           DB      0 ;类型
OffsetH         DW      0 ;32位偏移的高16位
Gate            ENDS
;----------------------------------------------------------------------------
;伪描述符结构类型定义(用于装入全局或中断描述符表寄存器)
;----------------------------------------------------------------------------
PDesc           STRUC
Limit           DW      0 ;16位界限
Base            DD      0 ;32位基地址
PDesc           ENDS
;----------------------------------------------------------------------------
;任务状态段结构类型定义
;----------------------------------------------------------------------------
TSS             STRUC
TRLink          DW      0      ;链接字段
                DW      0      ;不使用,置为0
TRESP0          DD      0      ;0级堆栈指针
TRSS0           DW      0      ;0级堆栈段寄存器
                DW      0      ;不使用,置为0
TRESP1          DD      0      ;1级堆栈指针
TRSS1           DW      0      ;1级堆栈段寄存器
                DW      0      ;不使用,置为0
TRESP2          DD      0      ;2级堆栈指针
TRSS2           DW      0      ;2级堆栈段寄存器
                DW      0      ;不使用,置为0
TRCR3           DD      0      ;CR3
TREIP           DD      0      ;EIP
TREFlag         DD      0      ;EFLAGS
TREAX           DD      0      ;EAX
TRECX           DD      0      ;ECX
TREDX           DD      0      ;EDX
TREBX           DD      0      ;EBX
TRESP           DD      0      ;ESP
TREBP           DD      0      ;EBP
TRESI           DD      0      ;ESI
TREDI           DD      0      ;EDI
TRES            DW      0      ;ES
                DW      0      ;不使用,置为0
TRCS            DW      0      ;CS
                DW      0      ;不使用,置为0
TRSS            DW      0      ;SS
                DW      0      ;不使用,置为0
TRDS            DW      0      ;DS
                DW      0      ;不使用,置为0
TRFS            DW      0      ;FS
                DW      0      ;不使用,置为0
TRGS            DW      0      ;GS
                DW      0      ;不使用,置为0
TRLDTR          DW      0      ;LDTR
                DW      0      ;不使用,置为0
TRTrip          DW      0      ;调试陷阱标志(只用位0)
TRIOMap         DW      $+2    ;指向I/O许可位图区的段内偏移
TSS             ENDS
;----------------------------------------------------------------------------
;存储段描述符类型值说明
;----------------------------------------------------------------------------
ATDR            EQU     90h ;存在的只读数据段类型值
ATDW            EQU     92h ;存在的可读写数据段属性值
ATDWA           EQU     93h ;存在的已访问可读写数据段类型值
ATCE            EQU     98h ;存在的只执行代码段属性值
ATCER           EQU     9ah ;存在的可执行可读代码段属性值
ATCCO           EQU     9ch ;存在的只执行一致代码段属性值
ATCCOR          EQU     9eh ;存在的可执行可读一致代码段属性值
;----------------------------------------------------------------------------
;系统段描述符类型值说明
;----------------------------------------------------------------------------
ATLDT           EQU     82h ;局部描述符表段类型值
ATTaskGate      EQU     85h ;任务门类型值
AT386TSS        EQU     89h ;可用386任务状态段类型值
AT386CGate      EQU     8ch ;386调用门类型值
AT386IGate      EQU     8eh ;386中断门类型值
AT386TGate      EQU     8fh ;386陷阱门类型值
;----------------------------------------------------------------------------
;DPL值说明
;----------------------------------------------------------------------------
DPL0            EQU     00h ;DPL=0
DPL1            EQU     20h ;DPL=1
DPL2            EQU     40h ;DPL=2
DPL3            EQU     60h ;DPL=3
;----------------------------------------------------------------------------
;RPL值说明
;----------------------------------------------------------------------------
RPL0            EQU     00h ;RPL=0
RPL1            EQU     01h ;RPL=1
RPL2            EQU     02h ;RPL=2
RPL3            EQU     03h ;RPL=3
;----------------------------------------------------------------------------
;IOPL值说明
;----------------------------------------------------------------------------
IOPL0           EQU     0000h ;IOPL=0
IOPL1           EQU     1000h ;IOPL=1
IOPL2           EQU     2000h ;IOPL=2
IOPL3           EQU     3000h ;IOPL=3
;----------------------------------------------------------------------------
;其它常量值说明
;----------------------------------------------------------------------------
D32             EQU     40h       ;32位代码段标志
GL              EQU     80h       ;段界限以4K为单位标志
TIL             EQU     04h       ;TI=1(局部描述符表标志)
VMFL            EQU     00020000h ;VMF=1
VMFLW           EQU     0002h
IFL             EQU     00000200h ;IF=1
RFL             EQU     00010000h ;RF=1(重启动标志,为1表示忽略调试故障)
RFLW            EQU     0001h
NTL             EQU     00004000h ;NT=1
;----------------------------------------------------------------------------
;分页机制使用的常量说明
;----------------------------------------------------------------------------
PL              EQU     1     ;页存在属性位
RWR             EQU     0     ;R/W属性位值,读/执行
RWW             EQU     2     ;R/W属性位值,读/写/执行
USS             EQU     0     ;U/S属性位值,系统级
USU             EQU     4     ;U/S属性位值,用户级
;----------------------------------------------------------------------------
;ENDIF

2.实例源程序

实例一的源程序如下所示：
;名称:ASM1.ASM
;功能:演示实方式和保护方式切换(切换到16位代码段)
;----------------------------------------------------------------------------
INCLUDE         386SCD.INC
;----------------------------------------------------------------------------
;字符显示宏指令的定义
;----------------------------------------------------------------------------
EchoCh          MACRO   ascii
                mov     ah,2
                mov     dl,ascii
                int     21h
                ENDM
;----------------------------------------------------------------------------
DSEG            SEGMENT USE16                 ;16位数据段
;----------------------------------------------------------------------------
GDT             LABEL   BYTE                  ;全局描述符表
DUMMY           Desc    <>                    ;空描述符
Code            Desc    <0ffffh,,,ATCE,,>     ;代码段描述符
DataS           Desc    <0ffffh,0,11h,ATDW,,> ;源数据段描述符
DataD           Desc    <0ffffh,,,ATDW,,>     ;目标数据段描述符
;----------------------------------------------------------------------------
GDTLen          =       $-GDT                 ;全局描述符表长度
VGDTR           PDesc   <GDTLen-1,>           ;伪描述符
;----------------------------------------------------------------------------
Code_Sel        =       Code-GDT              ;代码段选择子
DataS_Sel       =       Datas-GDT             ;源数据段选择子
DataD_Sel       =       DataD-GDT             ;目标数据段选择子
;----------------------------------------------------------------------------
BufLen          =       256                   ;缓冲区字节长度
Buffer          DB      BufLen DUP(0)         ;缓冲区
;----------------------------------------------------------------------------
DSEG            ENDS                          ;数据段定义结束
;----------------------------------------------------------------------------
CSEG            SEGMENT USE16                 ;16位代码段
                ASSUME  CS:CSEG,DS:DSEG
;----------------------------------------------------------------------------
Start           PROC
                mov     ax,DSEG
                mov     ds,ax
                ;准备要加载到GDTR的伪描述符
                mov     bx,16
                mul     bx
                add     ax,OFFSET GDT          ;计算并设置基地址
                adc     dx,0                   ;界限已在定义时设置好
                mov     WORD PTR VGDTR.Base,ax
                mov     WORD PTR VGDTR.Base+2,dx
                ;设置代码段描述符
                mov     ax,cs
                mul     bx
                mov     WORD PTR Code.BaseL,ax ;代码段开始偏移为0
                mov     BYTE PTR Code.BaseM,dl ;代码段界限已在定义时设置好
                mov     BYTE PTR Code.BaseH,dh
                ;设置目标数据段描述符
                mov     ax,ds
                mul     bx                     ;计算并设置目标数据段基址
                add     ax,OFFSET Buffer
                adc     dx,0
                mov     WORD PTR DataD.BaseL,ax
                mov     BYTE PTR DataD.BaseM,dl
                mov     BYTE PTR DataD.BaseH,dh
                ;加载GDTR
                lgdt    QWORD PTR VGDTR
                cli                            ;关中断
                EnableA20                      ;打开地址线A20
                ;切换到保护方式
                mov     eax,cr0
                or      eax,1
                mov     cr0,eax
                ;清指令预取队列,并真正进入保护方式
                JUMP16  Code_Sel,<OFFSET Virtual>
Virtual:        ;现在开始在保护方式下运行
                mov     ax,DataS_Sel
                mov     ds,ax                  ;加载源数据段描述符
                mov     ax,DataD_Sel
                mov     es,ax                  ;加载目标数据段描述符
                cld
                xor     si,si
                xor     di,di                  ;设置指针初值
                mov     cx,BufLen/4            ;设置4字节为单位的缓冲区长度
                repz    movsd                  ;传送
                ;切换回实模式
                mov     eax,cr0
                and     al,11111110b
                mov     cr0,eax
                ;清指令预取队列,进入实方式
                JUMP16  <SEG Real>,<OFFSET Real>
Real:           ;现在又回到实方式
                DisableA20
                sti
                mov     ax,DSEG
                mov     ds,ax
                mov     si,OFFSET Buffer
                cld
                mov     bp,BufLen/16
NextLine:       mov     cx,16
NextCh:         lodsb
                push    ax
                shr     al,1
                call    ToASCII
                EchoCh  al
                pop     ax
                call    ToASCII
                EchoCh  al
                EchoCh  ' '
                loop    NextCh
                EchoCh  0dh
                EchoCh  0ah
                dec     bp
                jnz     NextLine
                mov     ax,4c00h
                int     21h
Start           ENDP
;----------------------------------------------------------------------------
ToASCII         PROC
                and     al,0fh
                add     al,90h
                daa
                adc     al,40h
                daa
                ret
ToASCII         ENDP
;----------------------------------------------------------------------------
CSEG            ENDS                           ;代码段定义结束
;----------------------------------------------------------------------------
                END     Start

3.关于实例步骤的注释

在源程序的开头首先包含了文件“386SCD.INC”，在此包含文件中定义了保护模式程序设计要用到的 一些结构、宏及常量。下面对各实现步骤作些说明。

(1)切换到保护方式的准备工作

在从实模式切换到保护模式之前，必须作必要的准备。准备工作的内容根据实际而定。最起码的准备 工作是建立合适的全局描述符表，并使用GDTR指向该GDT。因为在切换到保护方式时，至少要把代码 段的选择子装载到CS，所以GDT中至少含有代码段的描述符。
从本实例源程序可见，全局描述符表GDT仅有四个描述符：第一个是空描述符；第二个是代码段描述 符；第三个和第四个分别为源数据段及目标数据段描述符。本实例各描述符中的段界限是在定义时设 置的，并且除伪描述符VGDTR中的界限按GDT的实际长度设置外，各使用的存储段描述符的界限都规定 为0FFFFH。另外，描述符中的段属性也根据所描述段的类型被预置，各属性的定义在包含文 件386SCD.INC中均有说明。从属性值可知，这三个段都是16位段。
由于在切换到保护方式后就要引用GDT，所以在切换到保护方式前必须装载GDTR。实例中使用如下指 令装载GDTR：
    LGDT  QWORD PTR VGDTR
该指令的功能是把存储器中的伪描述符VGDTR装入到全局描述符表寄存器GDTR中。伪描述符VGDTR的结 构如前所述结构类型PDESC所示，低字是以字节位单位的全局描述符表段的界限，高双字为描述符表 段的线性基地址(本实例不启用分页机制，所以线性地址等同于物理地址)。本实例中未涉及到局部描 述符表及中断描述符表，后面的文章将作详细说明。

(2)由实模式切换到保护模式

在做好准备后，从实模式切换到保护模式并不难。原则上只要把控制寄存器CR0中的PE位置1即可。本 实例采用如下三条指令设置PE位：
    mov     eax,cr0
    or      eax,1
    mov     cr0,eax
实际情况要比这复杂些。执行上面的三条指令后，处理器转入保护模式，但CS中的内容还是实模式下 代码段的段值，而不是保护模式下代码段的选择子，所以在取指令之前得把代码段的选择子装入CS。 为此，紧接着这三条指令，安排一条如下所示的段间转移指令：
    JUMP16  Code_Sel,<OFFSET Virtual>
这条段间转移指令在实模式下被预取并在保护方式下被执行。 利用这条段间转移指令可把保护模式下代码段的选择子装入CS，同时也刷新指令预取队列。从此真正 进入保护模式。

(3)由保护模式切换到实模式

在80386上，从保护模式切换到实模式的过程类似于从实模式切换到保护模式。原则上只要把控制寄 存器CR0中的PE位清0即可。实际上，在此之后也要安排一条段间转移指令，一方面清指令预取队列， 另一方面把实模式下代码段的段值送CS。这条段间转移指令在保护 方式下被预取并在实模式下被执行。

(4)保护模式下的数据传送

首先，把源数据段和目标数据段的选择子装入DS和ES寄存器，这两个描述符已在实模式下设置好， 把选择子装入段寄存器就意味着把包括基地址在内的段信息装入到了段描述符高速缓冲寄存器。然 后设置指针寄存器SI和DI的初值，也设置计数器CX的初值。根据预置的段属性，在保护方式下，代 码段也仅是16位段，串操作指令只使用16位的SI、DI和CX等寄存器。最后利用串操作指令实施传送。

(5)显示缓冲区中的内容

由于缓冲区在常规内存中，所以在实模式下根据要求按十六进制显示其内容是很容易理解的，这里 就不再多说。

4.内存映象

在源程序中没有把GDT作为一个单独的段对待，但在进入保护方式后，它是一个独立的段。从对代 码段和源数据段描述符所赋的基地址和段界限值可见，代码段和数据段有部分覆盖。尽管这样做不 利于代码和数据的安全，但如果需要，这样做是可行的。本实例运行时的内存映象如下图所示。

5.特别说明

作为第一个实模式和保护模式切换的例子，本实例作了大量的简化处理。
通常，由实模式切换到保护模式的准备工作还应包含建立中断描述符表。但本实例没有建立中断 描述符表。为此，要求整个过程在关中断的情况下进行；要求不使用软中断指令；假设不发生任 何异常。否则会导致系统崩溃。
本实例未使用局部描述符表，所以在进入保护模式后没有设置局部描述符表寄存器LDTR。为此， 在保护模式下使用的段选择子都指定GDT中的描述符。
本实例未定义保护模式下的堆栈段，GDT中没有堆栈段描述符，在保护模式下没有设置SS，所以 在保护方式下没有涉及堆栈操作的指令。
本实例各描述符特权级DPL和各选择子的请求特权级RPL均为0，在保护方式下运行时的当前特权 级CPL也是0。
本实例没有采用分页管理机制，也即CR0中的PG位为0，线性地址就是存储单元的物理地址。

6.打开和关闭地址线A20

PC及其兼容机的第21根地址线(A20)较特殊，计算机系统中一般安排一个 “门”控制该地址线是 否有效。为了访问地址在1M以上的存储单元，应先打开控制地址线A20的“门”。这种设置与实 模式下只使用最低端的1M字节存储空间有关，与处理器是否工作在实模式或保护方式无关，即使 在关闭地址线A20时，也可进入保护模式。
如何打开和关闭地址线A20与计算机系统的具体设置有关。在本文中介绍的包含文件386SCD.INC中 定义了两个宏，打开地址线A20的宏EnableA20和关闭地址线A20的宏DisableA20，此两个宏指令在 一般的PC兼容机上都是可行的。

<二>演示32位代码段和16位代码段切换的实例(实例二)

实例二的逻辑功能是，以十六进制数和ASCII字符两种形式显示从内存地址100000H开始的16个字 节的内容。
从功能上看，本实例类似于实例一，但在实现方法上却有了改变，它更能反映出实模式和保护模 式切换的情况。具体实现步骤是：(1)作切换到保护方式的准备；(2)切换到保护方式的一个32位 代码段；(3)把指定内存区域的内容以字节为单位，转换成对应的十六进制数的ASCII码，并直接 填入显示缓冲区实现显示；(4)再变换到保护方式下的一个16位代码段；(5)把指定内存区域的内 容直接作为ASCII码填入显示缓冲区中实现显示；(6)切换回实模式。

1.实例二源程序

实例二的源程序如下所示：
;名称:ASM2.ASM
;功能:演示实方式和保护方式切换(切换到32位代码段)
;----------------------------------------------------------------------------
INCLUDE         386SCD.INC
;----------------------------------------------------------------------------
DSEG            SEGMENT USE16                     ;16位数据段
;----------------------------------------------------------------------------
GDT             LABEL   BYTE                      ;全局描述符表
DUMMY           Desc    <>                        ;空描述符
Normal          Desc    <0ffffh,,,ATDW,,>         ;规范段描述符
Code32          Desc    <C32Len-1,,,ATCE,D32,>    ;32位代码段描述符
Code16          Desc    <0ffffh,,,ATCE,,>         ;16位代码段描述符
DataS           Desc    <DataLen-1,0,10h,ATDR,,>  ;源数据段描述符
DataD           Desc    <3999,8000h,0bh,ATDW,,>   ;显示缓冲区描述符
Stacks          Desc    <StackLen-1,,,ATDW,,>     ;堆栈段描述符
;----------------------------------------------------------------------------
GDTLen          =       $-GDT                     ;全局描述符表长度
VGDTR           PDesc   <GDTLen-1,>               ;伪描述符
;----------------------------------------------------------------------------
SaveSP          DW      ?                         ;用于保存SP寄存器
SaveSS          DW      ?                         ;用于保存SS寄存器
;----------------------------------------------------------------------------
Normal_Sel      =       Normal-GDT                ;规范段描述符选择子
Code32_Sel      =       Code32-GDT                ;32位代码段选择子
Code16_Sel      =       Code16-GDT                ;16位代码段选择子
DataS_Sel       =       Datas-GDT                 ;源数据段选择子
DataD_Sel       =       DataD-GDT                 ;目标数据段选择子
Stacks_Sel      =       Stacks-GDT                ;堆栈段描述符选择子
;----------------------------------------------------------------------------
DataLen         =       16
;----------------------------------------------------------------------------
DSEG            ENDS                              ;数据段定义结束
;----------------------------------------------------------------------------
StackSeg        SEGMENT PARA STACK USE16
StackLen        =       256
                DB      StackLen DUP(0)
StackSeg        ENDS
;----------------------------------------------------------------------------
CSEG1           SEGMENT USE16 'REAL'              ;16位代码段
                ASSUME  CS:CSEG1,DS:DSEG
;----------------------------------------------------------------------------
Start           PROC
                mov     ax,DSEG
                mov     ds,ax
                ;准备要加载到GDTR的伪描述符
                mov     bx,16
                mul     bx
                add     ax,OFFSET GDT             ;计算并设置基地址
                adc     dx,0                      ;界限已在定义时设置好
                mov     WORD PTR VGDTR.Base,ax
                mov     WORD PTR VGDTR.Base+2,dx
                ;设置32位代码段描述符
                mov     ax,CSEG2
                mul     bx
                mov     WORD PTR Code32.BaseL,ax
                mov     BYTE PTR Code32.BaseM,dl
                mov     BYTE PTR Code32.BaseH,dh
                ;设置16位代码段描述符
                mov     ax,CSEG3
                mul     bx
                mov     WORD PTR Code16.BaseL,ax  ;代码段开始偏移为0
                mov     BYTE PTR Code16.BaseM,dl  ;代码段界限已在定义时设置好
                mov     BYTE PTR Code16.BaseH,dh
                ;设置堆栈段描述符
                mov     ax,ss
                mov     WORD PTR SaveSS,ax
                mov     WORD PTR SaveSP,sp
                mov     ax,StackSeg
                mul     bx
                mov     WORD PTR Stacks.BaseL,ax
                mov     BYTE PTR Stacks.BaseM,dl
                mov     BYTE PTR Stacks.BaseH,dh
                ;加载GDTR
                lgdt    QWORD PTR VGDTR
                cli                               ;关中断
                EnableA20                         ;打开地址线A20
                ;切换到保护方式
                mov     eax,cr0
                or      al,1
                mov     cr0,eax
                ;清指令预取队列,并真正进入保护方式
                JUMP16  Code32_Sel,<OFFSET SPM32>
ToReal:         ;现在又回到实方式
                mov     ax,DSEG
                mov     ds,ax
                mov     sp,SaveSP
                mov     ss,SaveSS
                DisableA20
                sti
                mov     ax,4c00h
                int     21h
Start           ENDP
;----------------------------------------------------------------------------
CSEG1           ENDS                              ;代码段定义结束
;----------------------------------------------------------------------------
CSEG2           SEGMENT USE32 'PM32'
                ASSUME  CS:CSEG2
;----------------------------------------------------------------------------
SPM32           PROC
                mov     ax,Stacks_Sel
                mov     ss,ax
                mov     esp,StackLen
                mov     ax,DataS_Sel
                mov     ds,ax
                mov     ax,DataD_Sel
                mov     es,ax
                xor     esi,esi
                xor     edi,edi
                mov     ecx,DataLen
                cld
Next:           lodsb
                push    ax
                CALL    ToASCII
                mov     ah,7
                shl     eax,16
                pop     ax
                shr     al,4
                CALL    ToASCII
                mov     ah,7
                stosd
                mov     al,20h
                stosw
                loop    Next
                JUMP32   Code16_Sel,<OFFSET SPM16>
SPM32           ENDP
;----------------------------------------------------------------------------
ToASCII         PROC
                and     al,00001111b
                add     al,30h
                cmp     al,39h
                jbe     Isdig
                add     al,7
IsDig:          ret
ToASCII         ENDP
;----------------------------------------------------------------------------
C32Len          =       $
;----------------------------------------------------------------------------
CSEG2           ENDS
;----------------------------------------------------------------------------
CSEG3           SEGMENT USE16 'PM16'
                ASSUME  CS:CSEG3
;----------------------------------------------------------------------------
SPM16           PROC
                xor     si,si
                mov     di,DataLen*3*2
                mov     ah,7
                mov     cx,DataLen
AGain:          lodsb
                stosw
                loop    AGain
                mov     ax,Normal_sel
                mov     ds,ax
                mov     es,ax
                mov     ss,ax
                mov     eax,cr0
                and     al,11111110b
                mov     cr0,eax
                jmp     FAR PTR ToReal
SPM16           ENDP
;----------------------------------------------------------------------------
CSEG3           ENDS
;----------------------------------------------------------------------------
                END     Start

2.关于实现步骤的注释

(1)切换到保护模式的准备工作

建立全局描述符表，这里的全局描述符表含有两个16位数据段的描述符、一个16位代码段的描述符 和一个16位的堆栈段描述符。此外，GDT中还有一个32位的代码段描述符，描述32位代码段，该描 述符的属性字段中的D位为1。

(2)由实模式切换到保护模式

由实模式切换到保护模式32位代码段的方法与切换到16位代码段的方法相同。由保护模式16位代码 段切换回实模式的方法与实例一相似。
在保护模式下，通过如下直接段间转移指令从32位代码段切换到16位代码段：
    JUMP32   Code16_Sel,<OFFSET SPM16>
从该宏指令的定义可知，该转移指令含48位指针，其高16位是16位代码段的选择子，低32位是16位 代码段的入口偏移。该指令在32位方式下预取并执行。 由于在32位方式下执行，所以要使用48位指针。

(3)显示指定内存区域的内容

在本实例中，采用直接写显示缓冲区的方法实现显示。假设显示缓冲区的开始物理地址是0B8000H， 3号文本显示模式，在屏幕的第一行进行显示。

3.特别说明

本实例在保护方式下使用了涉及堆栈操作的指令，因此建立了一个16位的保护模式下的堆栈段。
本实例仍作了大量的简化处理。如：没有建立IDT和LDT等，各特权级均是0。也没有采用分页管理机制。
从本实例的GDT中可见，两个数据段的界限都是根据实际大小而设置的。从源程序代码段CSEG3可见，在 切换到实模式之前，把一个指向似乎没有用的数据段的描述符Normal的选择子装载到DS和ES。这是 为什么呢?

实模 式下 段描 述符 高速 缓冲 寄存 器的 内容 | 段寄存器 | 段基地址 | 段界限(固定) | 段属性(固定)
 |  |  |  | 存在性 | 特权级 | 已存取 | 粒度 | 扩展方向 | 可读性 | 可写性 | 可执行 | 堆栈大小 | 一致特权
 | CS | 当前CS*16 | 0000FFFFH | Y | 0 | Y | B | U | Y | Y | Y | - | N
 | SS | 当前SS*16 | 0000FFFFH | Y | 0 | Y | B | U | Y | Y | N | W | -
 | DS | 当前DS*16 | 0000FFFFH | Y | 0 | Y | B | U | Y | Y | N | - | -
 | ES | 当前ES*16 | 0000FFFFH | Y | 0 | Y | B | U | Y | Y | N | - | -
 | FS | 当前FS*16 | 0000FFFFH | Y | 0 | Y | B | U | Y | Y | N | - | -
 | GS | 当前GS*16 | 0000FFFFH | Y | 0 | Y | B | U | Y | Y | N | - | -

在分段管理机制一文中已介绍过，每个段寄存器都配有段描述符高速缓冲寄存器，这些高速缓冲寄存器 在实方式下仍发挥作用，只是内容上与保护模式下有所不同。如上表所示，其中“Y”表示“是”； “N”表示“否”；“B”表示字节；“U”表示向上扩展，“W”表示以字方式操作堆栈。段基地址仍是 32位，其值是相应段寄存器值(段值)乘以16，在把段值装载到段寄存器时刷新。由于其值是16位段值乘 上16，所以在实模式下基地址实际上有效位只有20位。每个段的32位段界限都固定为0FFFFH，段属性的 许多位也是固定的。所谓固定是指在实方式下不可设置这些属性值，只能继续沿用保护方式下所设置的 值。因此，在准备结束保护模式回到实模式之前，要通过加载一个合适的描述符选择子到有关段寄存器 ，以使得对应段描述符高速缓冲寄存器中含有合适的段界限和属性。本实例GDT中的描述符Normal就是 这样一个描述符，在返回实模式之前把对应选择子Normal_Sel加载到DS和ES就是此目的。由于SS段描述 符中的内容已符合实模式的需要，所以尽管也改变了SS，但不需要重新加载SS(本实例中重新加载 了SS，这除了稍增加运行时间外，并没有什么坏处)。16位代码段描述符中的内容也符合实模式的需要， 所以在通过16位代码段返回实模式时，CS段描述符中的内容也符合实模式的要求。需要注意的是，不能 从32位代码段返回实模式，这是因为无法实现从32位代码段返回时CS高速缓冲寄存器中的属性符合实模 式的要求(实模式不能改变段属性)。顺便说以下，实例一中的描述符都是符合实模式要求的。 段描述符高速缓冲寄存器中含有合适的段界限

4.关于32位代码段程序设计的说明

在32位代码段中，缺省的操作数大小是32位，缺省的存储单元地址大小是32位。由于串操作指令使用的 指针寄存器是ESI和EDI，LOOP指令使用的计数器是ECX，所以，在代码段CSEG2中，为了使用串操作指令 ，对ESI和EDI等寄存器赋初值。请比较代码段CSEG3中的相关片段和实例一中的相关片段，它们是16位 代码段。


五.任务状态段和控制门

每个任务有一个任务状态段TSS，用于保存任务的有关信息，在任务内变换特权级和任务切换时， 要用到这些信息。为了控制任务内发生特权级变换的转移，为了控制任务切换，一般要通过控制 门进行这些转移。本文将介绍任务状态段和控制门。

<一>系统段描述符

系统段是为了实现存储管理机制所使用的一种特别的段。在80386中，有两种系统段：任务状态 段TSS和局部描述符表LDT段。用于描述系统段的描述符称为系统段描述符。

1.系统段描述符的格式

系统段描述符的一般格式如下表所示。

系统段 描述符 | m+7 | m+6 | m+5 | m+4 | m+3 | m+2 | m+1 | m+0
 | Base(31...24) | Attributes |  | Segment Base(23...0) |  |  | Segment Limite(15...0)

系统段 描述符 的属性 | Byte m+6 |  |  |  |  |  |  |  | Byte m+5
 | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0 | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0
 | G | X | 0 | AVL | Limit(19...16) |  |  |  | P | DPL |  | DT0 | TYPE

与存储段描述符相比，它们很相似，区分的标志是属性字节中的描述符类型位DT的值。DT=1表示 存储段，DT=0表示系统段。系统段描述符中的段基地址和段界限字段与存储段描述符中的意义完 全相同；属性中的G位、AVL位、P位和DPL字段的作用也完全相同。存储段描述符属性中的D位在 系统段描述符中不使用，现用符号X表示。系统段描述符的类型字段TYPE仍是4位，其编码及表示 的类型列于下表，其含义与存储段描述符的类型却完全不同。

系统段 类 型 | 类型编码 | 说 明
 | 0 | 未定义
 | 1 | 可用286TSS
 | 2 | LDT
 | 3 | 忙的286TSS
 | 4 | 286调用门
 | 5 | 任务门
 | 6 | 286中断门
 | 7 | 286陷阱门

系统段 类 型 | 类型编码 | 说 明
 | 8 | 未定义
 | 9 | 可用386TSS
 | A | 未定义
 | B | 忙的386TSS
 | C | 386调用门
 | D | 未定义
 | E | 386中断门
 | F | 386陷阱门

从上表可见，只有类型编码为2、1、3、9和B的描述符才是真正的系统段描述符，它们用于描述 系统段LDT和任务状态段TSS，其它类型的描述符是门描述符。
利用前文定义的存储段描述符结构类型DESC仍能方便地在程序中说明系统段描述符。需要注意 的是，系统段描述符的选择子不能用来读写系统段，要想读写系统段，必须使用别名技术。

2.LDT段描述符

LDT段描述符描述任务的局部描述符表段。例如：下面的描述符LDTABLE描述一个局部描述符表 段，基地址是654321H，以字节为单位的界限是1FH，描述符特权级是0。
    LDTABLE  DESC  <1FH,4321H,65H,82H,,>
LDT段描述符必须安排在全局描述符表中才有效。在装载LDTR寄存器时，描述符中的LDT段基地 址和段界限等信息被装入LDT段描述符高速缓冲寄存器中。

3.任务状态段描述符

任务状态段TSS用于保存任务的各种状态信息。任务状态段描述符描述某个任务状态段TSS描述符 分为286TSS和386TSS两类。TSS描述符规定了任务状态段的基地址和任务状态段的大小等信息。 例如，下面的描述符TempTask描述一个可用的386任务状态段，基地址是123456H，以字节为单位 的界限是104，描述符特权级是0。
    TempTask  DESC  <104,3456H,12H,89H,,>
在装载任务状态段寄存器TR时，描述符中的段基地址和段界限等信息被装入到TR的高速缓冲寄存 器中。在任务切换或执行LTR指令时，要装载TR寄存器。
TSS描述符中的类型规定：TSS要么为“忙”，要么为“可用”。如果一个任务是当前正执行的任 务，或者是用TSS中的链接字段沿挂起任务链接到当前任务上的任务，那么该任务是“忙”的任 务；否则该任务为“可用”任务。
利用段间转移指令JMP和段间调用指令CALL，直接通过TSS描述符或通过任务门可实现任务切换。

<二>门描述符

除存储段描述符和系统段描述符外，还有一类门描述符。门描述符并不描述某种内存段，而是描 述控制转移的入口点。这种描述符好比一个同向另一代码段的门。通过这种门，可实现任务内特 权级的变换和任务间的切换。所以，这种门描述符也称为控制门。

1.门描述符的一般格式

门描述符的一般格式如下图所示。门描述符只有位于描述符内偏移5的类型字节与系统段保持一 致，也由该字节标示门描述符和系统段描述符。该字节内的P和DPL的意义与其它描述符种中的 意义相同。其它字节主要用于存放一个48位的全指针(16位的选择子和32位的偏移量)。

门描述符 | m+7 | m+6 | m+5 | m+4 | m+3 | m+2 | m+1 | m+0
 | Offset(31...16) |  | Attributes |  | Selector |  | Offset(15...0)

门描述 符属性 | Byte m+5 |  |  |  |  |  |  |  | Byte m+4
 | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0 | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0
 | P | DPL |  | DT0 | TYPE |  |  |  | 000 |  |  | Dword Count

根据上图给出的门描述符的结构，可定义如下的门描述符结构类型：
    GATE     STRUC      ;门结构类型定义
    OFFSETL  DW      0  ;32位偏移的低16位
    SELECTOR DW      0  ;选择子
    DCOUNT   DB      0  ;双字计数字段
    GTYPE    DB      0  ;类型
    OFFSETH  DW      0  ;32位偏移的高16位
    GATE     ENDS
利用门描述符结构类型GATE能方便地在程序中说明门描述符。
例如，下面的门描述符SUBRG描述一个386调用门，门内的选择子是10H，入口偏移是123456H，门 描述符特权级是3，双字计数是0。
    SUBRG    GATE  <3456,10H,,8CH+60H,12H>
从上述描述符类型的列表中可见，门描述符又可分为：任务门、调用门、中断门和陷阱门，并且 除任务门外，其它描述符还各分成286和386两种。

2.调用门

调用门描述某个子程序的入口。调用门内的选择子必须实现代码段描述符，调用门内的偏移是对 应代码段内的偏移。利用段间调用指令CALL，通过调用门可实现任务内从外层特权级变换到内层 特权级。
在上图所示的门描述符内偏移4字节的位0至位4是双字计数字段，该字段只在调用门描述符中有 效，在其它门描述符中无效。主程序通过堆栈把入口参数传递给子程序，如果在利用调用门调用 子程序时引起特权级的转换和堆栈的改变，那么就需要将外层堆栈中的参数复制到内层堆栈。该 双字计数字段就是用于说明这种情况发生时，要复制的双字参数的数量。

3.任务门

任务门指示任务。任务门内的选择子必须指示GDT中的任务状态段TSS描述符，门中的偏移无意 义。任务的入口点保存在TSS中。利用段间转移指令JMP和段间调用指令CALL，通过任务门可实 现任务切换。

4.中断门和陷阱门

中断门和陷阱门描述中断/异常处理程序的人口点。中断门和陷阱门内的选择子必须指向代码段 描述符，门内的偏移就是对应代码段的人口点的偏移。中断门和陷阱门只有在中断描述符表IDT中 才有效。关于中断门和陷阱门的区别将在以后的文章中论述。

<三>任务状态段

任务状态段(Task State Segment)是保存一个任务重要信息的特殊段。任务状态段描述符用于 描述这样的系统段。任务状态段寄存器TR的可见部分含有当前任务的任务状态段描述符的选择 子，TR的不可见的高速缓冲寄存器部分含有当前任务状态段的段基地址和段界限等信息。
TSS在任务切换过程中起着重要作用，通过它实现任务的挂起和恢复。所谓任务切换是指，挂起 当前正在执行的任务，恢复或启动另一任务的执行。在任务切换过程中，首先，处理器中各寄 存器的当前值被自动保存到TR所指定的TSS中；然后，下一任务的TSS的选择子被装入TR；最后， 从TR所指定的TSS中取出各寄存器的值送到处理器的各寄存器中。由此可见，通过在TSS中保存 任务现场各寄存器状态的完整映象，实现任务的切换。
任务状态段TSS的基本格式如下图所示。

任 务 状 态 段 基 本 部 分 的 格 式 | BIT31—BIT16 | BIT15—BIT1 | BIT0 | Offset
 | 0000000000000000 | 链接字段 |  | 0
 | ESP0 |  |  | 4
 | 0000000000000000 | SS0 |  | 8
 | ESP1 |  |  | 0CH
 | 0000000000000000 | SS1 |  | 10H
 | ESP2 |  |  | 14H
 | 0000000000000000 | SS2 |  | 18H
 | CR3 |  |  | 1CH
 | EIP |  |  | 20H
 | EFLAGS |  |  | 24H
 | EAX |  |  | 28H
 | ECX |  |  | 2CH
 | EDX |  |  | 30H
 | EBX |  |  | 34H
 | ESP |  |  | 38H
 | EBP |  |  | 3CH
 | ESI |  |  | 40H
 | EDI |  |  | 44H
 | 0000000000000000 | ES |  | 48H
 | 0000000000000000 | CS |  | 4CH
 | 0000000000000000 | SS |  | 50H
 | 0000000000000000 | DS |  | 54H
 | 0000000000000000 | FS |  | 58H
 | 0000000000000000 | GS |  | 5CH
 | 0000000000000000 | LDTR |  | 60H
 | I/O许可位图偏移 | 000000000000000 | T | 64H

从图中可见，TSS的基本格式由104字节组成。这104字节的基本格式是不可改变的，但在此之外 系统软件还可定义若干附加信息。基本的104字节可分为链接字段区域、内层堆栈指针区域、地 址映射寄存器区域、寄存器保存区域和其它字段等五个区域。

1.寄存器保存区域

寄存器保存区域位于TSS内偏移20H至5FH处，用于保存通用寄存器、段寄存器、指令指针和标志 寄存器。当TSS对应的任务正在执行时，保存区域是未定义的；在当前任务被切换出时，这些寄 存器的当前值就保存在该区域。当下次切换回原任务时，再从保存区域恢复出这些寄存器的值， 从而，使处理器恢复成该任务换出前的状态，最终使任务能够恢复执行。
从上图可见，各通用寄存器对应一个32位的双字，指令指针和标志寄存器各对应一个32位的双 字；各段寄存器也对应一个32位的双字，段寄存器中的选择子只有16位，安排再双字的低16位， 高16位未用，一般应填为0。

2.内层堆栈指针区域

为了有效地实现保护，同一个任务在不同的特权级下使用不同的堆栈。例如，当从外层特权 级3变换到内层特权级0时，任务使用的堆栈也同时从3级变换到0级堆栈；当从内层特权级0变 换到外层特权级3时，任务使用的堆栈也同时从0级堆栈变换到3级堆栈。所以，一个任务可能 具有四个堆栈，对应四个特权级。四个堆栈需要四个堆栈指针。
TSS的内层堆栈指针区域中有三个堆栈指针，它们都是48位的全指针(16位的选择子和32位的 偏移)，分别指向0级、1级和2级堆栈的栈顶，依次存放在TSS中偏移为4、12及20开始的位置。 当发生向内层转移时，把适当的堆栈指针装入SS及ESP寄存器以变换到内层堆栈，外层堆栈的 指针保存在内层堆栈中。没有指向3级堆栈的指针，因为3级是最外层，所以任何一个向内层 的转移都不可能转移到3级。
但是，当特权级由内层向外层变换时，并不把内层堆栈的指针保存到TSS的内层堆栈指针区域。 实际上，处理器从不向该区域进行写入，除非程序设计者认为改变该区域的值。这表明向内层 转移时，总是把内层堆栈认为是一个空栈。因此，不允许发生同级内层转移的递归，一旦发生 向某级内层的转移，那么返回到外层的正常途径是相匹配的向外层返回。

3.地址映射寄存器区域

从虚拟地址空间到线性地址空间的映射由GDT和LDT确定，与特定任务相关的部分由LDT确定， 而LDT又由LDTR确定。如果采用分页机制，那么由线性地址空间到物理地址空间的映射由包含 页目录表起始物理地址的控制寄存器CR3确定。所以，与特定任务相关的虚拟地址空间到物理 地址空间的映射由LDTR和CR3确定。显然，随着任务的切换，地址映射关系也要切换。
TSS的地址映射寄存器区域由位于偏移1CH处的双字字段(CR3)和位于偏移60H处的字 字段(LDTR)组成。在任务切换时，处理器自动从要执行任务的TSS中取出这两个字段，分别 装入到寄存器CR3和LDTR。这样就改变了虚拟地址空间到物理地址空间的映射。
但是，在任务切换时，处理器并不把换出任务但是的寄存器CR3和LDTR的内容保存到TSS中的 地址映射寄存器区域。事实上，处理器也从来不向该区域自动写入。因此，如果程序改变 了LDTR或CR3，那么必须把新值人为地保存到TSS中的地址映射寄存器区域相应字段中。可以 通过别名技术实现此功能。

4.链接字段

链接字段安排在TSS内偏移0开始的双字中，其高16位未用。在起链接作用时，地16位保存前 一任务的TSS描述符的选择子。
如果当前的任务由段间调用指令CALL或中断/异常而激活，那么链接字段保存被挂起任务的 TSS的选择子，并且标志寄存器EFLAGS中的NT位被置1，使链接字段有效。在返回时，由 于NT标志位为1，返回指令RET或中断返回指令IRET将使得控制沿链接字段所指恢复到链上的 前一个任务。

5.其它字段

为了实现输入/输出保护，要使用I/O许可位图。任务使用的I/O许可位图也存放在TSS中，作 为TSS的扩展部分。在TSS内偏移66H处的字用于存放I/O许可位图在TSS内的偏移(从TSS开头 开始计算)。关于I/O许可位图的作用，以后的文章中将会详细介绍。
在TSS内偏移64H处的字是为任务提供的特别属性。在80386中，只定义了一种属性，即调试 陷阱。该属性是字的最低位，用T表示。该字的其它位置被保留，必须被置为0。在发生任务 切换时，如果进入任务的T位为1，那么在任务切换完成之后，新任务的第一条指令执行之前 产生调试陷阱。

6.用结构类型定义TSS

根据上图给出的任务状态段TSS的结构，可定义如下的TSS结构类型：
;----------------------------------------------------------------------------
;任务状态段结构类型定义
;----------------------------------------------------------------------------
TSS             STRUC
TRLink          DW      0      ;链接字段
                DW      0      ;不使用,置为0
TRESP0          DD      0      ;0级堆栈指针
TRSS0           DW      0      ;0级堆栈段寄存器
                DW      0      ;不使用,置为0
TRESP1          DD      0      ;1级堆栈指针
TRSS1           DW      0      ;1级堆栈段寄存器
                DW      0      ;不使用,置为0
TRESP2          DD      0      ;2级堆栈指针
TRSS2           DW      0      ;2级堆栈段寄存器
                DW      0      ;不使用,置为0
TRCR3           DD      0      ;CR3
TREIP           DD      0      ;EIP
TREFlag         DD      0      ;EFLAGS
TREAX           DD      0      ;EAX
TRECX           DD      0      ;ECX
TREDX           DD      0      ;EDX
TREBX           DD      0      ;EBX
TRESP           DD      0      ;ESP
TREBP           DD      0      ;EBP
TRESI           DD      0      ;ESI
TREDI           DD      0      ;EDI
TRES            DW      0      ;ES
                DW      0      ;不使用,置为0
TRCS            DW      0      ;CS
                DW      0      ;不使用,置为0
TRSS            DW      0      ;SS
                DW      0      ;不使用,置为0
TRDS            DW      0      ;DS
                DW      0      ;不使用,置为0
TRFS            DW      0      ;FS
                DW      0      ;不使用,置为0
TRGS            DW      0      ;GS
                DW      0      ;不使用,置为0
TRLDTR          DW      0      ;LDTR
                DW      0      ;不使用,置为0
TRTrip          DW      0      ;调试陷阱标志(只用位0)
TRIOMap         DW      $+2    ;指向I/O许可位图区的段内偏移
TSS             ENDS


六.控制转移

控制转移基本上可分为两大类：同一任务内的控制转移和任务间的控制转移(任务 切换)。同一任务内的控制转移又可分为：段内转移、特权级不变的段间转移和特 权级变换的段间转移。段内转移与实模式下相似，不涉及特权级变换和任务切换。 只有段间转移才涉及特权级变换和任务切换。本文介绍保护方式下的控制转移， 重点是任务内的特权级变换和任务间的切换。 这里下载本文所有源代码。

<一>任务内无特权级变换的转移

各种段内转移与实模式下相似，当然不涉及特权级变换和任务切换。只有各种形式 的段间转移才涉及特权级变换和任务切换。

1.段间转移指令

与实模式下相同，指令JMP、CALL和RET都具有段间转移的功能，指令INT和IRET总是 段间转移。此外，中断/异常也将引起段间转移。有时把这些具有段间转移功能的指 令统称为段间转移指令。
在保护模式下，段间转移的目标位置由选择子和偏移构成的地址表示，常把它称为 目标地址指针。在32位代码段中，上述指针内的偏移使用32位表示，这样的指针也 称为48位全指针。在实例二的32位代码段内就使用了48位全指针。在16位代码段中 ，上述指针内的偏移只使用16位表示。
与实模式下相似，段间转移指令JMP和段间调用指令CALL还可分为段间直接转移和段 间间接转移两类。如果指令JMP和CALL在指令中直接含有目标地址指针，那么就是段 间直接转移；如果指令中含有指向包含目标地址指针的门描述符或TSS描述符的指针 ，那么就是段间间接转移，这种指针只有选择子部分有效，指示调用门、任务门或 TSS描述符，而偏移部分不起作用。实际上，当段间转移指令JMP和段间调用指令CALL所 含指针的选择子部分指示代码段描述符，那么就是段间直接转移，偏移部分表示目 标代码段的入口点；当选择子部分指示门描述符或TSS描述符时，就是段间间接转移。

2.向目标代码段转移的步骤

处理器在执行上述段间转移指令向目标代码段实施转移的过程中，一般至少要经过如 下步骤：
(1)判断目标地址指针内的选择子指示的描述符是否为空描述符。空描述符是GDT中的 第0个描述符，是一个特殊的描述符。目标代码段描述符不能为空描述符，也即选择 子的高14位不能为0。
(2)从全局或局部描述符表内读出目标代码段描述符。由选择子内的TI位，确定使用 全局描述符表还是局部描述符表。
(3)根据情况，检测描述符类型是否正确；调整RPL。
(4)把目标代码段描述符内的有关内容装载到CS高速缓冲寄存器。
(5)判断目标地址指针内的偏移是否越出代码段的界限。目标地址指针内的偏移必须 不超过目标代码段界限。
(6)装载CS段寄存器和指令指针寄存器EIP；CPL存入CS内选择子的RPL字段。
上述步骤只是对转移过程的简单说明，实际的动作还要复杂。在把目标代码段描述符 内的有关内容转载到CS高速缓冲寄存器时，还要进行如下保护检测，其中的DPL表示 目标代码段描述符的特权级：
(1)对于非一致代码段，要求CPL=DPL，RPL<=DPL；对于一致代码段，要求CPL>=DPL。
(2)代码段必须存在，即描述符中的P位必须是1。
通常描述符特权级DPL规定了对应段的特权级。如果描述符描述的是数据段，那么DPL就 规定了访问该数据段的最外层特权级；如果描述符描述的是代码段，那么DPL就规定了 执行该代码段所需要的CPL。但从上述装载CS高速缓冲寄存器时进行的保护检测可见， 对于一致代码段，却要求CPL>=DPL,也就是说，一致代码段描述符中的DPL规定了可以转 移到一致代码段的最内层特权级。于是，3级的程序可以转移到任何一致的代码段，而0级 的程序只允许转移到DPL等于0的一致代码段。一致代码段描述符内DPL的这种解释，正好 与正常的DPL的解释相反。
一致的可执行段是一种特别的段。这种存储段，为在多个特权级执行的程序，提供对子例 程的共享支持，而不要求改变特权级。例如，通过把数值库例程放在一致的代码段中，可 以使不同级执行的程序共享数值库例程。这样，任何特权级的程序可以使用段间调用指令 ，调用库中的例程，并在调用者所具有的特权级执行该例程。

3.任务内无特权级变换的转移

所谓任务内无特权级变换的转移指：在转移到新的代码段时，当前特权级CPL保持不变。 利用段间转移指令JMP、段间调用指令CALL和段间返回指令RET可实现任务内无特权级变换 的转移。利用INT指令和IRET指令也可实现任务内无特权级变换的转移。

(1)利用段间直接转移指令JMP或CALL

在执行段间转移指令JMP时，如果指令内所含指针指示一个代码段，那么就直接开始上述 向目标代码段转移的步骤；在执行段间调用指令CALL时，如果指令内所含指针指针指示一 个代码段，那么就把返回地址指针压栈，然后就直接开始上述向目标代码段转移的步骤。 顺利通过这几步(不调整RPL)后，就完成了任务内无特权级变换的转移。
由此可见，利用段间直接转移指令JMP或调用指令CALL可方便地进行任务内无特权级变换 的转移，但不能进行任务内特权级变换的转移。

(2)利用段间返回指令RET

在执行段间返回指令RET时，如果从堆栈中弹出的目标地址指针指示一个代码段，并且选 择子符合RPL=CPL的条件，那么就开始上述向目标代码段的转移步骤。顺利通过这几步后 ，就完成了任务内无特权级变换的转移。
通常情况下，段间返回指令RET与段间调用指令CALL对应。在利用段间调用指令CALL以任 务内无特权级变换的方式转移到某个子程序后，在子程序内利用段间返回指令RET以任务 内无特权级变换的方式返回主程序。由于调用时无特权级变换，所以返回时也无特权级变 换，如果真是如此，那么必须能够满足条件RPL=CPL。

(3)利用调用门和其它途径

如何利用调用门实行和其它方法实现任务内无特权级变换的转移将在后面的文章中介绍。

4.装载数据段和堆栈段寄存器时的特权检测

上面简单地说明了把选择子装入代码段寄存器CS时为实现保护而进行的检测，下面也简单 地说明在把选择子装入数据段寄存器和堆栈段寄存器时要进行的检测。
在把选择子装入数据段寄存器DS、ES、FS或GS时，要进行如下检测：
(1)选择子不能为空；
(2)选择子指定的描述符必须是数据段描述符、可读可执行的代码段或一致可读的可执行 代码段的描述符；
(3)对于数据段和可读可执行代码段，要求CPL<=DPL，RPL<=DPL；
(4)对应的段必须存在。
若装入的选择子不满足上述要求，则会产生异常。
在把选择子装入堆栈段寄存器SS时要进行如下检测：
(1)选择子不能为空；
(2)选择子指定的描述符必须是可读写的数据段描述符；
(3)要求CPL=DPL=RPL；
(4)对应段必须存在。
若装入的选择子不满足上述条件，则在装入SS时就会引起异常。

<二>演示任务内无特权级变换转移的实例(实例三)

在实例二中，32位代码段到16位代码段的转移就是任务内无特权级转移的例子。
下面再给出一个用于演示任务内无特权级变换转移的实例。该实例使用了段间转移指令JMP、 段间调用指令CALL和段间返回指令RET实现同一任务内相同特权级的转移。该实例还建立并使 用了局部描述符表LDT。

1.实现步骤和源程序

实现步骤如下：(1)实模式下的初始化，包括对GDT和演示任务LDT的初始化，装载GDTR；(2)从 实模式切换到保护模式，处于0特权级；(3)装载LDTR，并设置堆栈；(4)利用段间转移指 令JMP实现从代码段K到同级代码段L的转移；(5)利用段间调用指令CALL调用同级代码段C中的 子程序D显示字符串信息；(6)利用段间调用指令CALL调用同级代码段C中的子程序H把十六进制 数转换成对应的ASCII码；(7)再利用段间调用指令CALL调用同级代码段C中的子程序D显示字符 串信息；(8)利用段间转移指令JMP实现从代码段L到代码段K的转移；(9)从保护模式切换到实 模式；(10)在实模式下结束程序。
该实例的逻辑功能是用十六进制数的形式显示代码段L的段界限的值。源程序如下：
;名称:ASM3.ASM
;功能:演示任务内无特权级变换的转移
;编译:TASM ASM3.ASM
;连接:TLINK ASM3.OBJ
;----------------------------------------------------------------------------
INCLUDE         386SCD.INC
;----------------------------------------------------------------------------
GDTSeg          SEGMENT PARA USE16 'GDT'          ;全局描述符表数据段(16位)
;----------------------------------------------------------------------------
GDT             LABEL   BYTE                      ;全局描述符表
DUMMY           Desc    <>                        ;空描述符
Normal          Desc    <0ffffh,,,ATDW,,>         ;规范段描述符
CodeK           Desc    <0ffffh,,,ATCE,,>         ;代码段K的描述符
LDTable         Desc    <LDTLen-1,,,ATLDT,,>      ;局部描述符表段的描述符
;----------------------------------------------------------------------------
GDTLen          =       $-GDT                     ;全局描述符表长度
;----------------------------------------------------------------------------
Normal_Sel      =       Normal-GDT                ;规范段描述符选择子
CodeK_Sel       =       CodeK-GDT                 ;代码段K的选择子
LDT_Sel         =       LDTable-GDT               ;局部描述符表段的选择子
;----------------------------------------------------------------------------
GDTSeg          ENDS                              ;全局描述符表段定义结束
;----------------------------------------------------------------------------
LDTSeg          SEGMENT PARA USE16 'LDT'          ;局部描述符表数据段(16位)
LDT             LABEL   BYTE                      ;局部描述符表
;代码段L的描述符
CodeL           Desc    <CodeLLen-1,CodeLSeg,,ATCE,,>
;代码段C的描述符
CodeC           Desc    <CodeCLen-1,CodeCSeg,,ATCE,,>
;显示缓冲区段描述符
VideoBuf        Desc    <0ffffh,0b800h,,ATDW,,>
;LDT别名段描述符(DPL=3)
ToLDT           Desc    <LDTLen-1,LDTSEG,,ATDR+DPL3,,>
;显示信息缓冲区数据段描述符(DPL=3)
MData           Desc    <MDataLen-1,MDataSeg,,ATDW+DPL3,,>
;堆栈段描述符
StackS          Desc    <TopOfS-1,StackSeg,,ATDWA,,>
;----------------------------------------------------------------------------
LDTLen          =       $-LDT                     ;LDT所占字节数
LDNum           =       ($-LDT)/(SIZE Desc)       ;LDT含描述符项数
;----------------------------------------------------------------------------
CodeL_Sel       =       CodeL-LDT+TIL             ;代码段L的选择子
CodeC_Sel       =       CodeC-LDT+TIL             ;代码段C的选择子
Video_Sel       =       VideoBuf-LDT+TIL          ;显示缓冲区选择子
ToLDT_Sel       =       ToLDT-LDT+TIL             ;LDT别名段选择子
MData_Sel       =       MData-LDT+TIL+RPL3        ;显示信息数据段选择子
Stack_Sel       =       StackS-LDT+TIL            ;堆栈段选择子
;----------------------------------------------------------------------------
LDTSeg          ENDS                              ;局部描述符表段定义结束
;----------------------------------------------------------------------------
MDataSeg        SEGMENT PARA USE16 'MDATA'        ;显示信息缓冲区数据段
;----------------------------------------------------------------------------
Message         DB      'Value=',0
Buffer          DB      80 DUP(0)
MDataLen        =       $
;----------------------------------------------------------------------------
MDataSeg        ENDS                              ;显示缓冲区数据段结束
;----------------------------------------------------------------------------
StackSeg        SEGMENT DWORD USE16 'STACK'       ;堆栈段
;----------------------------------------------------------------------------
                DW      512 DUP(?)
TopOfS          =       $
;----------------------------------------------------------------------------
StackSeg        ENDS                              ;堆栈段结束
;----------------------------------------------------------------------------
CodeCSeg        SEGMENT PARA USE16 'CODEC'        ;任务代码段C
                ASSUME  CS:CodeCSeg
;----------------------------------------------------------------------------
;显示信息子程序
;入口参数:fs:si指向要显示的以0结尾的字符串,es:di指向显示缓冲区
;----------------------------------------------------------------------------
DispMsg         PROC    FAR
                mov     ah,01001110b
Disp1:          mov     al,BYTE PTR fs:[si]
                inc     si
                or      al,al
                jz      Disp2
                mov     WORD PTR es:[di],ax
                inc     di
                inc     di
                jmp     Disp1
Disp2:          ret
DispMsg         ENDP
;----------------------------------------------------------------------------
;把AL寄存器低4位二进制数(一位16进制数)转换成ASCII码
;----------------------------------------------------------------------------
HToASCII        PROC    FAR
                and     al,00001111b
                add     al,90h
                daa
                adc     al,40h
                daa
                ret
HToASCII        ENDP
;----------------------------------------------------------------------------
CodeCLen        =       $
;----------------------------------------------------------------------------
CodeCSeg        ENDS                              ;代码段C定义结束
;----------------------------------------------------------------------------
CodeLSeg        SEGMENT PARA USE16 'CODEL'
                ASSUME  CS:CodeLSeg
;----------------------------------------------------------------------------
Virtual2        PROC    FAR
                mov     ax,Video_Sel              ;设置显示缓冲区指针
                mov     es,ax
                mov     di,1986
                mov     ax,MData_Sel              ;设置提示信息缓冲区指针
                mov     fs,ax
                mov     si,OFFSET Message
                CALL16  CodeC_Sel,DispMsg         ;显示提示信息
                mov     ax,ToLDT_Sel              ;把演示任务的LDT的别名
                mov     gs,ax                     ;段的描述符选择子装入GS
                mov     dx,WORD PTR gs:CodeL.LimitL
                mov     si,OFFSET Buffer          ;取代码段L的段界限值
                mov     cx,4                      ;并转成对应可显示字符串
Vir:            rol     dx,4
                mov     al,dl
                CALL16  CodeC_Sel,HToASCII
                mov     BYTE PTR fs:[si],al
                inc     si
                loop    Vir
                mov     WORD PTR fs:[si],'H'
                mov     si,OFFSET Buffer
                CALL16  CodeC_Sel,DispMsg
                JUMP16  CodeK_Sel,Virtual3
CodeLLen        =       $
Virtual2        ENDP
;----------------------------------------------------------------------------
CodeLSeg        ENDS
;----------------------------------------------------------------------------
CodeKSeg        SEGMENT PARA USE16 'CODEK'
                ASSUME  CS:CodeKSeg
;----------------------------------------------------------------------------
Virtual1        PROC    FAR
                mov     ax,LDT_Sel
                LLDT    ax                        ;加载局部描述符表寄存器LDTR
                mov     ax,Stack_Sel
                mov     ss,ax                     ;建立演示任务堆栈
                mov     sp,OFFSET TopOfS
                JUMP16  CodeL_Sel,Virtual2
Virtual3:       mov     ax,Normal_Sel
                mov     es,ax
                mov     fs,ax
                mov     gs,ax
                mov     ss,ax
                mov     eax,cr0
                and     al,11111110b
                mov     cr0,eax
                JUMP16  <SEG Real>,<OFFSET Real>
CodeKLen        =       $
Virtual1        ENDP
;----------------------------------------------------------------------------
CodeKSeg        ENDS
;============================================================================
RDataSeg        SEGMENT PARA USE16                ;实方式数据段
VGDTR           PDesc   <GDTLen-1,>               ;GDT伪描述符
SPVar           DW      ?                         ;用于保存实方式下的SP
SSVar           DW      ?                         ;用于保存实方式下的SS
RDataSeg        ENDS
;----------------------------------------------------------------------------
RCodeSeg        SEGMENT PARA USE16
                ASSUME  CS:RCodeSeg
;----------------------------------------------------------------------------
Start           PROC
                ASSUME  DS:GDTSeg
                ;-----------------
                mov     ax,GDTSeg
                mov     ds,ax
                ;初始化全局描述符表
                mov     bx,16
                mov     ax,CodeKSeg
                mul     bx
                mov     CodeK.BaseL,ax
                mov     CodeK.BaseM,dl
                mov     CodeK.BaseH,dh
                mov     ax,LDTSeg
                mul     bx
                mov     LDTable.BaseL,ax
                mov     LDTable.BaseM,dl
                mov     LDTable.BaseH,dh
                ;设置GDT伪描述符
                ASSUME  DS:RDataSeg
                mov     ax,RDataSeg
                mov     ds,ax
                mov     ax,GDTSeg
                mul     bx
                mov     WORD PTR VGDTR.Base,ax
                mov     WORD PTR VGDTR.Base+2,dx
                ;初始化演示任务LDT
                cld
                call    Init_MLDT
                ;保存实方式堆栈指针
                mov     SSVar,ss
                mov     SPVar,sp
                ;装载GDTR
                lgdt    QWORD PTR VGDTR
                cli
                ;切换到保护方式
                mov     eax,cr0
                or      al,1
                mov     cr0,eax
                JUMP16  <CodeK_Sel>,<OFFSET Virtual1>
Real:           ;又回到实方式
                mov     ax,RDataSeg
                mov     ds,ax
                lss     sp,DWORD PTR SPVar
                sti
                mov     ax,4c00h
                int     21h
Start           ENDP
;----------------------------------------------------------------------------
Init_MLDT       PROC
                push    ds
                mov     ax,LDTSeg
                mov     ds,ax
                mov     cx,LDNum
                mov     si,OFFSET LDT
InitL:          mov     ax,[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     [si].BaseL,ax
                mov     [si].BaseM,dl
                mov     [si].BaseH,dh
                add     si,SIZE Desc
                loop    InitL
                pop     ds
                ret
Init_MLDT       ENDP
;----------------------------------------------------------------------------
RCodeSeg        ENDS
                END     Start

2.关于实例三的说明

有些步骤的实现方法已在前面的实例中做过介绍，下面就任务内无特权级变换的转移和使用局部 描述符LDT等作些说明：

(1)实模式下初始化LDT

演示任务使用了局部描述符表LDT，本实例中该LDT在实模式下初始化(当然，也可以在使用LDT前 的保护模式初始化)。为了简便，LDT中各描述符的界限和属性值在定义时预置，利用一个子程序 设置各段的段基地址。为方便起见，在定义时把各段的段值安排在相应描述符的段基地址低16位 字段中。由于实例中各段在实模式下定位(这是因为程序是从实模式下启动执行的)，所以把段值 乘以16就是对应的段基地址。

(2)装载LDTR寄存器

在使用LDT之前，还要装载局部描述符表寄存器LDTR。本实例中的如下两条指令用于装载LDTR：
    mov    ax,LDT_SEL
    lldt   ax
LLDT指令是专门用于装载LDTR的指令。该指令的操作数是对应LDT段描述符的选择子。根据该选择子， 处理器从GDT中取出相应的LDT段描述符，在进行合法性等检查后，LDT段描述符的基地址和界限等信息 被装入LDTR的高速缓冲寄存器中。由于要引用GDT，所以不能在实模式下装载LDTR。在“操作系统类指 令”一文中将对LLDT指令作详细说明。

(3)利用段间转移指令JMP实现任务内无特权级的转移

在本实例中进入保护方式后，特权级是0。通过如下段间直接转移指令实现从代码段K到代码段L的转移：
    JUMP16    CodeL_Sel,Virtual2
其中，选择子CodeL_Sel是对应代码段L的描述符的选择子。该描述符在LDT中，所以选择子中的描述 符表指示位TI为1。描述符特权级是0，表示对应代码段的特权级是0，选择子中的请求特权级RPL也 是0。目标代码段不是一致代码段，所以在CPL=DPL，RPL<=DPL的情况下，顺利进行相同特权级的转 移：目标代码段的选择子CodeL_Sel被装入CS，对应描述符中的信息被装入高速缓冲寄存器中，偏移 量Virtual2被装入指令指针寄存器。由于是16位代码段，所以偏移用16位表示。
类似地，通过如下段间直接转移指令实现从代码段L转移到代码段K：
    JUMP16    CodeK_Sel,Virtual3
其中，选择子CodeK_Sel是对应代码段K的描述符选择子。由于描述符在GDT中，所以选择子中的TI位是0。

(4)利用段间调用指令CALL实现任务内无特权级变换的转移

在代码段L中，通过段间直接调用指令CALL调用了在代码段C中的两个子程序，这些调用都是无特权级变 换的转移。例如，利用如下指令调用了显示字符串子程序DispMsg：
    CALL16    CodeC_Sel,DispMsg
其中，CodeC_Sel是代码段C的选择子，DispMsg表示子程序的入口。描述代码段C的描述符在LDT中， 描述符特权级DPL是0，所以使用的选择子CodeC_Sel的请求特权级RPL是0，描述符表指示位TI为1。 目标代码段C不是一致代码段，所以在CPL=DPL，RPL<=DPL的情况下，顺利进行相同特权级的转移： 当前CS和IP压入堆栈，目标代码段的选择子CodeC_Sel被装入CS，对应描述符中的信息被装入高速缓 冲寄存器中，16位偏移DispMsg被装入指令指针IP。由于是16位段，所以偏移用16位表示，压入堆栈 的是字而不是双字。

(5)段间返回指令RET实现任务内无特权级变换的转移

段间返回指令RET从堆栈的栈顶弹出返回地址(由选择子和偏移)构成。弹出选择子内的RPL=CPL，并 且对应DPL=CPL，RPL<=DPL是当然的，所以能顺利进行相同特权级的转移。

3.别名技术

在上述实例三中，使用了两个描述符来描述演示任务的LDT段。段描述符LDTable被安排在GDT中， 它是系统段描述符，把段LDTSeg描述成演示任务的局部描述符表LDT。描述符ToLDT被安排在LDT中， 它是数据段描述符，把段LDTSeg描述成一个普通数据段。描述符LDTable被装载到LDTR，描述 符ToLDT被装载到某个数据段寄存器。为什么要这样处理呢？根据实例三的功能要求，需要访问演示 任务的局部描述符表LDT段，以取得代码段L的段界限值，这需要通过某个段寄存器进行， 但不能把系统段描述符的选择子装载到段寄存器，所以采 用两个描述符来描述段LDTSeg。
这种为了满足对同一个段实施不同方式操作的需要，而用多个描述符加以描述的技术称为别名技术。 在保护方式程序设计中，常常要采用别名技术。例如：用两个具有不同类型值的描述符来描述同一个 段。再如，用两个具有不同DPL的描述符来描述同一个段。

<三>任务内不同特权级的变换

在一个任务内可以存在四种特权级，所以常常会发生不同特权级之间的变换。例如，外层的应用程 序调用内层操作系统的例程，以获得必要的诸如存储器分配等系统服务。内层操作系统的例程完成 后，返回到外层应用程序。
在同一任务内，实现特权级从外层到内层变换的普通途径是使用段间调用指令CALL，通过调用门进 行转移；实现特权级从内层向外层变换的普通途径是使用段间返回指令RET。注意，不能用JMP指令 实现任务内不同特权级的变换。

1.通过调用门的转移

当段间转移指令JMP和段间调用指令CALL所含指针的选择子指示调用门描述符时，就可以实现通过 调用门的转移。但只有CALL指令能变换到内层的特权级，JMP指令只能转移到同级的代码。
调用门描述符转移的入口点包含目标地址的段及偏移量的48位全指针。在执行通过任务门的段间转 移指令JMP或段间调用指令CALL时，指令所含指针内的选择子用 于确定调用门，而偏移被丢弃；把调用门内的48位全指针作为目标地址指针进行转移。
处理器采用与访问数据段相同的特权级规则控制对门描述符的访问。调用门描述符的DPL规定了访 问该门的最外层特权级，在取出调用门内的48位全指针，把它作为目标地址指针向目标代码段转移 之前，要进行特权级检查。只有在相同级或者更内层特权级的程序才可访问调用门，即CPL<=调用 门的DPL。同时，还要求指示门的选择子的RPL必须满足RPL<=调 用门的DPL的条件。检测通过后，才开始向目标代码段转移的步骤。其中还要检测目标描述 符是否为代码段描述符，调用门内的选择子指示的描述符必须是代码段描述符。此外，在装载代码 段描述符高速缓冲寄存器之前调整代码段选择子的RPL=0，也即调用门中代码段选择子的RPL被忽略。
在装载CS高速缓冲寄存器时，还要对目标代码段描述符进行保护检测。检测过程中的DPL不再是调 用门的DPL，而是调用门内选择子所指示的目标代码段描述符的DPL。段间调用指令CALL和段间转移 指令JMP所做的检测不一样。
对于使用调用门的段间转移指令JMP，检测条件与段间直接转移相同。由于已置RPL=0，所以可认为 RPL<=DPL的条件总能满足。所以，对于普通的非一致代码段，当CPL=DPL时，发生无特权级变换的 转移；对于一致代码段，在满足CPL>=DPL时也发生无特权级变换的转移；其它情形就引起异常。
对于使用调用门的段间调用指令CALL，情形就不同了。由于已置RPL=0，所以可认为RPL<=DPL的条 件总能满足。对于一致代码段，在满足CPL>=DPL时发生无特权级变换的转移。对于非一致代码段， 当CPL=DPL时，仍发生无特权级变换的转移；当CPL>DPL时，就发生向内层特权级变换的转移，将调 用门中的选择子和偏移装入CS和指令指针EIP中，并使CPL保持等于DPL，同时切换到内层堆栈。
综上所述，使用段间调用指令CALL，通过调用门可以实现从外层程序调用进入内层程序(JMP指令只 能实现无特权级变换的转移)；通过调用门也可实现无特权级变换的转移。需要注意的是，JMP指令 和CALL指令都不能实现向外层特权级的转移否则会引起异常。
当然，CALL指令在最后把目标代码段的指针装入CS和EIP之前，要把原CS和EIP，即返回地址保存到 堆栈。如无特权级变换，堆栈保持不变，返回地址就保存在原堆栈中；否则，返回地址保存在内层 堆栈中。

2.堆栈切换

在使用CALL指令通过调用门向内层转移时，不仅特权级发生变换，控制转移到一个新的代码段，而 且也切换到内层的堆栈段。从本教程第五篇的任务状态段TSS的格式可见，TSS中包含有指 向0级、1级和2级堆栈的指针。在特权级发生向内层变换时，根据变换到的特权级使用TSS中相应的 堆栈指针对SS及ESP寄存器进行初始化，建立起一个空栈。
在建立起内层堆栈后，处理器先把外层堆栈的指针SS及ESP寄存器的值压入内层堆栈，以使得相应 的向外层返回可恢复原来的外层堆栈。然后，从外层堆栈复制以双字为单位的调用参数到内层堆栈 中，调用门中的DCOUNT字段值决定了复制参数的数量。这些被复制的参数是主程序通过堆栈传递给 子程序的实参，在调用之前被压入外层堆栈。通过复制栈中的参数，使内层的子程序不需要考虑堆 栈的切换，而容易地访问主程序传递过来的实参。最后，调用的返回地址被压入堆栈，以便在调用 结束时返回。下图为在向内层变换时，建立内层堆栈，并从外层堆栈复制2个双字参数到内层堆栈 的示意图。图中每项是双字，可见的段寄存器内的选择子被扩展成32为存入堆栈，高16位为0。对 于16位的使用调用门的段也是如此。
需要注意的是，无论是否通过调用门，只要不发生特权级变换，就不会切换堆栈。

3.向外层返回

与使用CALL指令通过调用门向内层变换相反，使用RET指令实现向外层返回。段间返回指令RET从 堆栈中弹出返回地址，并且可以采用调整ESP的方法，跳过相应的在调用之前压入堆栈的参数。返 回地址的选择子指示要返回的代码段的描述符，从而确定返回的代码段。选择子的RPL确定返回后 的特权级，而不是对应描述符的DPL，这是因为，段间返回指令RET可能使控制返回到一致代码段， 而一致代码段可以在DPL规定的特权级以外的特权级执行。需要注意的是，RET指令所使用的返回 地址的选择子只能使用代码段描述符，而不能使用任何系统段描述符或门描述符，当然，更不能 使用数据段描述符，否则会引起异常。与CALL指令相对应，RET指令也不能向内层返回。
段间返回指令完成返回的步骤如下：
(1)RET指令先从堆栈中弹出返回地址。如果弹出地址的选择子的RPL规定相对于CPL更外层的特权 级，那么就引起向外层返回。
(2)为向外层返回，跳过内层堆栈中的参数，再从内层栈中弹出指向外层堆栈的指针，并装入SS及 ESP，以恢复外层堆栈。
(3)调整ESP，跳过在相应的调用之前压入到外层堆栈的参数。即返回指令不但弹出内层栈的参数， 而且也弹出外层栈的参数。
(4)然后，检查数据段寄存器DS、ES、FS及GS，以保证寻址的段在外层是可访问的，如果段寄存器 寻址的段在外层是不可访问的，那么装入一个空选择子，以避免在返回时发生保护空洞。
(5)返回外层继续执行。
上述五步是对带立即数的段间返回指令而言的，立即数规定了堆栈中要跳过的参数的字节数。对于 无立即数的段间返回指令缺少第二步和第三步。若RET指令不需要向外层返回，那么就只 有(1)和(5)两步。对于有通过堆栈传递参数的子程序，必须使用带立即数的返回指令返回，否则返 回时会装载错误的外层栈指针。
若不使用带立即数的返回指令，可以在返回前把外层栈的栈指针存入内层栈中的用于保存返回地址 上方两个双字的区域中，由外层返回的过程可知，这可正确恢复外层栈的指针，但在外层程序中， 必须人为调整外层栈指针，以便废除在外层栈中压入的参数。在使用C调用约定的程序中可使用此 方法。但这会增加代码的长度和处理时间，使代码效率变低。正因为如此，在Windows 9X下，新增 加了一种STDCALL的调用约定，它正是为了适应Intel系列处理器的体系结构而产生的。

<四>演示任务内特权级变换的实例(实例四)

下面给出一个演示任务内特权级变换的实例。该实例演示在任务内通过调用门从外层特权级变换到 内层特权级；也演示通过段间返回指令从内层特权级变换到外层特权级；还演示通过调用门的无特 权级变换的转移。实例使用了任务状态段TSS，这是因为任务内特权级变换时要使用的内层堆栈指 针存放在TSS中。

1.实现步骤

该实例的实现步骤为：
(1)实方式下初始化；
(2)切换到保护模式；
(3)设置TR和LDTR。由于在任务内发生特权级变换时要切换堆栈，而内层堆栈的指针存放在当前任 务的TSS中，所以在进入保护模式后设置任务状态段寄存器TR。由于演示任务使用了局部描述符表 ，所以设置LDTR；
(4)经调用门进入32位过渡代码段；
(5)建立返回3级演示代码段的环境；
(6)利用RET指令转移到3级的演示代码段。为了演示外层程序通过调用门调用内层程序，要 使CPL>0。实例先通过段间返回指令RET从特权级0变换到特权级3的演示代码段。在特权级3下， 通过调用门调用1级的子程序。随着执行段间返回指令RET，又回到3级的演示代码段；
(7)在3级的演示代码段中，经调用门转移到0级的32位过渡代码段；
(8)直接转0级的临时代码段；
(9)准备返回实模式；
(10)切换回实模式；
(11)实模式下的恢复工作。

2.源程序组织和清单

实例四由如下部分组成：
(1)全局描述符表GDT。GDT含有演示任务的TSS段描述符和LDT段描述符，此外还含有临时代码段 的描述符、规范数据段描述符和视频缓冲区段描述符。
(2)演示任务的LDT段。它含有除临时代码段外的其它代码段的描述符和演示任务各级堆栈段描 述符，还含有3个调用门。
(3)演示任务的TSS段。
(4)演示任务的0级、1级和3级堆栈段。
(5)显示子程序段。32位代码段，特权级1。
(6)演示代码段。32位代码段，特权级3。
(7)过渡代码段。32位段，特权级0。
(8)临时代码段。16位段，特权级0。
(9)实模式下的数据和代码段。
该实例的逻辑功能是显示演示代码段执行时的当前特权级CPL。源程序清单如下：
;名称:ASM4.ASM
;功能:演示任务内有特权级变换的转移
;编译:TASM ASM4.ASM
;连接:TLINK /32 ASM4.OBJ
;----------------------------------------------------------------------------
INCLUDE         386SCD.INC
;----------------------------------------------------------------------------
GDTSeg          SEGMENT PARA USE16                ;全局描述符表数据段(16位)
;----------------------------------------------------------------------------
                ;全局描述符表
GDT             LABEL   BYTE
                ;空描述符
DUMMY           Desc    <>
                ;规范段描述符
Normal          Desc    <0ffffh,,,ATDW,,>
                ;视频缓冲区段描述符(DPL=3)
VideoBuf        Desc    <07fffh,8000h,0bh,ATDW+DPL3,,>
;----------------------------------------------------------------------------
EFFGDT          LABEL   BYTE
                ;任务状态段TSS描述符
DemoTSS         Desc    <DemoTssLen-1,DemoTSSSeg,,AT386TSS,,>
                ;局部描述符表段的描述符
DemoLDTD        Desc    <DemoLDTLen-1,DemoLDTSeg,,ATLDT,,>
                ;临时代码段描述符
TempCode        Desc    <0ffffh,TempCodeSeg,,ATCE,,>
;----------------------------------------------------------------------------
GDTLen          =       $-GDT                     ;全局描述符表长度
GDNum           =       ($-EFFGDT)/(SIZE Desc)    ;需特殊处理的描述符数
;----------------------------------------------------------------------------
Normal_Sel      =       Normal-GDT                ;规范段描述符选择子
Video_Sel       =       VideoBuf-GDT              ;视频缓冲区段描述符选择子
;----------------------------------------------------------------------------
DemoTSS_Sel     =       DemoTSS-GDT               ;任务状态段描述符选择子
DemoLDT_Sel     =       DemoLDTD-GDT              ;局部描述符表段的选择子
TempCode_Sel    =       TempCode-GDT              ;临时代码段的选择子
;----------------------------------------------------------------------------
GDTSeg          ENDS                              ;全局描述符表段定义结束
;----------------------------------------------------------------------------
DemoLDTSeg      SEGMENT PARA USE16                ;局部描述符表数据段(16位)
;----------------------------------------------------------------------------
DemoLDT         LABEL   BYTE                      ;局部描述符表
                ;0级堆栈段描述符(32位段)
DemoStack0      Desc    <DemoStack0Len-1,DemoStack0Seg,,ATDW+DPL0,D32,>
                ;1级堆栈段描述符(32位段)
DemoStack1      Desc    <DemoStack1Len-1,DemoStack1Seg,,ATDW+DPL1,D32,>
                ;3级堆栈段描述符(16位段)
DemoStack3      Desc    <DemoStack3Len-1,DemoStack3Seg,,ATDW+DPL3,,>
                ;代码段描述符(32位段,DPL=3)
DemoCode        Desc    <DemoCodeLen-1,DemoCodeSeg,,ATCE+DPL3,D32,>
                ;过渡代码段描述符(32位段)
T32Code         Desc    <T32CodeLen-1,T32CodeSeg,,ATCE,D32,>
                ;显示子程序代码段描述符(32位段,DPL=1)
EchoSubR        Desc    <EchoSubRLen-1,EchoSubRSeg,,ATCER+DPL1,D32,>
;----------------------------------------------------------------------------
DemoLDNum       =       ($-DemoLDT)/(SIZE Desc)
;----------------------------------------------------------------------------
                ;0级堆栈描述符选择子(RPL=0)
DemoStack0_Sel  =       DemoStack0-DemoLDT+TIL+RPL0
                ;1级堆栈描述符选择子(RPL=1)
DemoStack1_Sel  =       DemoStack1-DemoLDT+TIL+RPL1
                ;3级堆栈描述符选择子(RPL=3)
DemoStack3_Sel  =       DemoStack3-DemoLDT+TIL+RPL3
                ;代码段描述符选择子(RPL=3)
DemoCode_Sel    =       DemoCode-DemoLDT+TIL+RPL3
                ;过渡代码段描述符选择子
T32Code_Sel     =       T32Code-DemoLDT+TIL
                ;显示子程序代码段描述符选择子(RPL=1)
Echo_Sel1       =       EchoSubR-DemoLDT+TIL+RPL1
                ;显示子程序代码段描述符选择子(RPL=3)
Echo_Sel3       =       EchoSubR-DemoLDT+TIL+RPL3
;----------------------------------------------------------------------------
                ;指向过渡代码段内T32Begin点的调用门(DPL=0)
ToT32GateA      Gate    <T32Begin,T32Code_Sel,,AT386CGate,>
                ;指向过渡代码段内T32End点的调用门(DPL=3)
ToT32GateB      Gate    <T32End,T32Code_Sel,,AT386CGate+DPL3,>
                ;指向显示子程序代码段的调用门(DPL=3)
ToEchoGate      Gate    <EchoSub,Echo_Sel3,,AT386CGate+DPL3,>
;----------------------------------------------------------------------------
DemoLDTLen      =       $-DemoLDT
;----------------------------------------------------------------------------
                ;指向过渡代码段内T32Begin点的调用门的选择子
ToT32A_Sel      =       ToT32GateA-DemoLDT+TIL
                ;指向过渡代码段内T32End点的调用门的选择子
ToT32B_Sel      =       ToT32GateB-DemoLDT+TIL
                ;显示子程序调用门的选择子
ToEcho_Sel      =       ToEchoGate-DemoLDT+TIL
;----------------------------------------------------------------------------
DemoLDTSeg      ENDS                              ;局部描述符表段定义结束
;----------------------------------------------------------------------------
DemoTSSSeg      SEGMENT PARA USE16                ;任务状态段TSS
;----------------------------------------------------------------------------
                DD      0                         ;Back
                DD      DemoStack0Len             ;0级堆栈指针
                DD      DemoStack0_Sel            ;初始化
                DD      DemoStack1Len             ;1级堆栈指针
                DD      DemoStack1_Sel            ;初始化
                DD      0                         ;2级堆栈指针
                DD      0                         ;未初始化
                DD      0                         ;CR3
                DD      0                         ;EIP
                DD      0                         ;EFLAGS
                DD      0                         ;EAX
                DD      0                         ;ECX
                DD      0                         ;EDX
                DD      0                         ;EBX
                DD      0                         ;ESP
                DD      0                         ;EBP
                DD      0                         ;ESI
                DD      0                         ;EDI
                DD      0                         ;ES
                DD      0                         ;CS
                DD      0                         ;SS
                DD      0                         ;DS
                DD      0                         ;FS
                DD      0                         ;GS
                DD      DemoLDT_Sel               ;LDT
                DW      0                         ;调试陷阱标志
                DW      $+2                       ;指向I/O许可位图
                DW      0ffffh                    ;I/O许可位图结束标志
;----------------------------------------------------------------------------
DemoTSSLen      =       $
;----------------------------------------------------------------------------
DemoTSSSeg      ENDS                              ;任务状态段TSS结束
;----------------------------------------------------------------------------
DemoStack0Seg   SEGMENT DWORD STACK USE32         ;0级堆栈段(32位段)
DemoStack0Len   =       512
                DB      DemoStack0Len DUP(?)
DemoStack0Seg   ENDS                              ;0级堆栈段结束
;----------------------------------------------------------------------------
DemoStack1Seg   SEGMENT DWORD STACK USE32         ;1级堆栈段(32位段)
DemoStack1Len   =       512
                DB      DemoStack1Len DUP(?)
DemoStack1Seg   ENDS                              ;1级堆栈段结束
;----------------------------------------------------------------------------
DemoStack3Seg   SEGMENT DWORD STACK USE16         ;3级堆栈段(16位段)
DemoStack3Len   =       512
                DB      DemoStack3Len DUP(?)
DemoStack3Seg   ENDS                              ;3级堆栈段结束
;----------------------------------------------------------------------------
EchoSubRSeg     SEGMENT PARA USE32                ;显示子程序代码段(32位,1级)
                ASSUME  CS:EchoSubRSeg
;----------------------------------------------------------------------------
Message         DB      'CPL=',0                  ;显示信息(该代码段可读)
;----------------------------------------------------------------------------
EchoSub         PROC    FAR
                cld
                push    ebp
                mov     ebp,esp
                mov     ax,Echo_Sel1              ;该代码段是可读段
                mov     ds,ax                     ;采用RPL=1的选择子
                mov     ax,Video_Sel
                mov     es,ax
                mov     edi,1996
                mov     esi,OFFSET Message
                mov     ah,4eh                    ;置显示属性(红底黄字)
EchoSub1:       lodsb
                or      al,al
                jz      EchoSub2
                stosw
                jmp     EchoSub1
EchoSub2:       mov     eax,[ebp+8]               ;从堆栈中取调用程序的选择子
                and     al,3                      ;调用程序的CPL在CS的RPL字段
                add     al,'0'
                mov     ah,4eh                    ;置显示属性(红底黄字)
                stosw
                pop     ebp
                retf
EchoSub         ENDP
;----------------------------------------------------------------------------
EchoSubRLen     =       $
;----------------------------------------------------------------------------
EchoSubRSeg     ENDS                              ;显示子程序代码段结束
;----------------------------------------------------------------------------
DemoCodeSeg     SEGMENT PARA USE32                ;32位代码段(3级)
                ASSUME  CS:DemoCodeSeg
;----------------------------------------------------------------------------
DemoBegin       PROC    FAR
                CALL32  ToEcho_Sel,0              ;显示当前特权级(变换到1级)
                CALL32  ToT32B_Sel,0              ;转到过渡代码段(变换到0级)
DemoBegin       ENDP
DemoCodeLen     =       $
;----------------------------------------------------------------------------
DemoCodeSeg     ENDS                              ;32位代码段结束
;----------------------------------------------------------------------------
T32CodeSeg      SEGMENT PARA USE32                ;32位过渡代码段(0级)
                ASSUME  CS:T32CodeSeg
;----------------------------------------------------------------------------
T32Begin        PROC    FAR
                mov     ax,DemoStack0_Sel         ;建立0级堆栈
                mov     ss,ax
                mov     esp,DemoStack0Len
                push    DWORD PTR DemoStack3_Sel  ;压入3级堆栈指针
                push    DemoStack3Len
                push    DWORD PTR DemoCode_SEL    ;压入入口点
                push    OFFSET DemoBegin
                retf                              ;利用RET实现转3级的演示代码
T32Begin        ENDP
;----------------------------------------------------------------------------
T32End          PROC    FAR
                JUMP32  TempCode_Sel,<OFFSET ToReal>
T32End          ENDP
T32CodeLen      =       $
;----------------------------------------------------------------------------
T32CodeSeg      ENDS
;----------------------------------------------------------------------------
TempCodeSeg     SEGMENT PARA USE16                ;16位临时代码段(0级)
                ASSUME  CS:TempCodeSeg
;----------------------------------------------------------------------------
Virtual         PROC    FAR
                mov     ax,DemoTSS_Sel            ;装载TR
                ltr     ax
                mov     ax,DemoLDT_Sel            ;装载LDTR
                lldt    ax
                JUMP16  ToT32A_Sel,0              ;通过调用门转过渡段
ToReal:         mov     ax,Normal_Sel             ;准备切换回实模式
                mov     ds,ax
                mov     es,ax
                mov     fs,ax
                mov     gs,ax
                mov     ss,ax
                mov     eax,cr0
                and     al,11111110b
                mov     cr0,eax
                JUMP16  <SEG Real>,<OFFSET Real>
Virtual         ENDP
TempCodeLen     =       $
;----------------------------------------------------------------------------
TempCodeSeg     ENDS
;============================================================================
RDataSeg        SEGMENT PARA USE16                ;实方式数据段
VGDTR           PDesc   <GDTLen-1,>               ;GDT伪描述符
SPVar           DW      ?                         ;用于保存实方式下的SP
SSVar           DW      ?                         ;用于保存实方式下的SS
RDataSeg        ENDS
;----------------------------------------------------------------------------
RCodeSeg        SEGMENT PARA USE16
                ASSUME  CS:RCodeSeg,DS:RDataSeg
;----------------------------------------------------------------------------
Start           PROC
                mov     ax,RDataSeg
                mov     ds,ax
                cld
                CALL    InitGDT                   ;初始化全局描述符表GDT
                mov     ax,DemoLDTSeg
                mov     fs,ax
                mov     si,OFFSET DemoLDT
                mov     cx,DemoLDNum
                CALL    InitLDT                   ;初始化局部描述符表LDT
                mov     SSVar,ss
                mov     SPVar,sp
                lgdt    QWORD PTR VGDTR           ;装载GDTR并切换到保护方式
                cli
                mov     eax,cr0
                or      al,1
                mov     cr0,eax
                JUMP16  <TempCode_Sel>,<OFFSET Virtual>
Real:           mov     ax,RDataSeg
                mov     ds,ax
                lss     sp,DWORD PTR SPVar        ;又回到实方式
                sti
                mov     ax,4c00h
                int     21h
Start           ENDP
;----------------------------------------------------------------------------
InitGDT         PROC
                push    ds
                mov     ax,GDTSeg
                mov     ds,ax
                mov     cx,GDNum
                mov     si,OFFSET EFFGDT
InitG:          mov     ax,[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR [si].BaseL,ax
                mov     BYTE PTR [si].BaseM,dl
                mov     BYTE PTR [si].BaseH,dh
                add     si,SIZE Desc
                loop    InitG
                pop     ds
                mov     bx,16
                mov     ax,GDTSeg
                mul     bx
                mov     WORD PTR VGDTR.Base,ax
                mov     WORD PTR VGDTR.Base+2,dx
                ret
InitGDT         ENDP
;----------------------------------------------------------------------------
;入口参数:FS:SI=第一个要初始化的描述符,CX=要初始化的描述符数
;----------------------------------------------------------------------------
InitLDT         PROC
ILDT:           mov     ax,WORD PTR FS:[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR fs:[si].BaseL,ax
                mov     BYTE PTR fs:[si].BaseM,dl
                mov     BYTE PTR fs:[si].BaseH,dh
                add     si,SIZE Desc
                loop    ILDT
                ret
InitLDT         ENDP
;----------------------------------------------------------------------------
RCodeSeg        ENDS
                END     Start

3.关于实例四的说明

程序中部分片段的背景和实现方法在前面的实例中做过介绍，下面主要就如何实现任务内特权级变换 做些说明：

(1)通过段间返回指令实现特权级变换

实例在两处使用段间返回指令实现任务内的特权级变换。一处是在0级的过渡代码段中用段间RET指令 从特权级0变换到特权级3的演示代码段。该处RET指令并不对应CALL指令。实例从实模式切换到保护 模式后CPL=0。为了演示如何通过调用门调用内层程序，要设法使CPL>0。为此，实例先建立一个已发 生的从外层到内层变换的环境，即按上图所示在当前堆栈(0级堆栈)中放入外层堆栈的指针和外层演 示程序的入口指针，形成一个如下图所示的0级堆栈，无需传递参数。然后，执行段间返回指令RET， 从堆栈中弹出3级演示代码段的选择子，RPL=3，而当时CPL=0，所以导致向外层变换特权级，从0级的 过渡代码段变换到3级的演示代码段，同时切换到3级堆栈。
另一处是从1级的显示子程序EchoSub返回到3级的演示程序段。该处的RET指令与演示程序中使用的通 过调用门的段间调用指令CALL相对应，执行段间返回指令RET时的1级堆栈也如上图所示，其中的返回 地址和外层栈指针由CALL指令压入。

(2)通过调用门实现特权级变换

实例在两处使用了段间调用指令，通过调用门实现特权级的变换。一处是3级演示代码通过调用 门ToEchoGate调用1级的显示子程序。调用门ToEchoGate自身的DPL=3，只有这样，3级的演示代码才 能够使用该调用门。由于调用门内的选择子Echo_Sel3所指示的显示子程序代码段描述符DPL=1，而当 时CPL=3，所以引起从外层特权级向内层特权级的变换，使CPL=1。同时形成如上图所示的1级堆栈。 虽然调用门内的选择子Echo_Sel3的RPL=3，大于目标代码段的DPL，但没有关系，因为在通过调用门 转移时，门内指示目标代码段的选择子RPL总被作0对待。
另一处是3级演示代码还通过调用门ToT32GateB调用了0级的过渡代码。该处使用的调用门描述符DPL也 等于3。由于调用门内的选择子T32Code_Sel所指示的过渡代码段描述符的DPL=0，而当时CPL=3，所以 引起从3特权级向0特权级的变换，使CPL=0。同时形成如上图所示的0级堆栈。但该处的调用实际上是 “有去无回”的，调用的目的是转移到0级的过渡代码，准备返回实模式。由于从3级的演示代码到0级 的过渡代码要发生特权级变换，所以不能使用转移指令JMP，必须使用调用指令CALL。

(3)通过调用门实现无特权级变换

在临时代码段中，使用调用门ToT32GateA转移到过渡代码段。尽管调用门内的选择子T32Code_Sel所指 示的过渡代码段描述符的DPL=0，但当时CPL=0，所以不发生特权级变换。正是这个原因，才可以使用 段间转移指令JMP。

(4)子程序EchoSub的实现

子程序EchoSub的功能是显示调用程序执行时的特权级。调用程序的执行特权级在代码段寄存器CS内选 择子的RPL字段，在调用EchoSub时，CS寄存器的内容被压入堆栈。子程序从堆栈取得调用程序的代码 段选择子，再从中分离出RPL就可得调用程序的执行特权级。

(5)装载任务状态段寄存器TR

在任务内发生特权级变换时堆栈也随着自动切换，外层堆栈指针保存在内层堆栈中，而内层堆栈指针 存放在当前任务的TSS中。所以，在从外层向内层变换时，要访问TSS(从内层向外层转移时不需要访 问TSS，而只需内层栈中保存的栈指针)。实例在进入保护模式下的临时代码段后，通过如下两条指令 装载任务状态段寄存器TR，使其指向已预置好的任务的TSS：
    mov     ax,DemoTSS_Sel
    ltr     ax
LTR指令是专门用于装载任务状态段寄存器TR的指令。该指令的操作数是对应TSS段描述符的选择 子。LTR指令从GDT中取出相应的TSS段描述符，把TSS段描述符的基地址和界限等信息装入TR的高速 缓冲寄存器中。

<五>任务切换

利用段间转移指令JMP或者段间调用指令CALL，通过任务门或直接通过任务状态段，可以切换到别的 任务。此外，在中断/异常或者执行IRET指令时也可能发生任务切换。需要注意的是，因为RET指令 的目标地址只能使用代码段描述符，所以，不能通过RET指令实现任务切换。

1.直接通过TSS进行任务切换

段间转移指令JMP或段间调用指令CALL所含指针的选择子指示一个可用任务状态段TSS描述符时， 正常情况下就发生从当前任务到由该可用TSS对应任务(目标任务)的切换。目标任务的入口点由 目标任务TSS内的CS和EIP字段所规定的指针确定。这样的JMP或CALL指令内的偏移被丢弃。另外， 对于段间调用指令CALL，若目标选择子指示一TSS段描述符或任务门时，则返回地址和外层栈指 针并不压入堆栈。
处理器采用与访问数据段相同的特权级规则控制对TSS段描述符的访问。TSS段描述符的DPL规定 了访问该描述符的最外层特权级，只有在相同级别或更内层级别的程序才可以访问它。同时，还 要求指示它的选择子的RPL必须满足RPL<=TSS的DPL的条件。当这些条件满足时，就开始任务切换。

2.通过任务门进行任务切换

任务门内的选择子指示某个任务的TSS描述符。当段间转移指令JMP或段间调用指令CALL所含指针 的选择子指示一个任务门时，正常情况下就发生任务切换，即从当前任务切换到由任务门内的选 择子所指示的TSS描述符对应的任务(目标任务)。这样的JMP或CALL指令内的偏移被丢弃；任务门 内的偏移也无意义。
处理器采用与访问数据段相同的特权级规则控制对任务门的访问。任务门的DPL规定了访问该任 务门的最外层特权级，只有在同级或更内层级别的程序才可以访问它。同时，还要求指示任务门 的选择子RPL必须满足RPL<=任务门的DPL的条件。在这些条件满足时，再检查任务门内的选择子， 要求该选择子指示GDT中的可用的TSS描述符。对于任务门所指向的TSS描述符的DPL不进行特权级 检查。检查通过以后，就开始任务切换。

3.任务切换过程

根据指示目标任务TSS描述符的选择子进行任务切换的一般过程如下：
第一，测试目标任务状态段的界限。TSS用于保存任务的各种状态信息，不同的任务，TSS中可以 有数量不等的其他信息，根据任务状态段的基本格式，TSS的界限应大于或等于103(104-1)。
第二，把寄存器现场保存到当前任务的TSS。把通用寄存器、段寄存器、EIP及EFLAGS的当前值保 存到当前TSS中。保存的EIP值是返回地址，指向引起任务切换指令的下一条指令。但不 把LDTR和CR3的内容保存到TSS中。
第三，把指示目标任务TSS的选择子装入TR寄存器中。同时把对应TSS的描述符装入TR的高速缓冲 寄存器中。此后，当前任务改称为原任务，目标任务改称为当前任务。
第四，基本恢复当前任务(目标任务)的寄存器现场。根据保存在TSS中的内容，恢复各通用寄存 器、段寄存器、EFLAGS及EIP。在装入寄存器的过程中，为了能正确地处理可能发生的异常，只 把对应选择子装入各段寄存器。此时选择子的P位为0。还装载CR3寄存器。
第五，进行链接处理。如果需要链接，那么将指向原任务TSS的选择子写入当前任务TSS的链接字 段，把当前任务TSS描述符类型改为“忙”(并不修改原任务状态段描述符的“忙”位)，并将标 志寄存器EFLAGS中的NT位置1，表示是嵌套任务。如果需要解链，那么把原任务TSS描述符类型改 为“可用”。如果无解链处理，那么将原任务TSS描述符类型置为“可用”，当前任务TSS描述符 类型置为“忙”。由于JMP指令引起的任务切换不实施链接/解链处理；由CALL指令、中断、IRET指 令引起的任务切换要实施链接/解链处理。
第六，把CR0中的TS标志置为1，这表示已发生过任务切换，在当前任务使用协处理器指令时，产生 自陷。由自陷处理程序完成有关协处理器现场的保存和恢复。这有利于快速地进行任务切换。
第七，把TSS中的CS选择子的RPL作为当前任务特权级设置为CPL。又因为装入CS高速缓冲寄存器时 要检测CPL=代码段描述符的DPL，所以TSS中的选择子所指示的代码段描述符的DPL必须等于该选择 子的RPL。任务切换可以在一个任务的任何特权级发生，并且可以切换到另一任务的任何特权级。
第八，装载LDTR寄存器。一个任务可以有自己的LDT，也可以没有。当任务没有LDT时，TSS中LDT选 择子为0。如果TSS中LDT选择子非空，则从GDT中读出对应的LDT描述符，在经过测试后，把所读 的LDT描述符装入LDTR高速缓冲寄存器。如果LDT选择子为空，则将LDT的存在位置为0，表明任务不 使用LDT。
第九，装载代码段寄存器CS、堆栈段寄存器SS和各数据段寄存器及其高速缓冲寄存器。在装入代码 段高速缓存之前，也要进行特权检查，处理器调整TSS中的CS选择子的RPL=0，装入之后，调 整CS的RPL等于目标代码段的DPL。堆栈段使用的是TSS中的SS和SP字段的值，而不是使用内层栈保 存区中的指针，即使发生了向内层特权级的变换。这与任务内的通过调用门的转移不同。
第十，把调试寄存器DR7中的局部启用位置为0，以清除局部于原任务的各个断点和方式。

4.关于任务状态和嵌套的说明

需要注意的是，任务切换不能递归。
在段间转移指令JMP引起任务切换时，不实施链接，不导致任务的嵌套。它要求目标任务是可用任 务。切换过程中把原任务置为“可用”，目标任务置为“忙”。
在段间调用指令CALL引起任务切换时，实施链接，导致任务的嵌套。它要求目标任务是可用的任务。 在切换过程中把目标任务置为“忙”，原任务仍保持“忙”；标志寄存器EFLAGS中的NT位被置为1， 表示任务是嵌套任务。
在由中断异常引起任务切换时，实施链接，导致任务的嵌套。要求目标任务是可用的任务。在切换 过程中把目标任务置为“忙”，原任务仍保持“忙”；标志寄存器EFLAGS中的NT位被置为1，表示任 务是嵌套任务。
在执行IRET指令时引起任务切换，那么实施解链。要求目标任务是忙的任务。在切换过程中把原任 务置为“可用”，目标任务仍保持“忙”。关于中断/异常如何引起任务切换和指令IRET如何考虑任 务切换的内容将在后面的文章中论述。

<六>演示任务切换的实例(实例五)

下面给出一个用于演示任务切换的实例。该实例的逻辑功能是在切换后显示原任务的挂起点(EIP)的 值。该实例演示内容包括：直接通过TSS段的任务切换，通过任务门的任务切换，任务内特权级的变 换及参数传递。

1.实现步骤

为了达到演示任务切换和特权级变换的目的，实例五在保护方式下涉及到两个任务，一个任务称为 临时任务，另一个任务称为演示任务。演示任务的功能是演示通过调用门实现特权级的变换和堆栈 间参数的自动复制。临时任务和演示任务配合展示任务切换。实例五的主要实现步骤如下：
(1)实模式下初始化；
(2)切换到保护模式；
(3)设置TR对应临时任务，特权级为0；
(4)直接切换到演示任务，演示任务的特权级为2；
(5)把入口参数压入堆栈，经调用门进入显示信息子程序，显示信息子程序的特权级为0；
(6)从堆栈中取出入口参数并处理；
(7)从显示信息子程序返回特权级为2的演示代码段；
(8)为切换回实模式作部分准备工作；
(9)经任务门切换到特权级为0的临时任务；
(10)准备返回实模式；
(11)切换到实模式；
(12)实模式下的恢复工作。
在任务切换时，把原任务的现场保存到TR所指示的TSS内，然后再把指向目标任务的TSS描述符的 选择子装入TR，所以在从临时任务切换到演示任务之前，要把指向临时任务TSS描述符的选择子 装入TR。通过把演示任务的TSS初始化成恢复点在特权级2的代码段，使得从临时任务切换到演示 任务后，当前特权级CPL=2。

2.源程序组织和清单

实例五由如下部分组成：
(1)全局描述符表GDT。GDT含有演示任务的TSS描述符和LDT段描述符，还含有临时任务TSS描述符 和临时任务的代码段描述符，此外，还含有子程序代码段描述符、规范数据段描述符和视频缓冲 区段描述符。
(2)演示任务的TSS。以根据演示要求初始化。
(3)演示任务的LDT段。它含有演示任务的0级和2级堆栈段描述符、代码段和数据段描述符、分别 以数据段方式描述LDT和临时任务TSS的数据段描述符、以及指向子程序的调用门和指向临时任务 的任务门。
(4)演示任务的0级和2级堆栈段。32位段，特权级分别为0和2。
(5)演示任务数据段。32位段，特权级3。
(6)子程序代码段。32位代码段，特权级0。
(7)演示任务代码段。32位代码段，特权级2。
(8)临时任务的TSS段。未初始化。
(9)临时任务代码段。16位代码段，特权级0。
(10)实模式下的数据和代码段。
该实例的逻辑功能是在切换后显示原任务的挂起点(EIP)的值。源程序清单如下：
;名称:ASM5.ASM
;功能:演示任务切换和任务内有特权级变换的转移
;编译:TASM ASM5.ASM
;链接:TLINK /32 ASM5.OBJ
;----------------------------------------------------------------------------
INCLUDE         386SCD.INC
;----------------------------------------------------------------------------
GDTSeg          SEGMENT PARA USE16                ;全局描述符表数据段(16位)
;----------------------------------------------------------------------------
                ;全局描述符表
GDT             LABEL   BYTE
                ;空描述符
DUMMY           Desc    <>
                ;规范段描述符及选择子
Normal          Desc    <0ffffh,,,ATDW,,>
Normal_Sel      =       Normal-GDT
                ;视频缓冲区段描述符(DPL=3)及选择子
VideoBuf        Desc    <0ffffh,8000h,0bh,ATDW+DPL3,,>
Video_Sel       =       VideoBuf-GDT
;----------------------------------------------------------------------------
EFFGDT          LABEL   BYTE
                ;演示任务的局部描述符表段的描述符及选择子
DemoLDTab       Desc    <DemoLDTLen-1,DemoLDTSeg,,ATLDT,,>
DemoLDT_Sel     =       DemoLDTab-GDT
                ;演示任务的任务状态段描述符及选择子
DemoTSS         Desc    <DemoTSSLen-1,DemoTSSSeg,,AT386TSS,,>
DemoTSS_Sel     =       DemoTSS-GDT
                ;临时任务的任务状态段描述符及选择子
TempTSS         Desc    <TempTSSLen-1,TempTSSSeg,,AT386TSS+DPL2,,>
TempTSS_Sel     =       TempTSS-GDT
                ;临时代码段描述符及选择子
TempCode        Desc    <0ffffh,TempCodeSeg,,ATCE,,>
TempCode_Sel    =       TempCode-GDT
                ;子程序代码段描述符及选择子
SubR            Desc    <SubRLen-1,SubRSeg,,ATCE,D32,>
SubR_Sel        =       SubR-GDT
;----------------------------------------------------------------------------
GDNum           =       ($-EFFGDT)/(SIZE Desc)    ;需处理基地址的描述符个数
GDTLen          =       $-GDT                     ;全局描述符表长度
;----------------------------------------------------------------------------
GDTSeg          ENDS                              ;全局描述符表段定义结束
;----------------------------------------------------------------------------
DemoLDTSeg      SEGMENT PARA USE16                ;局部描述符表数据段(16位)
;----------------------------------------------------------------------------
DemoLDT         LABEL   BYTE                      ;局部描述符表
                ;0级堆栈段描述符(32位段)及选择子
DemoStack0      Desc    <DemoStack0Len-1,DemoStack0Seg,,ATDW,D32,>
DemoStack0_Sel  =       DemoStack0-DemoLDT+TIL
                ;2级堆栈段描述符(32位段)及选择子
DemoStack2      Desc    <DemoStack2Len-1,DemoStack2Seg,,ATDW+DPL2,D32,>
DemoStack2_Sel  =       DemoStack2-DemoLDT+TIL+RPL2
                ;演示任务代码段描述符(32位段,DPL=2)及选择子
DemoCode        Desc    <DemoCodeLen-1,DemoCodeSeg,,ATCE+DPL2,D32,>
DemoCode_Sel    =       DemoCode-DemoLDT+TIL+RPL2
                ;演示任务数据段描述符(32位段,DPL=3)及选择子
DemoData        Desc    <DemoDataLen-1,DemoDataSeg,,ATDW+DPL3,D32,>
DemoData_Sel    =       DemoData-DemoLDT+TIL
                ;把LDT作为普通数据段描述的描述符(DPL=2)及选择子
ToDLDT          Desc    <DemoLDTLen-1,DemoLDTSeg,,ATDW+DPL2,,>
ToDLDT_Sel      =       ToDLDT-DemoLDT+TIL
                ;把TSS作为普通数据段描述的描述符(DPL=2)及选择子
ToTTSS          Desc    <TempTSSLen-1,TempTSSSeg,,ATDW+DPL2,,>
ToTTSS_Sel      =       ToTTSS-DemoLDT+TIL
;----------------------------------------------------------------------------
DemoLDNum       =       ($-DemoLDT)/(SIZE Desc)   ;需处理基地址的LDT描述符数
;----------------------------------------------------------------------------
                ;指向子程序SubRB代码段的调用门(DPL=3)及选择子
ToSubR          Gate    <SubRB,SubR_Sel,,AT386CGate+DPL3,>
ToSubR_Sel      =       ToSubR-DemoLDT+TIL+RPL2
                ;指向临时任务Temp的任务门(DPL=3)及选择子
ToTempT         Gate    <,TempTSS_Sel,,ATTaskGate+DPL3,>
ToTempT_Sel     =       ToTempT-DemoLDT+TIL
;----------------------------------------------------------------------------
DemoLDTLen      =       $-DemoLDT
;----------------------------------------------------------------------------
DemoLDTSeg      ENDS                              ;局部描述符表段定义结束
;----------------------------------------------------------------------------
DemoTSSSeg      SEGMENT PARA USE16                ;任务状态段TSS
;----------------------------------------------------------------------------
                DD      0                         ;链接字
                DD      DemoStack0Len             ;0级堆栈指针
                DW      DemoStack0_Sel,0          ;0级堆栈选择子
                DD      0                         ;1级堆栈指针(实例不使用)
                DW      0,0                       ;1级堆栈选择子(实例不使用)
                DD      0                         ;2级堆栈指针
                DW      0,0                       ;2级堆栈选择子
                DD      0                         ;CR3
                DW      DemoBegin,0               ;EIP
                DD      0                         ;EFLAGS
                DD      0                         ;EAX
                DD      0                         ;ECX
                DD      0                         ;EDX
                DD      0                         ;EBX
                DD      DemoStack2Len             ;ESP
                DD      0                         ;EBP
                DD      0                         ;ESI
                DD      1986                      ;EDI
                DW      Video_Sel,0               ;ES
                DW      DemoCode_Sel,0            ;CS
                DW      DemoStack2_Sel,0          ;SS
                DW      DemoData_Sel,0            ;DS
                DW      ToDLDT_Sel,0              ;FS
                DW      ToTTSS_Sel,0              ;GS
                DW      DemoLDT_Sel,0             ;LDTR
                DW      0                         ;调试陷阱标志
                DW      $+2                       ;指向I/O许可位图
                DB      0ffh                      ;I/O许可位图结束标志
DemoTSSLen      =       $
;----------------------------------------------------------------------------
DemoTSSSeg      ENDS                              ;任务状态段TSS结束
;----------------------------------------------------------------------------
DemoStack0Seg   SEGMENT PARA USE32                ;演示任务0级堆栈段(32位段)
DemoStack0Len   =       1024
                DB      DemoStack0Len DUP(0)
DemoStack0Seg   ENDS                              ;演示任务0级堆栈段结束
;----------------------------------------------------------------------------
DemoStack2Seg   SEGMENT PARA USE32               ;演示任务2级堆栈段(32位段)
DemoStack2Len   =       512
                DB      DemoStack2Len DUP(0)
DemoStack2Seg   ENDS                              ;演示任务2级堆栈段结束
;----------------------------------------------------------------------------
DemoDataSeg     SEGMENT PARA USE32                ;演示任务数据段(32位段)
Message         DB      'Value=',0
DemoDataLen     =       $
DemoDataSeg     ENDS                              ;演示任务数据段结束
;----------------------------------------------------------------------------
SubRSeg         SEGMENT PARA USE32                ;子程序代码段(32位)
                ASSUME  CS:SubRSeg
;----------------------------------------------------------------------------
SubRB           PROC    FAR
                push    ebp
                mov     ebp,esp
                pushad                            ;保护现场
                mov     esi,DWORD PTR [ebp+12]    ;从0级栈中取出显示串偏移
                mov     ah,4ah                    ;设置显示属性
                jmp     SHORT SubR2
SubR1:          stosw
SubR2:          lodsb
                or      al,al
                jnz     SubR1
                mov     ah,4eh                    ;设置显示属性
                mov     edx,DWORD PTR [ebp+16]    ;从0级栈中取出显示值
                mov     ecx,8
SubR3:          rol     edx,4
                mov     al,dl
                call    HToASCII
                stosw
                loop    SubR3
                popad
                pop     ebp
                ret     8
SubRB           ENDP
;----------------------------------------------------------------------------
HToASCII        PROC
                and     al,0fh
                add     al,90h
                daa
                adc     al,40h
                daa
                ret
HToASCII        ENDP
;----------------------------------------------------------------------------
SubRLen         =       $
SubRSeg         ENDS                              ;子程序代码段结束
;----------------------------------------------------------------------------
DemoCodeSeg     SEGMENT PARA USE32                ;演示任务的32位代码段
                ASSUME  CS:DemoCodeSeg,DS:DemoDataSeg
;----------------------------------------------------------------------------
DemoBegin       PROC    FAR
                ;把要复制的参数个数置入调用门
                mov     BYTE PTR fs:ToSubR.DCount,2
                ;向2级堆栈中压入参数
                push    DWORD PTR gs:TempTask.TREIP
                push    OFFSET Message
                ;通过调用门调用SubRB
                CALL32  ToSubR_Sel,0
                ;把指向规范数据段描述符的选择子填入临时任务TSS
                ASSUME  DS:TempTSSSeg
                push    gs
                pop     ds
                mov     ax,Normal_Sel
                mov     WORD PTR TempTask.TRDS,ax
                mov     WORD PTR TempTask.TRES,ax
                mov     WORD PTR TempTask.TRFS,ax
                mov     WORD PTR TempTask.TRGS,ax
                mov     WORD PTR TempTask.TRSS,ax
                ;通过任务门切换到临时任务
                JUMP32  ToTempT_Sel,0
                jmp     DemoBegin
DemoBegin       ENDP
DemoCodeLen     =       $
;----------------------------------------------------------------------------
DemoCodeSeg     ENDS                              ;演示任务的32位代码段结束
;----------------------------------------------------------------------------
TempTSSSeg      SEGMENT PARA USE16                ;临时任务的任务状态段TSS
TempTask        TSS     <>
                DB      0ffh                      ;I/O许可位图结束标志
TempTSSLen      =       $
TempTSSSeg      ENDS
;----------------------------------------------------------------------------
TempCodeSeg     SEGMENT PARA USE16                ;临时任务的代码段
                ASSUME  CS:TempCodeSeg
;----------------------------------------------------------------------------
Virtual         PROC    FAR
                mov     ax,TempTSS_Sel            ;装载TR
                ltr     ax
                JUMP16  DemoTSS_Sel,0             ;直接切换到演示任务
                clts                              ;清任务切换标志
                mov     eax,cr0                   ;准备返回实模式
                and     al,11111110b
                mov     cr0,eax
                JUMP16  <SEG Real>,<OFFSET Real>
Virtual         ENDP
;----------------------------------------------------------------------------
TempCodeSeg     ENDS
;============================================================================
RDataSeg        SEGMENT PARA USE16                ;实方式数据段
VGDTR           PDesc   <GDTLen-1,>               ;GDT伪描述符
SPVar           DW      ?                         ;用于保存实方式下的SP
SSVar           DW      ?                         ;用于保存实方式下的SS
RDataSeg        ENDS
;----------------------------------------------------------------------------
RCodeSeg        SEGMENT PARA USE16
                ASSUME  CS:RCodeSeg,DS:RDataSeg,ES:RDataSeg
;----------------------------------------------------------------------------
Start           PROC
                mov     ax,RDataSeg
                mov     ds,ax
                cld
                call    InitGDT                   ;初始化全局描述符表GDT
                mov     ax,DemoLDTSeg
                mov     fs,ax
                mov     si,OFFSET DemoLDT
                mov     cx,DemoLDNum
                call    InitLDT                   ;初始化局部描述符表LDT
                mov     SSVar,ss
                mov     SPVar,sp
                lgdt    QWORD PTR VGDTR           ;装载GDTR并切换到保护方式
                cli
                mov     eax,cr0
                or      al,1
                mov     cr0,eax
                JUMP16  <TempCode_Sel>,<OFFSET Virtual>
Real:           mov     ax,RDataSeg
                mov     ds,ax
                lss     sp,DWORD PTR SPVar        ;又回到实方式
                sti
                mov     ax,4c00h
                int     21h
Start           ENDP
;----------------------------------------------------------------------------
InitGDT         PROC
                push    ds
                mov     ax,GDTSeg
                mov     ds,ax
                mov     cx,GDNum
                mov     si,OFFSET EFFGDT
InitG:          mov     ax,[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR [si].BaseL,ax
                mov     BYTE PTR [si].BaseM,dl
                mov     BYTE PTR [si].BaseH,dh
                add     si,SIZE Desc
                loop    InitG
                pop     ds
                mov     bx,16
                mov     ax,GDTSeg
                mul     bx
                mov     WORD PTR VGDTR.Base,ax
                mov     WORD PTR VGDTR.Base+2,dx
                ret
InitGDT         ENDP
;----------------------------------------------------------------------------
;入口参数:FS:SI=第一个要初始化的描述符,CX=要初始化的描述符数
;----------------------------------------------------------------------------
InitLDT         PROC
                mov     ax,WORD PTR fs:[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR fs:[si].BaseL,ax
                mov     BYTE PTR fs:[si].BaseM,dl
                mov     BYTE PTR fs:[si].BaseH,dh
                add     si,SIZE Desc
                loop    InitLDT
                ret
InitLDT         ENDP
;----------------------------------------------------------------------------
RCodeSeg        ENDS
                END     Start

3.关于实例五的说明

程序中部分片段的背景和实现方法在前面的实例中做过介绍，下面主要就任务切换和通过调用门 实现任务内特权级变换时参数的复制等情形做些说明：

(1)从临时任务直接通过TSS切换到演示任务

从实模式切换到保护模式后，就认为进入了临时任务。但TR并没有指向临时任务的TSS。在从临 时任务切换到演示任务时，要把临时任务的现场保存到临时任务的TSS，这就要求TR指向临时任 务的TSS。所以首先要使用LTR指令把指向临时任务TSS描述符的选择子装入TR。在利用LTR指令显 示地装载TR时，并不引用TSS的内容，所以临时任务的TSS几乎没有初始化。理由是这不是真正的 任务切换。
临时任务采用段间转移指令JMP，直接指向演示任务的TSS，切换到演示任务。在执行切换到演示 任务的段间转移指令JMP时，CPL=0，JMP指令中所含选择子内的RPL=0，演示任务TSS的描述符特 权级DPL=0，并且是一个可用的TSS，所以顺利进行从临时任务到演示任务的切换。切换过程包 括：把临时任务的执行现场保存到临时任务的TSS中；从演示任务的TSS中恢复演示任务的现场； 把演示任务的LDT描述符选择子装载到LDTR等。从源程序可见，初始化后的演示任务的TSS中CS字 段存放的选择子是DemoCode_Sel，对应的描述符在演示任务的LDT中，并且DPL=2，它描述了代码 段DemoCode；挂起点是DemoBegin，所以在切换到演示任务后从该点开始执行，并且CPL=2。由于 使用JMP指令进行任务切换，所以不实施任务链接。

(2)从演示任务通过任务门切换到临时任务

演示任务采用段间转移指令JMP，通过任务门ToTempT切换到临时任务。在执行切换到临时任务的 段间转移指令JMP时，CPL=2，JMP指令中所含选择子ToTempT_Sel内的RPL=0，它指示的任务门的 描述符特权级DPL=3，所以可以访问该任务门。任务门内的选择子TempTSS_Sel指示临时任务 的TSS，并且此时的临时任务TSS是可用的，所以可顺利进行任务切换。演示任务的现场保存到演 示任务的TSS；临时任务的现场从临时任务的TSS恢复。
临时任务的挂起点是临时任务代码段的ToRela点，所以恢复后的临时任务从该点开始，CS含临时 任务代码段的选择子。但由于在演示任务内“强硬”地改变了临时任务TSS内的SS和DS等字段， 所以在恢复到临时任务时，SS和DS等段寄存器内已含有规范数据段的选择子，而非挂起时的原有 值。注意，这种做法不被提倡，但在这里却充分地展示了如何从TSS恢复任务。

(3)演示任务内的特权级变换和堆栈传递参数

演示任务采用段间调用指令CALL，通过调用门ToSubR调用子程序SubRB。执行段间调用指令CALL时 的CPL=2，指令所含指向调用门的选择子的RPL=2，调用门的DPL=3，所以对调用门的访问是允许 的；尽管调用门内的选择子的RPL=3，但由于它所指示的子程序代码段描述符的DPL=0，所以在调 用过程中就发生了从特权级2到特权级0的变换，同时堆栈也被切换。
演示代码段通过堆栈传递了两个参数给子程序SubRB。在把参数压入堆栈时，CPL=2，使用的也是 对应特权级2 的堆栈。通过调用门进入子程序后，CPL=0，使用0级堆栈。为此，把调用门ToSubR中 的DCount字段设置为2，表示在特权级向内层变换时，需从外层堆栈依次复制2各个双字参数到内层 堆栈。随着特权级变换，堆栈也跟着变换。这种在堆栈切换的同时复制所需参数的做法，保证了子 程序方便地访问堆栈中的参数，而无需考虑是哪个堆栈。
随着从子程序SubRB的返回，CPL=0变换为CPL=2，堆栈也回到2级堆栈。由于再次进入0级堆栈，总 是从空开始，所以在返回前不是非要保持内层堆栈平衡不可。但2级堆栈中的2个双字参数需要废 除。从源程序可见，这是采用带立即数的段间返回指令实现的，在返回的同时，自动废除外层堆栈 中的参数，同时也废除了内层堆栈中的参数。

(4)别名技术的应用

关于别名技术，前文已经作过介绍。实例五也有两处应用了别名技术。
为了把调用门ToSubR中的DCount字段设置成2，使用一个数据段描述符ToDLDT描述调用门所在演示 任务的LDT段，该描述符把演示任务的LDT段描述成数据段。
还有一处是把临时任务的TSS视为普通数据段。从演示任务切换到临时任务之前，把指向描述规范 数据段的描述符Normal的选择子Normal_Sel填到临时任务TSS中的各数据段寄存器(包括堆栈段寄 存器)字段，于是在切换到临时任务时，作为恢复临时任务的现场，该选择子就被装到DS等数据段 寄存器，对应的描述符Normal内的信息也就被装入到对应的高速缓冲寄存器中，达到为从临时任 务切换到实模式作准备的目的。


七.中断和异常

80386除了保持8086/80386的相关功能外，还增强了中断处理能力，并引入了“异常”的概念。本文 将介绍80386中断和异常的机制。这里下载本文所有源代码。

<一>80386的中断和异常

8086/8088把中断分为内部中断和外部中断两大类。为了支持多任务和虚拟存储器等功能，80386把 外部中断称为“中断”，把内部中断称为“异常”。与8086/8088一样，80386通常在两条指令之间 响应中断或异常。80386最多处理256种中断或异常。

1.中断

对80386而言，中断是由异步的外部事件引起的。外部事件及中断响应与正执行的指令没有关系。 通常，中断用于指示I/O设备的一次操作已完成。与8086/8088一样，80386有两根引脚INTR和NMI接 受外部中断请求信号。INTR接受可屏蔽中断请求。NMI接受不可屏蔽中断请求。在80386中，标志寄 存器EFLAGS中的IF标志决定是否屏蔽可屏蔽中断请求。
外部硬件在通过INTR发出中断请求信号的同时，还要向处理器给出一个8位的中断向量。处理器在 响应可屏蔽中断请求时，读取这个由外部硬件给出的中断向量号。处理器对这个中断向量号并没有 规定。但在具体的微机系统中，系统必须通过软件和硬件的配合设置，使得给出的这个中断向量号 不仅与外部中断源对应，而且要避免中断向量号使用冲突情况的出现。可编程中断控制器芯片8259A可 配合80386工作，能够根据设置向处理器提供上述中断向量号，还能处理中断请求的优先级。每 个8259A芯片可以支持8路中断请求信号，如果使用9个8259A芯片(一个主片，8个从片)，就可使80386在 单个引脚INTR上接受多达64个中断源的中断请求信号。
处理器不屏蔽来自NMI的中断请求。处理器在响应NMI中断时，不从外部硬件接收中断向量号。 与8086/8088一样，在80386中，不可屏蔽中断所对应的中断向量号固定为2。为了不可屏蔽中断的 嵌套，每当接受一个NMI中断，处理器就在内部屏蔽了再次响应NMI，这一屏蔽过程直到执行中断返回 指令IRET后才结束。所以，NMI处理程序应以IRET指令结束。

2.异常

异常是80386在执行指令期间检测到不正常的或非法的条件所引起的。异常与正执行的指令有直接 的联系。例如，执行除法指令时，除数等于0。再如，执行指令时发现特权级不正确。当发生这些 情况时，指令就不能成功完成。软中断指令“INT n”和“INTO”也归类于异常而不称为中断，这 是因为执行这些指令产生异常事件。
80386识别多种不同类别的异常，并赋予每一种类别以不同的中断向量号。异常发生后，处理器就 象响应中断那样处理异常。即根据中断向量号，转相应的中断处理程序。把这种中断处理程序称为 异常处理程序可能更合适。
根据引起异常的程序是否可被恢复和恢复点不同，把异常进一步分类为故障(Fault)、陷阱(Trap)和 中止(Abort)。我们把对应的异常处理程序分别称为故障处理程序、陷阱处理程序和中止处理程序。
故障是在引起异常的指令之前，把异常情况通知给系统的一种异常。80386认为故障是可排除的。当 控制转移到故障处理程序时，所保存的断点CS及EIP的值指向引起故障的指令。这样，在故障处理程 序把故障排除后，执行IRET返回到引起故障的程序继续执行时，刚才引起故障的指令可重新得到执 行。这种重新执行，不需要操作系统软件的额外参与。故障的发现可能在指令开始执行之前，也可 能在指令执行期间。如果在指令执行期间检测到故障，那么中止故障指令，并把指令的操作数恢复 为指令开始执行之前的值。这可保证故障指令的重新执行得到正确的结果。例如，在一条指令的执 行期间，如果发现段不存在，那么停止该指令的执行，并通知系统产生段故障，对应的段故障处理 程序可通过加载该段的方法来排除故障，之后，原指令就可成功执行，至少不再发生段不存在的故障。
陷阱是在引起异常的指令之后，把异常情况通知给系统的一种异常。当控制转移到异常处理程序时， 所保存的断点CS及EIP的值指向引起陷阱的指令的下一条要执行的指令。下一条要执行的指令，不一 定就是下一条指令。因此，陷阱处理程序并不是总能根据保存的断点，反推确定出产生异常的指令。 在转入陷阱处理程序时，引起陷阱的指令应正常完成，它有可能改变了寄存器或存储单元。软中断 指令、单步异常是陷阱的例子。
中止是在系统出现严重情况时，通知系统的一种异常。引起中止的指令是无法确定的。产生中止时， 正执行的程序不能被恢复执行。系统接收中止后，处理程序要重新建立各种系统表格，并可能重新 启动操作系统。硬件故障和系统表中出现非法值或不一致的值是中止的例子。

3.优先级

在一条指令执行期间，入检测到不只一个中断或异常，那么按下表所列优先级通知系统。把优先级 最高的中断或异常通知系统，其它优先级较低的异常被废弃，而优先级较高的中断则保持悬挂。

80386响应 中断/异常 的优先级 | 中断/异常类型 | 优先级
 | 调试故障 | 最高
 | 其它故障 | ↓
 | 陷阱指令INT n和INTO | ↓
 | 调试陷阱 | ↓
 | NMI中断 | ↓
 | INTR中断 | 最低

<二>异常类型

象中断分为多种类型一样，异常也可分为多种类型。

1.80386识别的异常

80386识别的多种不同类别的异常及赋予的对应中断向量号如下表所示。某些异常还以出错码的形式 提供一些附加信息传递给异常处理程序，出错代码列中的“无”表示没有出错代码，“有”表示有 出错代码。

异 常 一 览 表 | 向量号 | 异常名称 | 异常类型 | 出错代码 | 相关指令
 | 0 | 除法出错 | 故障 | 无 | DIV,IDIV
 | 1 | 调试异常 | 故障/陷阱 | 无 | 任何指令
 | 3 | 单字节INT3 | 陷阱 | 无 | INT 3
 | 4 | 溢出 | 陷阱 | 无 | INTO
 | 5 | 边界检查 | 故障 | 无 | BOUNT
 | 6 | 非法操作码 | 故障 | 无 | 非法指令编码或操作数
 | 7 | 设备不可用 | 故障 | 无 | 浮点指令或WAIT
 | 8 | 双重故障 | 中止 | 有 | 任何指令
 | 9 | 协处理器段越界 | 中止 | 无 | 访问存储器的浮点指令
 | 0AH | 无效TSS异常 | 故障 | 有 | JMP、CALL、IRET或中断
 | 0BH | 段不存在 | 故障 | 有 | 装载段寄存器的指令
 | 0CH | 堆栈段异常 | 故障 | 有 | 装载SS寄存器的任何指令、对SS寻址的段访问的任何指令
 | 0DH | 通用保护异常 | 故障 | 有 | 任何特权指令、任何访问存储器的指令
 | 0EH | 页异常 | 故障 | 有 | 任何访问存储器的指令
 | 10H | 协处理器出错 | 故障 | 无 | 浮点指令或WAIT
 | 11H—0FFH | 软中断 | 陷阱 | 无 | INT n

由上表可见，保护模式下的某些中断向量号的分配与实模式的中断向量号发生了冲突。实 模式下的中断向量号的分配基于PC微机系统的8086/8088 CPU，上表中的中断向量号的分配 是80386所规定的。实际上，Intel在宣布8086/8088时，保留了这些发生冲突的中断向量号。 尽管发生这样的冲突，但以80386为CPU的微机系统仍可保持与以8086/8088为CPU的微机系统 的兼容，原因是在80386的实模式下，几乎不发生那些中断向量号与外部硬件中断请求时所 提供的中断向量号存在冲突的异常。需要注意的是，在保 护模式下必须重新设置8259A中断控制器，以产生不与异常相冲突的硬件中断向量。

2.故障类异常

当发生故障，控制转移到故障处理程序时，所保存的断点CS及EIP的值指向引起故障的指令， 以便在排除故障后恢复执行。

(1)除法出错故障(异常0)

除法出错是一种故障。当执行DIV指令或IDIV指令时，如果除数等于0或者商太大，以至于存 放商的操作数容纳不下，那么就产生这一故障。除法出错故障不提供出错码。

(2)边界检查故障(异常5)

如果BOUND指令发现被测试的值超过了指令中给定的范围，那么就发生边界检查故障。边界 检查故障不提供出错码。

(3)非法操作码故障(异常6)

如果80386不能把CS和EIP所指向存储单元的位模式识别为某条指令的部分，那么就发生非 法操作码故障。当出现如下情况时，发生这样的故障：(1)操作码字段的内容不是一个合法 的80386指令代码；(2)要求使用存储器操作数的场合，使用了寄存器操作数；(3)不能被加 锁的指令前使用了LOCK前缀。非法操作码故障不提供操作码。

(4)设备不可用故障(异常7)

设备不可用故障支持80387数字协处理器。在没有80387协处理器硬件的系统中，可用该异 常的处理程序代替协处理器的软件模拟器。在发生任务切换时，使得只有在新任务使用浮 点指令时，才进行80387寄存器状态的切换。设备不可用故障不提供出错码。该故障在下列 情况下产生：(1)在执行浮点指令时，控制寄存器CR0中的EM位或TS位为1；(2)在执行WAIT指 令时，控制寄存器CR0中TS位及EM位都为1。需要注意的 是，本异常的处理程序必须是一个过程而不能是任务，否则当处理程序发布一条IRET指令 时，80386就设置TS位。然后协处理器再次执行这个发生故障的指令，发现TS是置位的，因 此就再次发生异常7，结果是无休止的循环。处理程序能通过陷阱门被调用，因为执行期间 可以允许中断。

(5)无效TSS故障(异常0AH)

当正从任务状态段TSS装入选择子时，如果发生了除了段不存在故障以外的段异常时，就发 生无效TSS故障。在进入故障处理程序时，保存的CS及EIP指向发生故障的指令；或者该故障 作为任务切换的一部分发生时，指向任务的第一条指令。
无效TSS故障提供了一个出错码，出错码的格式如下图所示。其中选择子部分是指向引起故 障的TSS的选择子。16位的出错代码的主要成分是选择子，指向引起故障的TSS的选择子。 高13位是选择子的索引部分，TI位是描述符表指示位。

出错代码的格式
BIT15—BIT3 | BIT2 | BIT1 | BIT0
选择子的索引部分 | TI | IDT | EXT

上图所示出错码格式是异常时出错码的一般格式。从图中可见出错码中不含选择子的RPL， 而由IDT位和EXT位代替。当处理某一异常或外部中断时，又发生了某种异常，那么EXT位 置1。当从中断描述符表IDT中读出表项并产生异常时，IDT位置1，这只在中断或异常的处 理期间才会发生。当没有选择子时，构成出错码选择子部分的值为0。
一些引起无效TSS故障的原因如下：
TSS描述符中的段限长小于103；
无效的LDT描述符，或者LDT未出现；
堆栈段不是一个可写段；
堆栈段选择子索引的描述符超出描述符表界限；
堆栈段DPL与新的CPL不匹配；
堆栈段选择子的RPL不等于CPL；
代码段选择子索引的描述符超出描述符表界限；
代码段选择子不指向代码段；
非一致代码段的DPL不等于新的CPL；
一致代码段的DPL大于新的CPL；
对应DS、ES、FS或GS的选择子指向一个不可读段(如系统段)；
对应DS、ES、FS或GS的选择子索引的描述符超出描述符表的界限。

(6)段不存在故障(异常0BH)

处理器在把描述符装入非SS段寄存器的高速缓冲时，如果发现描述符其它方面有效，而P位 为0(表示对应段不存在)，那么在引用此描述符时就发生段不存在故障。有关SS段的情形纳 入堆栈段故障。在进入故障处理程序时，保存的CS及EIP执行发生故障的指令；或者该故障 作为任务切换的一部分发生时，指向任务的第一条指令。
段不存在故障提供了一个包含引起该故障的段选择子的出错代码。出错码的格式如上图所 示。选择子索引部分为引起段不存在故障的段描述符选择子的索引。

(7)堆栈段故障(异常0CH)

当处理器检测到用SS寄存器进行寻址的段有关的某种问题时，就发生堆栈段故障。在进入 故障处理程序时，保存的CS及EIP指向发生故障的指令；或者该故障作为任务切换的一部分 发生时，指向任务的第一条指令。堆栈段故障提供一个出错码，出错码的格式也如上图。
具体地说，当出现下列三种情况时，将引起堆栈段故障：
(1)在堆栈操作时，偏移超出段界限所规定的范围。这种情况下的出错码是0。例如PUSH操 作时，堆栈溢出。
(2)在由特权级变换所引起的对内层堆栈的操作时，偏移超出段界限所规定的范围。这种情 况下的出错码包含有内层堆栈的选择子。
(3)装入到SS寄存器(高速缓冲寄存器)的描述符中的存在位为0。这种情况下的出错码包含 有对应的选择子。
上述第一种情况是容易辨别的。第二和第三种情况的辨别要通过判断出错代码所含选择子 所指示的描述符中的存在位进行。如果存在位为1，那么是第二种情况；否则是第三种情况。

(8)通用保护故障(异常0DH)

除了明确列出的段异常外，其它的段异常都被视为通用保护故障。在进入故障处理程序时， 保存的CS及EIP指向发生故障的指令；或者该故障作为任务切换的一部分发生时，指向任务 的第一条指令。通用保护故障提供一个出错码，出错码的格式也如上图所示。
根据处理程序可能作出的响应，通用保护故障可分为如下两类：
(1)违反保护方式，但程序无须中止的异常。这类故障提供的出错码为0。这种异常在应用 程序执行特权指令或I/O访问时发生，支持虚拟8086程序的系统或支持虚拟I/O访问的系统 需要模拟这些指令，并在模拟完成产生故障的指令后，重新执行被中断的程序。
(2)违反保护方式，并导致程序终止的异常。这类故障提供的出错码可能为0，也可能不 为0(能确定选择子时)。引起这类故障的一些原因如下：
向某个只读数据段或代码段写；
从某个只能执行的代码段读出；
将某个系统段描述符装入到数据段寄存器DS、ES、FS、GS或SS；
将控制转移到一个不可执行的段；
在通过段寄存器CS、DS、ES、FS或GS访问内存时，偏移超出段界限；
当访问某个描述符表时，超过描述符表段界限；
把PG位为1但PE位为0的控制信息装入到CR0寄存器；
切换到一个正忙的任务。
对上述两类通用保护故障的辨别，可通过检查引起故障的指令和出错码进行。如果出错码 非0，那么肯定是第二类通用保护故障。如果出错码是0，那么需要进一步检查引起故障的 指令，以确定它是否是系统支持的可以模拟的指令。

(9)页故障(异常0EH)

关于页故障的详细说明请见后面的文章。

(10)协处理器出错(异常10H)

协处理器出错故障指示协处理器发生了未被屏蔽的数字错误，如上溢或下溢。在引起故障 的浮点指令之后的下一条浮点指令或WAIT指令，把协处理器出错作为一个故障通知给系统。 协处理器出错故障不提供出错码。

3.陷阱类异常

(1)调试陷阱(异常1)

调试异常有故障类型，也有陷阱类型。调试程序可以访问调试寄存器DR6，以确定调试异常 的原因和类型。调试异常不提供出错码。

(2)单字节INT3(异常3)

INT3是一条特别的单字节“INT n”指令。调试程序可利用该指令支持程序断点。INT3指令 被看成是一种陷阱，而不是一个中断。当由于执行INT3指令进入异常3处理程序时，被保存 的CS和EIP指向紧跟INT3的指令，即INT3后面的字节。INT3陷阱不提供出错码。

(3)溢出(异常4)

INTO指令提供条件陷阱。如果OF标志为1，那么INTO指令产生陷阱；否则不产生陷阱，继续 执行INTO后面的指令。在进入溢出处理程序时，被保存的CS和EIP指向INTO指令的下一条指 令。溢出陷阱不提供出错码。

4.中止类异常

(1)双重故障异常(异常8)

当系统正在处理一个异常时，如果又检测到一个异常，处理器试图向系统通知一个双重故障， 而不是通知第二个异常。双重故障属于中止类异常，所以在转入双重故障处理程序时，被保 存的CS和EIP可能不指向引起双重故障的指令，而且指令的重新启动不支持双重故障。双重故 障提供的出错码为0。
当正处理一个段故障异常时，有可能又产生一个页故障。在这种情况下，通知给系统的是一 个页故障异常而不是双重故障异常。但是，如果正处理一个段故障或页故障时，又一个段故 障被检测到；或者如果正处理一个页故障时，又一个页故障被检测到，那么就引起双重故障。
当正处理一个双重故障时，又一个段或页故障被检测到，那么处理器暂停执行指令，并进入 关机方式。关机方式类似于处理器指令一条HLT指令后的状态：处理器空转，并维持到处理器 接收到一个NMI中断请求或者被重新启动为止。在关机方式下，处理器不响应INTR中断请求。
双重故障通常指示系统表出现严重的问题，例如段描述符表、页表或中断描述符表出现问题。 双重故障处理程序在重建系统表后，可能不得不重新启动操作系统。

(2)协处理器段越界(异常9)

协处理器段越界异常属于中止类异常，这是因为引起该异常的指令不能被重新启动。当浮点 指令操作数超出段界限时，产生该中止异常。协处理器段越界异常不提供出错码。在异常处 理程序入口保存的CS及EIP指向被中止的指令。这种中止不是系统的中止，而是只影响检测到 这种异常时正执行的指令所在的程序。

<三>中断和异常的转移方法

80386实模式下的中断和异常的转移方法与8086相同。这里介绍的中断和异常的转移方法是指 80386在保护模式下响应中断和处理异常时所采用的转移方法。

1.中断描述符表IDT

与8086/8088一样，在响应中断或者处理异常时，80386根据中断向量号转对应的处理程序。但 是，在保护模式下，80386不使用实模式下的中断向量表，而是使用中断描述符表IDT。在保护 模式下，80386把中断向量号作为中断描述符表IDT中描述符的索引，而不再是中断向量表中的 中断向量的索引。
象全局描述符表GDT一样，在整个系统中，中断描述符表IDT只有一个。中断描述符表寄存 器IDTR指示IDT在内存中的位置。由于80386只识别256个中断向量号，所以IDT最大长度是2K。
中断描述符表IDT所含的描述符只能是中断门、陷阱门和任务门。也就是说，在保护模式 下，80386只有通过中断门、陷阱门或任务门才能转移到对应的中断或异常处理程序。
从前文中给出的门描述符的格式可见，门描述符包含由选择子和偏移构成的48位全指针。另 外，双字计数字段对中断门、陷阱门和任务门而言无意义。

2.中断响应和异常处理的步骤

由硬件自动实现的中断响应和异常处理的步骤如下：
首先，判断中断向量号要索引的门描述符是否超出IDT的界限。若超出界限，就引起通用保护 故障，出错码为中断向量号乘8再加2。
其次，从IDT中取得对应的门描述符，分解出选择子、偏移量和描述符属性类型，并进行有关 检查。描述符只能是任务门、286中断门、286陷阱门、386中断门或386陷阱门，否则就引起 通用保护故障，出错码是中断向量号乘8再加2。如果是由INT n指令或INTO指令引起转移，还 要检查中断门、陷阱门或任务门描述符中的DPL是否满足CPL<=DPL(对于其它的异常或中断， 门中的DPL被忽略)。这种检查可以避免应用程序执行INT n指令时，使用分配给各种设备用的 中断向量号。如果检查不通过，就引起通用保护故障，出错码是中断向量号乘8再加2。门描 述符中的P位必须是1，表示门描述符是一个有效项，否则就引起段不存在故障，出错码是中 断向量号乘8再加2。
最后，根据门描述符类型，分情况转入中断或异常处理程序。
对于异常处理，在开始上述步骤之前，还要根据异常类型确定返回点；如果有出错代码，则形 成符合出错码格式的出错码，并在实际执行异常处理程序之前把出错码压入堆栈。为了保证栈 的双字边界对齐，16位的出错码以32位的值压入，其中高16位的值未作定义，对于16位段也是 如此。

3.通过中断门或陷阱门的转移

如果中断向量号所指示的门描述符是386中断门或386陷阱门，那么控制转移到当前任务的一个 处理程序过程，并且可以变换特权级。与其它调用门的CALL指令一样，从中断门和陷阱门中获 取指向处理程序的48位全指针。其中16位选择子是对应处理程序或代码段的选择子，它指示全 局描述符表GDT或局部描述符表LDT中的代码段描述符；32位偏移指示处理程序入口点在代码段 内的偏移量。
通过中断门或陷阱门的转移过程如下所示，该过程由 硬件自动进行。
(1)若选择子为空，则产生通用保护故障；
(2)取对应的描述符；
(3)若非存储段描述符，则产生通用保护故障；
(4)若非一致代码段且DPL
(5)调整RPL=0；
(6)把描述符装入CS；
(7)若入口偏移越界，则产生通用保护故障；
(8)EFLAGS压入堆栈；
(9)CS压入堆栈；
(10)EIP压入堆栈；
(11)使TF=0，NT=0；
(12)若为中断门，则使IF=0；
(13)若有出错码，则把出错码压入堆栈；
(14)转入处理程序。
由上述转移过程可见，中断门或陷阱门中指示处理程序的选择子必须指向描述一个可执行的 代码段的描述符。如果选择子为空，就引起通用保护故障，出错码是0。如果描述符不是代码 段描述符，就引起通用保护故障，出错码含选择子。
中断或异常可以转移到同一特权级或内层特权级。上述指定处理程序代码段的描述符中的类 型及DPL字段，决定了这种同一任务内的转移是否要发生特权级变换。如果是一个非一致代码 段，并且DPLCPL则产生通用保护异常。
上述转移过程中的第六步，也就是“把描述符装入CS”，是指把上述指定处理程序段的描述符 装入CS的高速缓冲寄存器中，在这一步骤中要对描述符进行类似通过调用门进行转移的其它检 查，包括是否代码段描述符和代码段描述符是否存在等，因此可能再发生异常。在对该描述符 进行检查时，通过调整门中选择子的RPL=0(在处理器内部调整，而不影响存储器中的选择子 的RPL字段)的方法，实现只考虑代码段的DPL，而不考虑门中选择子的RPL。把描述符装入CS之 后，还要检查门描述符中给出的表示处理程序代码段入口的偏移是否越界，即是否超出段界限。 如果越界，就引起出错码为0的通用保护故障。
从转移过程还可以看出，把标志寄存器和断点压入堆栈的做法和顺序与实模式是相同的，但这 里每一次堆栈操作是一个双字，CS被扩展成32位。在16位段中亦是如此。
把TF置成0，表示不允许处理程序单步执行。把NT置成0，表示处理程序在利用中断返回指 令IRET返回时，返回到同一任务而不是一个嵌套任务。需要注意的是，任何特权级的程序都可 改变NT位，这样可以利用中断或陷阱处理程序完成任务切换。
通过中断门的转移和通过陷阱门的转移之间的差别只是对IF标志的处理。对于中断门，在转移 过程中把IF置为0，使得在处理程序执行期间屏蔽掉INTR中断(当然，在中断处理程序中可以人 为设置IF标志打开中断，以使得在处理程序执行期间允许响应可屏蔽中断)；对于陷阱门，在 转移过程中保持IF位不变，即如果IF位原来是1，那么通过陷阱门转移到处理程序之后仍允 许INTR中断。因此，中断门最适宜于处理中断，而陷阱门适宜于处理异常。
在有出错码的情况下，转入处理程序之前还要把出错码压入堆栈。只有异常处理才可能有出错 码。下图给出了通过中断门或陷阱门转移时的堆栈情况。(a)是没有变换特权级和没有出错码 的情形；(b)是没有变换特权级有出错码的情形；(c)是变换特权级和没有出错码的内层堆栈的 情形。(d)是变换特权级和有出错码的内层堆栈情形。注意图中每一项为双字。

4.通过任务门的转移

如果中断向量号所只是的门描述符是任务门描述符，那么控制转移到一个作为独立的任务方式 出现的处理程序。任务门中的选择子是指向描述对应处理程序任务的TSS段的选择子，即该选择 子指示一个可用的286TSS或386TSS。通过任务门的转移与通过任务门到一个可用的386TSS的段 间调用指令CALL的转移很相似，主要的区别是，对于提供出错码的异常处理，在完成任务切换 之后，把出错码压入新任务的堆栈中(通过任务门进行转移时，返回地址和外层栈指针不压入新 任务的堆栈)。
通过任务门的转移，在进入中断或异常处理程序时，标志寄存器EFLAGS中的NT位被置为1，表示 是嵌套任务，则IRET指令返回时，沿TSS中的链接字段返回到最后一个被挂起的任务。
在响应中断或处理异常时，使用任务门可提供一个处理程序任务的自动调度。这种任务调度由 硬件直接执行，并且越过包含在操作系统中的软件任务切换，这就为处理程序提供了一个快速 的任务切换。

5.转移方法的比较

对中断的响应和异常的处理，80386允许通过使用中断门或陷阱门实现由当前任务之内的一个过 程进行处理；也允许通过使用任务门实现由另一个任务进行处理。在当前任务之内的处理程序 较为简单，并可以很快地转移到处理程序，但处理程序要负责保存及恢复处理器的寄存器等内 容。转到不同任务的处理程序要花费较长时间，保存及恢复处理器寄存器内容的开销作为任务 切换的一部分。使用当前任务内的处理程序的方法，在响应中断或处理异常时，对正执行任务 的状态可直接进行访问，但是这就要求每一个任务之内都包含一个处理程序。使用独立任务的 处理方法，使处理程序得到较好的隔离，但在响应中断或处理异常时，对原任务状态的访问变 得较为复杂。需要注意得是，有些异常必须由中断门或陷阱门进行处理，如上面提到得异常7。
无效TSS异常必须使用任务门进行处理，以保证处理程序有一个有效得任务环境。其它得异常通 常在任务环境之内进行处理。在任务内，异常被检测并且不必屏蔽中断，所以，所以使用陷阱 门。由陷阱门指示的异常处理程序是一个由所有任务共享的过程，所以该处理程序最好置于全 局地址空间之内。如果各个任务要求有不同的处理程序，那么全局异常处理程序可保存一个各 处理程序的入口表，并为引起异常的任务调用相应的处理程序。
中断通常与正执行的任务没有关系，并可能从使用任务门提供的隔离中获得好处。要求较快响 应的中断，通过中断门可以得到较好的处理。因为中断随时都可能发生，所以，通过中断门访 问的中断处理程序，必须置于全局地址空间中，以便对所有的任务都有效。还需强调的 是，80386程序绝不能调用一个低特权级的过程，当处理器调用一个中断或异常处理程序时，它 实施相同的规则，所以，这样一个过程必须至少具有与在其任务上下文中被调用的、由任务所 执行的最高特权级的过程相同的特权级。因此，在使用中断门时，中断处理程序通常必须被安 排在特权级0，否则若正在特权级0执行时发生中断，则不能进入中断处理程序，而会引起通用 保护故障(中断处理程序使用任务门时除外，因为任务切换可以从任何特权级切换到目标任务的 任何特权级)。

6.中断或异常处理后的返回

中断返回指令IRET用于从中断或异常处理程序的返回。该指令的执行根据任务嵌套标志NT位是 否为1分为两种情形。
NT位为1，表示是嵌套任务的返回。当前TSS中的链接字段保存有前一任务的TSS的选择子，取出 该选择子进行任务切换就完成了返回。这种情形在由通过任务门转入的中断或异常处理程序返 回时出现，因为在由中断门或陷阱门转入处理程序时，NT位已被清0。
NT位为0，表示当前任务内的返回。这种情形在由通过中断门或陷阱门转入的中断或异常处理程 序返回时出现。具体进行的操作包括：从堆栈顶弹出返回指针EIP及CS，然后弹出EFLAGS值。弹 出的CS选择子的RPL字段，确定返回后的特权级。如果返回选择子的RPL与CPL相同，则不进行特 权级改变。若RPL规定了一个外层特权级，则需要特权级改变，从内层堆栈中弹出外层堆栈的指 针ESP及SS的值。这些做法与RET指令相似。例如，使用CS选择子的RPL，而不是由选择子标识的 段的DPL，是为了返回到不在DPL给定特权级的一致代码段。若弹出的CS的选择子的RPL规定了一 个内层特权级，则产生通用保护故障。需要注意的是，对于IRET指令，保存在当前堆栈中的返回 地址中的选择子字段必须指向代码段描述符。而不能是系统段或门描述符。否则将引起通用保护 故障。
对于提供出错代码的异常处理程序，必须先人为地从堆栈中弹出出错代码，然后再执行IRET指令， 及出错代码不会自动被处理器弹出或取消。
中断返回指令IRET不仅能够用于由中断/异常引起的嵌套任务的返回，而且也适用于由段间调用 指令CALL通过任务门引起的嵌套任务的返回，如前文所述，在执行通过任务门进行任务切换的段 间调用指令CALL时，标志寄存器中的NT位被置为1，表示任务嵌套。而RET指令不能实现此功能。

<四>演示中断处理的实例(实例六)

下面给出一个用于演示中断处理的实例。该实例的逻辑功能是，在屏幕的左上角以倒计时方式显 示秒为单位的时间，在时间用完后结束。该实例演示内容包括：外部中断处理程序和陷阱处理程序。

1.源程序组织和清单

本实例由如下几部分组成：
(1)全局描述符表GDT。GDT中除了含有常见的几个描述符外，还含有描述时钟中断处理程序所使用 的代码段和数据段描述符，以及描述显示程序所使用的代码段和数据段描述符。
(2)中断描述符表IDT。为了在保护模式下响应中断和处理异常，必须有IDT。IDT含有256个门描述 符。8号安排的是一个通向时钟中断处理程序的中断门，0FEH号安排的是通向显示处理程序的陷阱 门，其它均安排成通向其它中断或异常处理程序的陷阱门。
(3)时钟中断处理程序的代码段和数据段。
(4)实现直接写显示缓冲区进行显示的程序代码段和数据段。
(5)处理其它中断或异常的处理程序的代码段。
(6)演示程序的代码段、数据段和堆栈段。
(7)实模式下执行的启动和结束程序代码段和数据段。
源程序清单如下：
;名称:ASM6.ASM
;功能:演示中断处理的实现
;编译:TASM ASM6.ASM
;连接:TLINK ASM6.OBJ
;----------------------------------------------------------------------------
INCLUDE         386SCD.INC
;----------------------------------------------------------------------------
;部分常量定义
;----------------------------------------------------------------------------
EOICOM          =       20h                       ;外部中断处理结束命令
ICREGP          =       20h                       ;中断控制寄存器端口地址
IMREGP          =       21h                       ;中断屏蔽寄存器端口地址
;----------------------------------------------------------------------------
GDTSeg          SEGMENT PARA USE16                ;全局描述符表数据段(16位)
;----------------------------------------------------------------------------
                ;全局描述符表GDT
GDT             LABEL   BYTE
                ;空描述符
DUMMY           Desc    <>
                ;规范段描述符
Normal          Desc    <0ffffh,,,ATDW,,>
                ;视频缓冲区段描述符(DPL=3)
VideoBuf        Desc    <0ffffh,8000h,0bh,ATDW,,>
;----------------------------------------------------------------------------
EFFGDT          LABEL   BYTE
                ;临时代码段描述符
TempCode        Desc    <0ffffh,TempCodeSeg,,ATCE,,>
                ;演示代码段描述符
DemoCode        Desc    <DemoCodeLen-1,DemoCodeSeg,,ATCE,,>
                ;演示数据段描述符
DemoData        Desc    <DemoDataLen-1,DemoDataSeg,,ATDW,,>
                ;演示堆栈段描述符
DemoStack       Desc    <DemoStackLen-1,DemoStackSeg,,ATDWA,,>
                ;0feh号中断处理程序(显示程序)代码段描述符
EchoCode        Desc    <EchoCodeLen-1,EchoCodeSeg,,ATCE,,>
                ;0feh号中断处理程序(显示程序)数据段描述符
EchoData        Desc    <EchoDataLen-1,EchoDataSeg,,ATDW,,>
                ;8号中断处理程序代码段描述符
TICode          Desc    <TICodeLen-1,TICodeSeg,,ATCE,,>
                ;8号中断处理程序数据段描述符
TIData          Desc    <TIDataLen-1,TIDataSeg,,ATDW,,>
                ;其它中断或异常处理程序代码段描述符
Other           Desc    <OtherCodeLen-1,OtherCodeSeg,,ATCE,,>
;----------------------------------------------------------------------------
GDTLen          =       $-GDT                     ;全局描述符表长度
GDNum           =       ($-EFFGDT)/(SIZE Desc)    ;需特殊处理的描述符数
;----------------------------------------------------------------------------
Normal_Sel      =       Normal-GDT                ;规范段描述符选择子
Video_Sel       =       VideoBuf-GDT              ;视频缓冲区段描述符选择子
;----------------------------------------------------------------------------
TempCode_Sel    =       TempCode-GDT              ;临时代码段的选择子
DemoCode_Sel    =       DemoCode-GDT              ;演示代码段的选择子
DemoData_Sel    =       DemoData-GDT              ;演示数据段的选择子
DemoStack_Sel   =       DemoStack-GDT             ;演示堆栈段的选择子
EchoCode_Sel    =       EchoCode-GDT              ;0feh号中断程序代码段选择子
EchoData_Sel    =       EchoData-GDT              ;0feh号中断程序数据段选择子
TICode_Sel      =       TICode-GDT                ;8号中断程序代码段选择子
TIData_Sel      =       TIData-GDT                ;8号中断程序数据段选择子
Other_Sel       =       Other-GDT                 ;其它中断或异常代码段选择子
;----------------------------------------------------------------------------
GDTSeg          ENDS                              ;全局描述符表段定义结束
;----------------------------------------------------------------------------
IDTSeg          SEGMENT PARA USE16                ;中断描述符表数据段(16位)
;----------------------------------------------------------------------------
IDT             LABEL   BYTE                      ;中断描述符表
                ;0--7的8个陷阱门描述符
                REPT    8
                Gate    <OtherBegin,Other_Sel,,AT386TGate,>
                ENDM
                ;对应8号(时钟)中断处理程序的门描述符
                Gate    <TIBegin,TICode_Sel,,AT386IGate,>
                ;从9--0fdh的245个陷阱门描述符
                REPT    245
                Gate    <OtherBegin,Other_Sel,,AT386TGate,>
                ENDM
                ;对应0feh号中断处理程序的陷阱门描述符
                Gate    <EchoBegin,EchoCode_Sel,,AT386TGate,>
                ;对应0ffh号中断处理程序的陷阱门描述符
                Gate    <OtherBegin,Other_Sel,,AT386TGate,>
;----------------------------------------------------------------------------
IDTLen          =       $-IDT
;----------------------------------------------------------------------------
IDTSeg          ENDS                              ;中断描述符表段定义结束
;----------------------------------------------------------------------------
;其它中断或异常处理程序的代码段
;----------------------------------------------------------------------------
OtherCodeSeg    SEGMENT PARA USE16
                ASSUME  CS:OtherCodeSeg
;----------------------------------------------------------------------------
OtherBegin      PROC    FAR
                mov     ax,Video_Sel
                mov     es,ax
                mov     ah,17h                    ;在屏幕左上角显示兰底白字
                mov     al,'!'                    ;符号"!"
                mov     WORD PTR es:[0],ax
                jmp     $                         ;无限循环
OtherBegin      ENDP
;----------------------------------------------------------------------------
OtherCodeLen    =       $
OtherCodeSeg    ENDS
;----------------------------------------------------------------------------
;8号中断处理程序的数据段
;----------------------------------------------------------------------------
TIDataSeg       SEGMENT PARA USE16
Count           DB      0                         ;中断发生的计数器
TIDataLen       =       $
TIDataSeg       ENDS
;----------------------------------------------------------------------------
;8号中断处理程序的代码段
;----------------------------------------------------------------------------
TICodeSeg       SEGMENT PARA USE16
                ASSUME  CS:TICodeSeg,DS:TIDataSeg
;----------------------------------------------------------------------------
TIBegin         PROC    FAR
                push    eax                       ;保护现场
                push    ds
                push    fs
                push    gs
                mov     ax,TIData_Sel             ;置中断处理程序数据段
                mov     ds,ax
                mov     ax,EchoData_Sel           ;置显示过程数据段
                mov     fs,ax
                mov     ax,DemoData_Sel           ;置演示程序数据段
                mov     gs,ax
                cmp     Count,0
                jnz     TI2                       ;计数非0表示未到1秒
                mov     Count,18                  ;每秒约18次
                int     0feh                      ;调用0FEH号中断处理程序显示
                cmp     BYTE PTR fs:Mess,'0'
                jnz     TI1
                mov     BYTE PTR gs:Flag,1        ;显示符号'0'时置标记
TI1:            dec     BYTE PTR fs:Mess          ;调整显示符号
TI2:            dec     Count                     ;调整计数
                pop     gs                        ;恢复现场
                pop     fs
                pop     ds
                mov     al,EOICOM                 ;通知中断控制器中断处理结束
                out     ICREGP,al
                pop     eax
                iretd                             ;中断返回
TIBegin         ENDP
;----------------------------------------------------------------------------
TICodeLen       =       $
TICodeSeg       ENDS
;----------------------------------------------------------------------------
;0FEH号中断处理程序数据段
;----------------------------------------------------------------------------
EchoDataSeg     SEGMENT PARA USE16
Mess            DB      '8',4eh
EchoDataLen     =       $
EchoDataSeg     ENDS
;----------------------------------------------------------------------------
;0FEH号中断处理程序(显示程序)的代码段
;----------------------------------------------------------------------------
EchoCodeSeg     SEGMENT PARA USE16
                ASSUME  CS:EchoCodeSeg,DS:EchoDataSeg
;----------------------------------------------------------------------------
EchoBegin       PROC    FAR
                push    ax                        ;保护现场
                push    ds
                push    es
                mov     ax,EchoData_Sel           ;置显示过程数据段
                mov     ds,ax
                mov     ax,Video_Sel              ;置视频缓冲区数据段
                mov     es,ax
                mov     ax,WORD PTR Mess
                mov     WORD PTR es:[0],ax
                pop     es
                pop     ds
                pop     ax
                iretd
EchoBegin       ENDP
;----------------------------------------------------------------------------
EchoCodeLen     =       $
EchoCodeSeg     ENDS
;----------------------------------------------------------------------------
;演示任务的堆栈段
;----------------------------------------------------------------------------
DemoStackSeg    SEGMENT PARA USE16
DemoStackLen    =       1024
                DB      DemoStackLen DUP(0)
DemoStackSeg    ENDS
;----------------------------------------------------------------------------
;演示任务的数据段
;----------------------------------------------------------------------------
DemoDataSeg     SEGMENT PARA USE16
Flag            DB      0
DemoDataLen     =       $
DemoDataSeg     ENDS
;----------------------------------------------------------------------------
;演示任务的代码段
;----------------------------------------------------------------------------
DemoCodeSeg     SEGMENT PARA USE16
                ASSUME  CS:DemoCodeSeg,DS:DemoDataSeg
;----------------------------------------------------------------------------
DemoBegin       PROC    FAR
                mov     ax,DemoStack_Sel          ;置堆栈
                mov     ss,ax
                mov     sp,DemoStackLen           ;置数据段
                mov     ax,DemoData_Sel
                mov     ds,ax
                mov     es,ax
                mov     fs,ax
                mov     gs,ax
                mov     al,11111110b              ;置中断屏蔽字
                out     IMREGP,al                 ;只开发时钟中断
                sti                               ;开中断
DemoConti:      cmp     BYTE PTR Flag,0           ;判标志
                jz      DemoConti                 ;直到不为0
                cli                               ;关中断
                ;转回临时代码段,准备回实方式
                JUMP16  TempCode_Sel,<OFFSET ToDos>
DemoBegin       ENDP
;----------------------------------------------------------------------------
DemoCodeLen     =       $
DemoCodeSeg     ENDS
;----------------------------------------------------------------------------
TempCodeSeg     SEGMENT PARA USE16                ;临时任务的代码段
                ASSUME  CS:TempCodeSeg
;----------------------------------------------------------------------------
Virtual         PROC    FAR
                JUMP16  DemoCode_Sel,DemoBegin    ;转演示任务
ToDos:          mov     ax,Normal_Sel             ;恢复实方式段描述符高速缓存
                mov     ds,ax
                mov     es,ax
                mov     fs,ax
                mov     gs,ax
                mov     ss,ax
                mov     eax,cr0                   ;准备返回实模式
                and     al,11111110b
                mov     cr0,eax
                JUMP16  <SEG Real>,<OFFSET Real>
Virtual         ENDP
;----------------------------------------------------------------------------
TempCodeSeg     ENDS
;============================================================================
RDataSeg        SEGMENT PARA USE16                ;实方式数据段
VGDTR           PDesc   <GDTLen-1,>               ;GDT伪描述符
VIDTR           PDesc   <IDTLen-1,>               ;IDT伪描述符
NORVIDTR        PDesc   <3ffh,>                   ;用于保存原IDTR值
SPVar           DW      ?                         ;用于保存实方式下的SP
SSVar           DW      ?                         ;用于保存实方式下的SS
IMaskRegV       DB      ?                         ;用于保存原中断屏蔽寄存器值
RDataSeg        ENDS
;----------------------------------------------------------------------------
RCodeSeg        SEGMENT PARA USE16                ;实方式代码段
                ASSUME  CS:RCodeSeg,DS:RDataSeg
;----------------------------------------------------------------------------
Start           PROC
                mov     ax,RDataSeg
                mov     ds,ax
                cld
                call    InitGDT                   ;初始化全局描述符表GDT
                call    InitIDT                   ;初始化中断描述符表IDT
                mov     SSVar,ss                  ;保存堆栈指针
                mov     SPVar,sp
                sidt    QWORD PTR NORVIDTR        ;保存IDTR
                in      al,IMREGP
                mov     BYTE PTR IMaskRegV,al
                lgdt    QWORD PTR VGDTR           ;装载GDTR
                cli                               ;关中断
                lidt    QWORD PTR VIDTR           ;装载IDTR
                mov     eax,cr0
                or      al,1
                mov     cr0,eax
                JUMP16  <TempCode_Sel>,<OFFSET Virtual>
Real:           mov     ax,RDataSeg
                mov     ds,ax
                lss     sp,DWORD PTR SPVar        ;又回到实方式
                lidt    QWORD PTR NORVIDTR
                mov     al,IMaskRegV
                out     IMREGP,al
                sti
                mov     ax,4c00h
                int     21h
Start           ENDP
;----------------------------------------------------------------------------
InitGDT         PROC
                push    ds
                mov     ax,GDTSeg
                mov     ds,ax
                mov     cx,GDNum
                mov     si,OFFSET EFFGDT
InitG:          mov     ax,[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR [si].BaseL,ax
                mov     BYTE PTR [si].BaseM,dl
                mov     BYTE PTR [si].BaseH,dh
                add     si,SIZE Desc
                loop    InitG
                pop     ds
                mov     bx,16
                mov     ax,GDTSeg
                mul     bx
                mov     WORD PTR VGDTR.Base,ax
                mov     WORD PTR VGDTR.Base+2,dx
                ret
InitGDT         ENDP
;----------------------------------------------------------------------------
InitIDT         PROC
                mov     bx,16
                mov     ax,IDTSeg
                mul     bx
                mov     WORD PTR VIDTR.Base,ax
                mov     WORD PTR VIDTR.Base+2,dx
                ret
InitIDT         ENDP
;----------------------------------------------------------------------------
RCodeSeg        ENDS
                END     Start

2.关于实例六的说明

(1)时钟中断仍使用8H号中断向量

为了即简单又清楚地演示在保护模式下响应外部中断并进行处理，实例使用了时钟中断源， 但没有通过重新设置中断控制器的方法改变对应的中断向量。所以，时钟中断使用的8H号中 断向量号就与双重故障异常使用的中断向量号发生冲突。但实例仅是演示程序，所以只要保 证不发生双重故障异常，就可避免冲突，从而不会影响演示。
设置中断屏蔽寄存器，仅开放时钟中断。所以，在开中断状态下，也只可能发生时钟中断， 而不会发生其它外部中断。

(2)时钟中断处理程序的设计

由于通过中断门转时钟中断处理程序，所以在控制转移时不发生任务切换。但外部中断随时 可能发生，因此中断处理程序必须采取保护现场等措施。作为演示程序，该中断处理程序检 查和调整在其数据段中的计数器，满18次后，就认为已满一秒，再调整用于显示的倒计数信 息。如果倒计数信息为0，那么就设置演示程序数据段中的时间为0标志。该中断处理程序通 过约定的数据区与显示程序及演示程序交换信息。

(3)利用一个软中断(陷阱处理)程序实现显示

为了演示陷阱及其处理，把显示过程安排成陷阱处理程序。上述时钟中断处理程序通过软中 断调用指令INT调用该显示程序，以显示倒计数。在控制转移时，也没有任务切换。该陷阱 处理程序相当于一个“软中断”处理程序，类似实模式下的BIOS中断INT 10H。

(4)对其它中断或异常的响应

为了简单，除了8H号和0FEH号外，IDT中其它的门均通向一个处理程序。该处理程序用于处 理其它中断或异常。处理过程也极其简单，在屏幕左上角显示蓝底白字的符号“!”，然后 进入无限循环。实际上，按演示程序现在的安排，不可能发生这种情况。

(5)没有特权级变换

为了简单，实例涉及的中断处理程序和异常处理程序都保持特权级0。所以，控制转移时不 发生特权级变换。因此，没有使用其它堆栈。

(6)对IDT的初始化

由于IDT中门描述符没有32位段基地址，并且入口点偏移较小，所以就直接填写门描述符结 构变量，没有额外再初始化。过程InitIDT只是设置IDT伪描述符。

(7)装载和保存IDTR寄存器

再使IDT发挥作用之前，还要装载中断描述符表寄存器IDTR；但为了回到实模式后，恢复原 来的IDTR之内容，所以先保存IDTR的内容。实例使用如下指令保存IDTR：
    sidt    QWORD PTR NORVIDTR
该指令的功能是把IDTR的内容保存到存储器中的伪描述符NORVIDTR中。该伪描述符的结构如 前文所述的结构类型PDESC所示，低字是以字节为单位的界限，高双字是基地址。在后面的文 章中将对SIDT指令作详细说明。
本实例使用如下指令装载IDTR寄存器：
    lidt    QWORD PTR VIDTR
    lidt    QWORD PTR NORVIDTR
LIDT指令类似于LGDT指令，在后面的文章中将对LIDT指令作详细说明。

<五>演示异常处理的实例(实例七)

下面给出一个用于模拟异常和演示异常处理的实例。该实例的逻辑功能是，在屏幕上显示一条 提示用户以按键方式选择异常类型的字符，然后模拟指定的异常。该实例演示内容包括：除法 出错故障处理、溢出陷阱处理、段不存在故障处理、堆栈段出错处理和通用保护故障处理；还 有作为一个独立任务方式出现的陷阱处理程序。

1.源程序组织和清单

为了演示以独立任务方式出现的陷阱处理程序，实例含有两个任务：演示任务和读键盘任务。 实例由如下几部分组成：
(1)全局描述符表GDT和中断描述符表IDT；
(2)读键盘任务局部描述符表、任务状态段、堆栈段和代码段等；
(3)演示任务的局部描述符表、任务状态段、堆栈段、代码段和数据段等；
(4)作为演示任务一部分的有关陷阱处理和故障处理程序的代码段；
(5)作为演示任务一部分的显示出错码过程的代码段；
(6)实模式下执行的启动和结束程序代码段和数据段。
在切换到保护模式后，就进入临时代码段。为了简单，演示任务不发生特权级变换。演示步骤 如下：
(1)从临时代码段转移到演示代码段。
(2)做演示准备。把演示任务的LDT选择子装入LDTR，并填入TSS，装载任务寄存器TR，建立演示 任务堆栈，设置其它数据段寄存器。
(3)接收要模拟的异常类型号。通过软中断指令INT调用读键盘任务完成该步骤。读键盘任务只 有在接收到指定的字符后才结束。接收的字符是0、4、B、C和D。
按接收的字符模拟异常。即根据键入的字符，执行有关程序片段。在这些片段中，有意安排了 能引起有关故障或陷阱的指令。
结束演示，转临时代码段，返回DOS。
程序清单如下：
;名称:ASM7.ASM
;功能:模拟异常和演示异常处理
;编译:TASM ASM7.ASM
;连接:TLINK ASM7.OBJ
;----------------------------------------------------------------------------
INCLUDE         386SCD.INC
;----------------------------------------------------------------------------
GDTSeg          SEGMENT PARA USE16                ;全局描述符表数据段(16位)
;----------------------------------------------------------------------------
                ;全局描述符表GDT
GDT             LABEL   BYTE
                ;空描述符
DUMMY           Desc    <>
                ;规范段描述符及选择子
Normal          Desc    <0ffffh,,,ATDW,,>
Normal_Sel      =       Normal-GDT
                ;视频缓冲区段描述符(DPL=3)及选择子
VideoBuf        Desc    <0ffffh,8000h,0bh,ATDW,,>
VideoBuf_Sel    =       VideoBuf-GDT
;----------------------------------------------------------------------------
EFFGDT          LABEL   BYTE
                ;临时代码段描述符及选择子
TempCode        Desc    <0ffffh,TempCodeSeg,,ATCE,,>
TempCode_Sel    =       TempCode-GDT
                ;演示代码段描述符及选择子
DemoCode        Desc    <DemoCodeLen-1,DemoCodeSeg,,ATCE,,>
DemoCode_Sel    =       DemoCode-GDT
                ;演示任务局部描述符表段描述符及选择子
DemoLDT         Desc    <DemoLDTLen-1,DemoLDTSeg,,ATLDT,,>
DemoLDT_Sel     =       DemoLDT-GDT
                ;演示任务TSS段描述符及选择子
DemoTSS         Desc    <DemoTSSLen-1,DemoTSSSeg,,AT386TSS,,>
DemoTSS_Sel     =       DemoTSS-GDT
                ;缓冲数据段描述符及选择子
XBuffer         Desc    <BufferLen-1,BufferSeg,,ATDW,,>
XBuffer_Sel     =       XBuffer-GDT
                ;读键盘任务局部描述符表段描述符及选择子
GKeyLDT         Desc    <GKeyLDTLen-1,GKeyLDTSeg,,ATLDT,,>
GKeyLDT_Sel     =       GKeyLDT-GDT
                ;读键盘任务TSS段描述符及选择子
GKeyTSS         Desc    <GKeyTSSLen-1,GKeyTSSSeg,,AT386TSS,,>
GKeyTSS_Sel     =       GKeyTSS-GDT
                ;显示陷阱处理程序代码段描述符及选择子
EchoCode        Desc    <EchoCodeLen-1,EchoCodeSeg,,ATCE,,>
EchoCode_Sel    =       EchoCode-GDT
                ;显示出错码过程代码段描述符及选择子
SubCode         Desc    <SubCodeLen-1,SubCodeSeg,,ATCE,,>
SubCode_Sel     =       SubCode-GDT
                ;其它中断或异常处理程序代码段描述符及选择子
Other           Desc    <OtherCodeLen-1,OtherCodeSeg,,ATCE,,>
Other_Sel       =       Other-GDT
;----------------------------------------------------------------------------
GDTLen          =       $-GDT                     ;全局描述符表长度
GDNum           =       ($-EFFGDT)/(SIZE Desc)    ;需处理基地址的描述符个数
;----------------------------------------------------------------------------
GDTSeg          ENDS                              ;全局描述符表段定义结束
;----------------------------------------------------------------------------
IDTSeg          SEGMENT PARA USE16                ;中断描述符表数据段(16位)
;----------------------------------------------------------------------------
IDT             LABEL   BYTE                      ;中断描述符表
                ;0号陷阱门描述符(对应除法出错故障)
                Gate    <DivBegin,Divide_Sel,,AT386TGate,>
                ;从1--3的3个陷阱门描述符
                REPT    3
                Gate    <OtherBegin,Other_Sel,,AT386TGate,>
                ENDM
                ;4号陷阱门描述符(对应溢出陷阱)
                Gate    <OFBegin,OF_Sel,,AT386TGate,>
                ;从5--0ah的的6个陷阱门描述符
                REPT    6
                Gate    <OtherBegin,Other_Sel,,AT386TGate,>
                ENDM
                ;0bh号陷阱门描述符(对应段不存在故障)
                Gate    <SNPBegin,SNP_Sel,,AT386TGate,>
                ;0ch号陷阱门描述符(对应堆栈段故障)
                Gate    <SSEBegin,SSE_Sel,,AT386TGate,>
                ;0dh号陷阱门描述符(对应通用保护故障)
                Gate    <GPBegin,GP_Sel,,AT386TGate,>
                ;从0eh--0edh的240个陷阱门描述符
                REPT    240
                Gate    <OtherBegin,Other_Sel,,AT386TGate,>
                ENDM
                ;对应0feh号陷阱门描述符(对应显示中断处理程序)
                Gate    <EchoBegin,EchoCode_Sel,,AT386TGate,>
                ;0ffh号任务门描述符(对应读键盘中断处理任务)
                Gate    <,GKeyTSS_Sel,,ATTaskGate,>
;----------------------------------------------------------------------------
IDTLen          =       $-IDT
;----------------------------------------------------------------------------
IDTSeg          ENDS                              ;中断描述符表段定义结束
;----------------------------------------------------------------------------
;读键盘任务局部描述符表段
;----------------------------------------------------------------------------
GKeyLDTSeg      SEGMENT PARA USE16
;----------------------------------------------------------------------------
GLDT            LABEL   BYTE
                ;代码段描述符及选择子
GKeyCode        Desc    <0ffffh,GKeyCodeSeg,,ATCE,,>
GKeyCode_Sel    =       GKeyCode-GLDT+TIL
                ;堆栈段描述符及选择子
GKeyStack       Desc    <GKeyStackLen-1,GKeyStackSeg,,ATDWA,,>
GKeyStack_Sel   =       GKeyStack-GLDT+TIL
;----------------------------------------------------------------------------
GKeyLDNum       =       ($-GLDT)/(SIZE Desc)      ;需初始化基地址的描述符个数
GKeyLDTLen      =       $                         ;局部描述符表段长度
;----------------------------------------------------------------------------
GKeyLDTSeg      ENDS
;----------------------------------------------------------------------------
;读键盘任务TSS段
;----------------------------------------------------------------------------
GKeyTSSSeg      SEGMENT PARA USE16
                DD      0                         ;链接字
                DD      ?                         ;0级堆栈指针
                DW      ?,?
                DD      ?                         ;1级堆栈指针
                DW      ?,?
                DD      ?                         ;2级堆栈指针
                DW      ?,?
                DD      0                         ;CR3
                DW      GKeyBegin,0               ;EIP
                DD      0                         ;EFLAGS
                DD      0                         ;EAX
                DD      0                         ;ECX
                DD      0                         ;EDX
                DD      0                         ;EBX
                DW      GKeyStackLen,0            ;ESP
                DD      0                         ;EBP
                DD      0                         ;ESI
                DD      0                         ;EDI
                DW      Normal_Sel,0              ;ES
                DW      GKeyCode_Sel,0            ;CS
                DW      GKeyStack_Sel,0           ;SS
                DW      Normal_Sel,0              ;DS
                DW      Normal_Sel,0              ;FS
                DW      Normal_Sel,0              ;GS
                DW      GKeyLDT_Sel,0             ;LDTR
                DW      0                         ;调试陷阱标志
                DW      $+2                       ;指向I/O许可位图的偏移
                DB      0ffh                      ;I/O许可位图结束字节
GKeyTSSLen      =       $
GKeyTSSSeg      ENDS
;----------------------------------------------------------------------------
;读键盘任务堆栈段
;----------------------------------------------------------------------------
GKeyStackSeg    SEGMENT PARA USE16
GKeyStackLen    =       1024
                DB      GKeyStackLen DUP(0)
GKeyStackSeg    ENDS
;----------------------------------------------------------------------------
;读键盘任务代码段
;----------------------------------------------------------------------------
GKeyCodeSeg     SEGMENT PARA USE16
                ASSUME  CS:GKeyCodeSeg,DS:RDataSeg,ES:BufferSeg
;----------------------------------------------------------------------------
GKeyBegin       PROC    FAR
                push    ds
                push    es
                push    fs
                push    gs
                mov     ax,Normal_Sel
                mov     ss,ax                     ;准备转实方式
                mov     eax,cr0
                and     al,11111110b
                mov     cr0,eax                   ;转实方式
                JUMP16  <SEG GetKey>,<OFFSET GetKey>
GetKey:         mov     ax,RDataSeg               ;实方式
                mov     ds,ax
                mov     ebp,esp                   ;恢复实方式部分现场
                lss     sp,DWORD PTR SPVar
                lidt    QWORD PTR NORVIDTR
                sti
                mov     dx,OFFSET Mess
                mov     ah,9
                int     21h                       ;显示提示信息
GetKey1:        mov     ah,0
                int     16h                       ;读键盘
                cmp     al,'0'
                jz      GetKey2
                cmp     al,'4'
                jz      GetKey2
                and     al,11011111b              ;小写转大写
                cmp     al,'B'
                jb      GetKey1
                cmp     al,'D'
                ja      GetKey1                   ;只有[0,4,b,c,d]有效
GetKey2:        mov     dl,al
                mov     ah,2
                int     21h                       ;显示所按字符
                mov     ax,BufferSeg
                mov     es,ax
                mov     BYTE PTR es:KeyASCII,dl   ;保存到缓冲数据段
                cli                               ;准备返回保护方式
                lidt    QWORD PTR VIDTR
                mov     eax,cr0
                or      al,1
                mov     cr0,eax
                JUMP16  <GKeyCode_Sel>,<OFFSET GetKeyV>
GetKeyV:        mov     ax,GKeyStack_Sel          ;又进入保护方式
                mov     ss,ax
                mov     esp,ebp
                pop     gs
                pop     fs
                pop     es
                pop     ds
                iretd
                jmp     GKeyBegin
GKeyBegin       ENDP
;----------------------------------------------------------------------------
GKeyCodeLen     =       $
GKeyCodeSeg     ENDS
;----------------------------------------------------------------------------
;其它中断或异常处理程序的代码段
;----------------------------------------------------------------------------
OtherCodeSeg    SEGMENT PARA USE16
                ASSUME  CS:OtherCodeSeg
;----------------------------------------------------------------------------
OtherBegin      PROC    FAR
                mov     si,OFFSET MessOther
                int     0feh                      ;显示提示信息
                mov     WORD PTR es:[0],ax
                jmp     $                         ;进入无限循环
OtherBegin      ENDP
;----------------------------------------------------------------------------
OtherCodeLen    =       $
OtherCodeSeg    ENDS
;----------------------------------------------------------------------------
;除法出错故障处理程序代码段
;----------------------------------------------------------------------------
DivCodeSeg      SEGMENT PARA USE16
                ASSUME  CS:DivCodeSeg
;----------------------------------------------------------------------------
DivBegin        PROC    FAR
                mov     si,OFFSET Mess0
                mov     di,0
                int     0feh                      ;显示提示信息
                shr     ax,1                      ;处理模拟的除法错误
                iretd                             ;返回
DivBegin        ENDP
;----------------------------------------------------------------------------
DivCodeLen      =       $
DivCodeSeg      ENDS
;----------------------------------------------------------------------------
;溢出陷阱处理程序代码段
;----------------------------------------------------------------------------
OFCodeSeg       SEGMENT PARA USE16
                ASSUME  CS:OFCodeSeg
;----------------------------------------------------------------------------
OFBegin         PROC    FAR
                mov     si,OFFSET Mess4
                mov     di,0
                int     0feh                      ;显示提示信息
                iretd                             ;返回
OFBegin         ENDP
;----------------------------------------------------------------------------
OFCodeLen       =       $
OFCodeSeg       ENDS
;----------------------------------------------------------------------------
;段不存在故障处理程序代码段
;----------------------------------------------------------------------------
SNPCodeSeg      SEGMENT PARA USE16
                ASSUME  CS:SNPCodeSeg
;----------------------------------------------------------------------------
SNPBegin        PROC    FAR
                mov     si,OFFSET MessB
                mov     di,0
                int     0feh                      ;显示提示信息
                pop     eax                       ;弹出出错代码
                CALL16  SubCode_Sel,SubBegin      ;显示出错代码
                pop     eax
                add     eax,2                     ;按模拟的引起段不存在指令
                push    eax                       ;调整返回地址
                iretd
SNPBegin        ENDP
;----------------------------------------------------------------------------
SNPCodeLen      =       $
SNPCodeSeg      ENDS
;----------------------------------------------------------------------------
;堆栈段故障处理程序代码段
;----------------------------------------------------------------------------
SSECodeSeg      SEGMENT PARA USE16
                ASSUME  CS:SSECodeSeg
;----------------------------------------------------------------------------
SSEBegin        PROC    FAR
                mov     si,OFFSET MessC
                mov     di,0
                int     0feh                      ;显示提示信息
                pop     eax                       ;弹出出错代码
                CALL16  SubCode_Sel,SubBegin      ;显示出错代码
                pop     eax
                add     eax,4                     ;按模拟的引起堆栈段错误的
                push    eax                       ;指令调整返回地址
                iretd
SSEBegin        ENDP
;----------------------------------------------------------------------------
SSECodeLen      =       $
SSECodeSeg      ENDS
;----------------------------------------------------------------------------
;通用保护故障处理程序代码段
;----------------------------------------------------------------------------
GPCodeSeg       SEGMENT PARA USE16
                ASSUME  CS:GPCodeSeg
;----------------------------------------------------------------------------
GPBegin         PROC    FAR
                push    ebp
                mov     ebp,esp
                push    eax
                push    esi
                push    edi                       ;保护现场
                mov     si,OFFSET MessD
                mov     di,0
                int     0feh                      ;显示提示信息
                mov     eax,[bp+4]                ;从堆栈中取出出错代码
                CALL16  SubCode_Sel,SubBegin      ;显示出错代码
                pop     edi
                pop     esi
                pop     eax                       ;恢复部分现场
                add     DWORD PTR [ebp+8],2       ;按模拟的故障指令调整返回
                pop     ebp                       ;地址
                add     esp,4                     ;废除堆栈中的出错代码
                iretd
GPBegin         ENDP
;----------------------------------------------------------------------------
;显示出错码过程代码段
;----------------------------------------------------------------------------
SubCodeSeg      SEGMENT PARA USE16
                ASSUME  CS:SubCodeSeG
;----------------------------------------------------------------------------
SubBegin        PROC                              ;AX中含出错代码
                push    ax                        ;保护现场
                push    cx
                push    dx
                push    si
                push    di
                mov     si,OFFSET ErrCode
                mov     dx,ax
                mov     cx,4
SubR1:          rol     dx,4                      ;把16位出错代码转换成4位
                mov     al,dl                     ;十六进制数的ASCII码并保存
                and     al,0fh
                add     al,30h
                cmp     al,'9'
                jbe     SubR2
                add     al,7
SubR2:          mov     [si],al
                inc     si
                loop    SubR1
                mov     si,OFFSET ErrMess
                Mov     di,80*2                   ;从第二行行首开始
                int     0feh                      ;显示出错码
                pop     di                        ;恢复现场
                pop     si
                pop     dx
                pop     cx
                pop     ax
                retf                              ;返回
SubBegin        ENDP
;----------------------------------------------------------------------------
SubCodeLen      =       $
SubCodeSeg      ENDS
;----------------------------------------------------------------------------
GPCodeLen       =       $
GPCodeSeg       ENDS
;----------------------------------------------------------------------------
;实现显示的陷阱处理程序代码段
;入口参数--DS:SI指向显示信息串,ES:DI指向显示缓冲区
;----------------------------------------------------------------------------
EchoCodeSeg     SEGMENT PARA USE16
                ASSUME  CS:EchoCodeSeg
;----------------------------------------------------------------------------
EchoBegin       PROC    FAR
                pushad                            ;保护现场
                cld
                mov     ah,7
                mov     al,20h
                mov     cx,80
                push    di
                rep     stosw                     ;清所在显示行
                pop     di
Echo1:          lodsb
                or      al,al
                jz      Echo2
                stosw                             ;显示指定信息串
                jmp     Echo1
Echo2:          popad                             ;恢复现场
                iretd
EchoBegin       ENDP
;----------------------------------------------------------------------------
EchoCodeLen     =       $
EchoCodeSeg     ENDS
;----------------------------------------------------------------------------
;缓冲区数据段
;----------------------------------------------------------------------------
BufferSeg       SEGMENT PARA USE16
KeyASCII        DB      ?
Buffer          DB      128 DUP(?)
BufferLen       =       $
BufferSeg       ENDS
;----------------------------------------------------------------------------
;演示任务局部描述符表段
;----------------------------------------------------------------------------
DemoLDTSeg      SEGMENT PARA USE16
;----------------------------------------------------------------------------
DLDT            LABEL   BYTE
                ;演示任务TSS段作为数据段的描述符及选择子
ToDemoTSS       Desc    <DemoTSSLen-1,DemoTSSSeg,,ATDW,,>
ToDemoTSS_Sel   =       ToDemoTSS-DLDT+TIL
                ;演示任务堆栈段描述符及选择子
DemoStack       Desc    <DemoStackLen-1,DemoStackSeg,,ATDWA,,>
DemoStack_Sel   =       DemoStack-DLDT+TIL
                ;演示任务数据段描述符及选择子
DemoData        Desc    <DemoDataLen-1,DemoDataSeg,,ATDW,,>
DemoData_Sel    =       DemoData-DLDT+TIL
                ;除法出错故障处理程序代码段描述符及选择子
Divide          Desc    <DivCodeLen-1,DivCodeSeg,,ATCE,,>
Divide_Sel      =       Divide-DLDT+TIL
                ;溢出陷阱处理程序代码段描述符及选择子
OverFlow        Desc    <OFCodeLen-1,OFCodeSeg,,ATCE,,>
OF_Sel          =       OverFlow-DLDT+TIL
                ;段不存在故障处理程序代码段描述符及选择子
SNPCode         Desc    <SNPCodeLen-1,SNPCodeSeg,,ATCE,,>
SNP_Sel         =       SNPCode-DLDT+TIL
                ;堆栈段出错故障处理程序代码段描述符及选择子
SSECode         Desc    <SSECodeLen-1,SSECodeSeg,,ATCE,,>
SSE_Sel         =       SSECode-DLDT+TIL
                ;通用保护故障处理程序代码段描述符及选择子
GPCode          Desc    <GPCodeLen-1,GPCodeSeg,,ATCE,,>
GP_Sel          =       GPCode-DLDT+TIL
                ;为模拟段不存在故障而安排的数据段描述符及选择子
TestNPS         Desc    <0ffffh,,,ATDW-80h,,>
TestNPS_Sel     =       TestNPS-DLDT+TIL
;----------------------------------------------------------------------------
DemoLDNum       =       ($-DLDT)/(SIZE Desc)      ;LDT描述符个数
DemoLDTLen      =$
;----------------------------------------------------------------------------
DemoLDTSeg      ENDS
;----------------------------------------------------------------------------
;演示任务TSS段
;----------------------------------------------------------------------------
DemoTSSSeg      SEGMENT PARA USE16
DemoTaskSS      TSS     <>
                DB      0ffh
DemoTSSLen      =       $
DemoTSSSeg      ENDS
;----------------------------------------------------------------------------
;演示任务的堆栈段
;----------------------------------------------------------------------------
DemoStackSeg    SEGMENT PARA USE16
DemoStackLen    =       1024
                DB      DemoStackLen DUP(0)
DemoStackSeg    ENDS
;----------------------------------------------------------------------------
;演示任务的数据段
;----------------------------------------------------------------------------
DemoDataSeg     SEGMENT PARA USE16
Mess0           DB      'Divide Error (Exception 0)',0
Mess4           DB      'Overflow (Exception 4)',0
MessB           DB      'Segment Not Present (Exception 11)',0
MessC           DB      'Stack Segment (Exception 12)',0
MessD           DB      'General Protection (Exception 13)',0
MessOther       DB      'Other Exception',0
ErrMess         DB      'Error Code = '
ErrCode         DB      4 DUP(0),'H',0
DemoDataLen     =       $
DemoDataSeg     ENDS
;----------------------------------------------------------------------------
;演示任务的代码段
;----------------------------------------------------------------------------
DemoCodeSeg     SEGMENT PARA USE16
                ASSUME  CS:DemoCodeSeg,DS:DemoDataSeg
;----------------------------------------------------------------------------
DemoBegin       PROC    FAR
                mov     ax,DemoLDT_Sel
                lldt    ax                        ;装载LDTR
                mov     ax,DemoStack_Sel          ;置堆栈
                mov     ss,ax
                mov     esp,DemoStackLen
                mov     ax,ToDemoTSS_Sel
                mov     gs,ax                     ;把演示任务LDT选择子填入TSS
                mov     WORD PTR gs:DemoTaskSS.TRLDTR,DemoLDT_Sel
                mov     ax,DemoTSS_Sel
                ltr     ax                        ;装载TR
                mov     ax,DemoData_Sel
                mov     ds,ax                     ;装载其它数据段寄存器
                mov     ax,VideoBuf_Sel
                mov     es,ax
                mov     ax,XBuffer_Sel
                mov     fs,ax
                mov     ax,XBuffer_Sel
                mov     gs,ax
                int     0ffh                      ;接收要模拟的异常类型号
                mov     al,BYTE PTR fs:KeyASCII   ;按接收的字符模拟异常号
                cmp     al,'0'
                jnz     Demo4
                mov     ax,2000
                mov     cl,2                      ;模拟除法出错故障
                div     cl                        ;该指令长2字节
                jmp     Over
Demo4:          cmp     al,'4'
                jnz     Demo11
                mov     al,100
                add     al,50
                into                              ;模拟溢出陷阱
                JMP     OVER
Demo11:         cmp     al,'B'
                jnz     Demo12
                mov     ax,TestNPS_Sel            ;模拟段不存在故障
                mov     gs,ax                     ;该指令长2字节
                JMP     Over
Demo12:         cmp     al,'C'
                jnz     Demo13
                mov     ebp,esp                   ;模拟堆栈出错故障
                mov     al,[ebp]                  ;该指令长4字节
                jmp     Over
Demo13:         mov     ax,DemoTSS_Sel            ;模拟通用保护故障
                mov     gs,ax                     ;该指令长2字节
Over:           ;转临时代码段
                JUMP16  TempCode_Sel,<OFFSET ToDos>
DemoBegin       ENDP
;----------------------------------------------------------------------------
DemoCodeLen     =       $
DemoCodeSeg     ENDS
;----------------------------------------------------------------------------
TempCodeSeg     SEGMENT PARA USE16                ;临时任务的代码段
                ASSUME  CS:TempCodeSeg
;----------------------------------------------------------------------------
Virtual         PROC    FAR
                JUMP16  DemoCode_Sel,DemoBegin    ;转演示任务
ToDos:          mov     ax,Normal_Sel             ;恢复实方式段描述符高速缓存
                mov     ds,ax
                mov     es,ax
                mov     fs,ax
                mov     gs,ax
                mov     ss,ax
                mov     eax,cr0                   ;准备返回实模式
                and     al,11111110b
                mov     cr0,eax
                JUMP16  <SEG Real>,<OFFSET Real>
Virtual         ENDP
;----------------------------------------------------------------------------
TempCodeSeg     ENDS
;============================================================================
RDataSeg        SEGMENT PARA USE16                ;实方式数据段
VGDTR           PDesc   <GDTLen-1,>               ;GDT伪描述符
VIDTR           PDesc   <IDTLen-1,>               ;IDT伪描述符
NORVIDTR        PDesc   <3ffh,>                   ;用于保存原IDTR值
SPVar           DW      ?                         ;用于保存实方式下的SP
SSVar           DW      ?                         ;用于保存实方式下的SS
Mess            DB      'Press a key[0,4,B,C,D]:$';提示信息
RDataSeg        ENDS
;----------------------------------------------------------------------------
RCodeSeg        SEGMENT PARA USE16                ;实方式代码段
                ASSUME  CS:RCodeSeg,DS:RDataSeg
;----------------------------------------------------------------------------
Start           PROC
                mov     ax,RDataSeg
                mov     ds,ax
                cld
                call    InitGDT                   ;初始化全局描述符表GDT
                call    InitIDT                   ;初始化中断描述符表IDT
                mov     ax,GKeyLDTSeg
                mov     fs,ax
                mov     cx,GKeyLDNum
                mov     si,OFFSET GLDT
                CALL    InitLDT
                mov     ax,DemoLDTSeg
                mov     fs,ax
                mov     cx,DemoLDNum
                mov     si,OFFSET DLDT
                CALL    InitLDT
                mov     SSVar,ss                  ;保存堆栈指针
                mov     SPVar,sp
                lgdt    QWORD PTR VGDTR           ;装载GDTR
                sidt    QWORD PTR NORVIDTR        ;保存IDTR
                cli                               ;关中断
                lidt    QWORD PTR VIDTR           ;装载IDTR
                mov     eax,cr0
                or      al,1
                mov     cr0,eax
                JUMP16  <TempCode_Sel>,<OFFSET Virtual>
Real:           mov     ax,RDataSeg
                mov     ds,ax
                lss     sp,DWORD PTR SPVar        ;又回到实方式
                lidt    QWORD PTR NORVIDTR
                sti
                mov     ax,4c00h
                int     21h
Start           ENDP
;----------------------------------------------------------------------------
InitGDT         PROC
                push    ds
                mov     ax,GDTSeg
                mov     ds,ax
                mov     cx,GDNum
                mov     si,OFFSET EFFGDT
InitG:          mov     ax,[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR [si].BaseL,ax
                mov     BYTE PTR [si].BaseM,dl
                mov     BYTE PTR [si].BaseH,dh
                add     si,SIZE Desc
                loop    InitG
                pop     ds
                mov     bx,16
                mov     ax,GDTSeg
                mul     bx
                mov     WORD PTR VGDTR.Base,ax
                mov     WORD PTR VGDTR.Base+2,dx
                ret
InitGDT         ENDP
;----------------------------------------------------------------------------
InitIDT         PROC
                mov     bx,16
                mov     ax,IDTSeg
                mul     bx
                mov     WORD PTR VIDTR.Base,ax
                mov     WORD PTR VIDTR.Base+2,dx
                ret
InitIDT         ENDP
;----------------------------------------------------------------------------
;入口参数:FS:SI=第一个要初始化的描述符,CX=要初始化的描述符数
;----------------------------------------------------------------------------
InitLDT         PROC
ILDT:           mov     ax,WORD PTR FS:[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR fs:[si].BaseL,ax
                mov     BYTE PTR fs:[si].BaseM,dl
                mov     BYTE PTR fs:[si].BaseH,dh
                add     si,SIZE Desc
                loop    ILDT
                ret
InitLDT         ENDP
;----------------------------------------------------------------------------
RCodeSeg        ENDS
                END     Start

2.关于实例七的说明

上述模拟与演示程序的许多内容与实例六相同，下面就各异常处理程序和读键盘任务的实现 作些说明：

(1)除法出错故障处理程序的实现

从源程序可见，除法出错是在执行故意安排的被除数为2000，而除数为2的无符号除指令时 引起的。作为演示，除法出错故障处理程序先显示一条提示信息，然后把存放被除数AX的内 容右移一位，然后就返回。由于除法出错为故障类异常，所以在故障处理结束后，仍执行该 无符号除指令。显然将再次引起同样的故障，仍把被除数右移一位。由于每次处理时都把被 除数减半，所以几次故障后就不发生该故障了。

(2)溢出陷阱处理程序的实现

作为演示的溢出陷阱处理程序较简单。先显示一条提示信息，然后就返回。因为溢出异常为 陷阱类异常，所以在陷阱处理结束后，就直接返回到引起陷阱指令的下一条指令。

(3)段不存在故障处理程序的实现

从源程序可见，段不存在故障是在执行故意安排的把一个选择子送段寄存器GS的指令时引起 的。该选择子索引的描述符中的存在位P被置为0，表示对应段不在内存。在正常情况下，段 不存在故障处理程序要把对应的段装入内存，再把描述符内的P位修改为1，于是，在故障处 理结束后，引起故障的指令可得到顺序执行。为了简单，这里安排的故障处理程序先显示一 条提示信息，然后显示出错码，最后调整堆栈中的返回地址并返回。段不存在故障提供一个 出错码，该故障处理程序利用POP指令把它用堆栈中弹出，这样堆栈指针就指向返回地址。由 于段不存在异常属于故障类异常，所以返回点仍是引起故障的指令。因此，演示程序调整了 堆栈中的返回地址，使其返回到引起故障的指令的下一条指令。

(4)堆栈段出错故障处理程序的实现

引起堆栈出错故障的原因有多种，实例通过执行故意安排的偏移超过段界限的堆栈段访问指 令来模拟堆栈段出错故障的产生。作为演示的堆栈出错故障处理程序比较简单，先显示一条 信息，然后显示出错码，最后调整堆栈中的返回地址并返回。

(5)通用保护障处理程序的实现

引起通用保护故障处理程序的原因有多种，实例通过把一个指向系统段描述符的选择子装入 数据段寄存器GS来模拟通用保护故障的产生。作为演示的通用保护故障处理程序，象上述两 个故障处理程序一样比较简单，先显示一条提示信息，然后显示出错码，最后调整堆栈中的 返回地址并返回，但在废除堆栈中的出错码和调整堆栈中的返回地址时采用了其它方法。

(6)异常处理程序的一般说明

在实例中，通向上述各种异常处理程序的门都是陷阱门。所以，在发生异常而转入这些异常 处理程序时，都不发生任务切换。于是，这些异常处理仍作为演示任务的一部分。
正常情况下，异常处理程序应该注意现场的保护和恢复，但为了简单，作为演示的异常处理 程序没有能够切实地保护现场。注意，这些异常处理程序所采用的处理方法与所模拟的指令 有关，不适用于一般情况。

(7)显示出错代码的过程

实例采用一个过程用于显示出错代码，该过程的入口参数是AX含出错码。利用该过程不仅缩 短程序，而且也用于表现异常处理程序的实现。

(8)读键盘任务的实现

在实例的IDT中，0FFH号门描述符是任务门，指向一个独立的任务。该任务的功能是读键盘， 接收一个指定范围内的字符。演示任务通过指令“INT 0FFH”来调用它，接收一个代表需要 模拟异常的字符。
为了简单，该任务在实模式下读键盘，接收指定范围内的字符。为此，该任务每次经历如下 步骤：(1)转到实模式。此前要作必要的准备，转到实模式后，要恢复必须的实模式下的部 分现场。(2)接收指定的字符。调用DOS功能显示提示信息，调用BIOS中断读键盘，如果在指 定范围内，那么就显示，并保存在约定的数据段中。(3)转回到保护模式，此前也要作必要 的准备。
尽管在任务切换时，自动利用TSS保护和恢复现场，但由于该任务相当于一个读键盘的过程， 所以在开始任务时，还通过堆栈保护必要的现场，在结束任务时恢复现场。请特别注意，安 排在该任务代码段中的IRETD指令之后的转移指令的作用。当执行IRETD指令时，由于NT位 为1，所以按反向链进行任务切换，同时保存各寄存器到当前的TSS，为了下次进入时仍能从 头开始执行此任务，所以在IRETD后加一条转移到该任务开头的指令。

<六>各种转移途径小结

如上所述，中断/异常可引起任务切换、任务内特权级变换和任务内无特权级变换的转移。 至此，任务切换、任务内特权级变换和任务内无特权级变换转移的各种途径已全部列出。

1.任务切换的途径

任务之间切换的途径如下图所示。段间转移指令JMP、段间调用指令CALL、软中断指令INT和 中断返回指令IRET引起的任务切换是主动的任务切换，或者说是当前任务要求的任务切换。 中断和异常(不包括软中断指令)引起的任务切换是被动的任务切换，或者说是不受当前任务 左右的任务切换。
伴随着任务切换，特权级当然可能发生变换。只要任务切换发生，这种特权级的变换取决于 目标任务，而与当前任务无关。

2.任务内特权级变换的途径

任务内特权级变换的途径如下图所示。图中特权级m是外层特权级，特权级n是内层特权级。 通常RET与CALL对应；IRET与INT、中断/异常对应。但也可以通过在堆栈中建立合适环境的 手段，使RET或IRET从内层特权级变换到外层特权级。

3.任务内相同特权级转移的途径

任务内相同特权级转移的途径如下图所示。由图可见，任务内相同特权级转移的途径多种 多样。

参考资料 | 书 名 | 出 版 社 | 作 者
 | 《保护方式下的80386及其编程》 | 清华大学出版社 | 周明德主编
 | 《80X86汇编语言程序设计教程》 | 清华大学出版社 | 扬季文主编
 | 《32位系统软件编程指南》 | 电子工业出版社 | 程荷、武航翻译


八.操作系统类指令

本文介绍操作系统类指令。其中的某些指令始于286。通常只在操作系统代码中使用这些指令， 而不在应用程序中使用这些指令。这是把它们称为操作系统类指令的原因。为了保证操作系统 的安全，保护模式下的80386支持四个特权级。相应地，这些操作系统类指令也可分为三种：实 模式和任何特权级下可执行的指令、实模式及特权级0下可执行的指令和仅在保护模式下执行的 指令。这里下载本文源代码。

<一>实模式和任何特权级下可执行的指令

1.存储全局和中断描述符表寄存器指令

全局描述符表GDT和中断描述符表IDT包含着系统的重要数据，对应的两个描述符表寄存 器GDTR和IDTR含有这两张表的定位信息。利用存储描述符表寄存器指令能把描述符表寄存器 的内容保存到指定的存储单元。这样，访问这些存储单元就可获得描述符表的定位信息。 与GDT和IDT被所有任务共享不同，LDT是每个任务私有的，所以存储局部描述符表寄存器LDTR的 指令不在所列。

(1)存储全局描述符表寄存器指令

存储全局描述符表寄存器指令的格式如下：
    SGDT    QWORD PTR DST
其中操作数DST是48位(6字节)的存储器操作数。该指令的功能是把全局描述符表寄存器GDTR的 内容存储到存储单元DST。GDTR中的16位界限存入DST的低字，GDTR中的32位基地址存入DST的 高双字。该指令对标志没有影响。

(2)存储中断描述符表寄存器指令

存储中断描述符表寄存器指令的格式如下：
    SIDT    QWORD PTR DST
其中操作数DST是48位(6字节)的存储器操作数。该指令的功能是把中断描述符表寄存器IDTR的 内容存储到存储单元DST。IDTR中的16位界限存入DST的低字，IDTR中的32位基地址存入DST的 高双字。该指令对标志没有影响。

2.存储机器状态字指令

存储机器状态字指令的格式如下：
    SMSW    DST
其中操作数DST可以是16位(字)的存储器操作数或寄存器。该指令的功能是把机器状态字的 内容存储到DST。该指令对标志没有影响。
80386有此指令是为了包含80286的指令集，以便与其兼容。由于80386的控制寄存器CR0的低16位 等同于80286的机器状态字，所以，在为80386编程时，如果需要存储机器状态字，那么最好使用 存储CR0寄存器的指令。

<二>实模式及特权级0下可执行的指令

下列指令涉及设置关键的寄存器，所以只能在实模式和保护模式的特权级0下执行。为了从初始 时的实模式转入保护模式，必须做基本的准备工作。例如，设置妥全局描述符表寄存器GDTR等。 这是允许下列指令在实模式下工作的原因。
在保护模式下，如果当前特权级(CPL)不为0，执行这些指令将引起错误码为0的通用保护故障。 在虚拟8086方式下，因为其CPL为3，所以执行这些指令也将会引起出错码为0的通用保护故障。

1.清任务切换标志指令

每当任务切换时，控制寄存器CR0中的任务切换标志TS被自动置1。这样安排的原因已在前文中 说明过。清任务切换标志指令的功能是把TS标志清0，该指令的格式如下：
    CLTS
该指令仅影响TS标志，对其它标志没有影响。

2.暂停指令

暂停指令的格式如下：
    HLT
该指令使处理器暂停执行。暂停之后的系统，只有在接受一个已经启用的中断或让系统复位， 才能重新启动。该指令对标志没有影响。

3.装载全局描述符表和中断描述符表寄存器的指令

(1)装载全局描述符表寄存器指令

装载全局描述符表寄存器指令的格式如下：
    LGDT    QWORD PTR SRC
其中操作数SRC是48位(6字节)的存储器操作数。该指令的功能是把存储器中的伪描述符装入到 全局描述符表寄存器GDTR中。伪描述符SRC的结构如前文所述结构类型PDESC所示，低字是以字 节为单位的段界限，高双字是段基地址。该指令对标志没有影响。

(2)装载中断描述符表寄存器指令

装载中断描述符表寄存器指令的格式如下：
    LIDT    QWORD PTR SRC
其中操作数SRC是48位(6字节)的存储器操作数。该指令的功能是把存储器中的伪描述符装入到 中断描述符表寄存器IDTR中。伪描述符SRC的结构如前文所述结构类型PDESC所示，低字是以字 节为单位的段界限，高双字是段基地址。该指令对标志没有影响。

4.装载机器状态字指令

装载机器状态字指令的格式如下：
    LMSW    DST
其中操作数DST可以是16位(字)的存储器操作数或寄存器。该指令的功能是把DST的内容装载到 机器状态字。该指令对标志没有影响。
将PE位置为1，便进入保护模式。在80286中，没有控制寄存器，为进入保护模式需要通过该指 令把MSW中的PE位置为1。如果的确是这样，那么LMSW指令后面必须紧跟一条转移指令。
80386有此指令是为了包含80286的指令集，以便与其兼容。由于80386的控制寄存器CR0的低16位 等同于80286的机器状态字，所以，在为80386编程时，如果需要装载机器状态字，那么最好使用 控制寄存器传送指令。

5.控制寄存器数据传送指令

控制寄存器数据传送指令的一般格式如下：
    MOV     DST,SRC
控制寄存器数据传送指令实现80386的控制寄存器和32位通用寄存器之间的数据传送。所以，操 作数SRC和DST可以是80386使用的三个控制寄存器和任一32位通用寄存器，但不能同时是控制寄 存器。该指令对标志没有影响。

6.调试寄存器数据传送指令指令

调试寄存器数据传送指令的一般格式与上面的控制寄存器数据传送指令的格式相同。功能是实 现80386的调试寄存器和32位通用寄存器之间的数据传送。操作数SRC和DST可以是80386使用 的6个调试寄存器和任一32位通用寄存器，但不能同时为调试寄存器。该指令不影响标志。
80386可使用的6个调试寄存器记为：DR0、DR1、DRW、DR3、DR6和DR7。其它说明与控制寄存器 数据传送指令相同。

7.测试寄存器数据传送指令

测试寄存器数据传送指令的一般格式与上面的控制寄存器数据传送指令的格式相同。功能 是实现80386的测试寄存器和32位通用寄存器之间的数据传送。80386使用的2个测试寄存器 是TR6和TR7。其它说明与控制寄存器数据传送指令相同。

<三>只能在保护模式下执行的指令

下面介绍的指令只能在保护模式下执行，如果在实模式下执行这些指令，将引起非法操作码 故障(向量号为6)。

1.装载和存储局部描述符表寄存器指令

(1)装载局部描述符表寄存器指令

装载局部描述符表寄存器指令的格式如下：
    LLDT    SRC
其中，操作数SRC可以是16位通用寄存器或存储单元。该指令的功能是把SRC中的内容作为 指示局部描述符表LDT的选择子装入到LDTR寄存器。该指令不影响标志。
操作数SRC给定的选择子应该指示GDT中的类型为LDT的描述符。但LRC也可是一个空选择子， 如果这样的话，表示暂时不使用局部描述符表LDT。
若CPL不为0，那么执行该指令将产生出错码为0的通用保护故障。若被装载的选择子不指 示GDT中的描述符，或者描述符类型不是LDT描述符，那么产生通用保护故障，错误码由该 选择子构成。
象段寄存器那样，LDTR也有两部分。在把指示LDT的选择子装入到LDTR可见部分时，处理器 自动把选择子所索引的LDT描述符中的段基地址等信息保存到不可见的高速缓冲寄存器中。

(2)存储局部描述符表寄存器指令

存储局部描述符表寄存器指令的格式如下：
    SLDT    DST
其中，操作数DST可以是16位通用寄存器或存储单元。该指令的功能是把局部描述符表寄存器 LDTR的内容存储到存储单元DST中，也即把指向当前任务LDT的选择子存储到DST中。该指令不 影响标志。

2.装载和存储任务寄存器指令

任务寄存器TR指示当前任务状态段TSS。随着任务的切换，TR的内容也随之改变；如果任务嵌 套，那么TR的原值作为链接字保存到新任务的TSS中。但有时候需要直接地装载或者保存TR， 这就需要使用装载TR指令和存储TR指令。

(1)装载任务寄存器指令

装载任务寄存器指令的格式如下：
    LTR     SRC
其中，操作数SRC可以是16位通用寄存器或存储单元。该指令的功能是将SRC作为指示TSS描述符 的选择子装载到任务寄存器TR。由前文可知，TR分为两部分，即程序员可见部分和不可见的高 速缓冲寄存器部分。在把TSS的选择子装入到TR可见部分时，处理器自动把选择子所索引的描述 符中的段基地址等信息保存到不可见的高速缓冲寄存器中。所以，SRC表示的选择子不能为空， 必须索引位于GDT中的描述符，并且描述符类型必须是可用TSS，该加载的TSS被处理器自动标为 “忙”。该指令对标志没有影响。
若CPL不为0，那么执行该指令将产生错误码为0的通用保护故障。若被加载的选择子不指示GDT中 的可用TSS描述符，那么产生通用保护故障，错误码由该选择子构成。

(2)存储任务寄存器指令

存储任务寄存器指令的格式如下：
    STR     DST
其中，操作数DST可以是16位通用寄存器或存储单元。该指令的功能是把TR所含的指示当前任 务TSS描述符的选择子存储到DST。该指令不影响标志。

3.调整申请特权级指令

调整申请特权级指令的格式如下：
    ARPL    OPRD1,OPRD2
其中，操作数OPRD1可以是16位通用寄存器或存储单元，操作数OPRD2是16位通用寄存器。该指 令把操作数OPRD1和OPRD2视为两个选择子，用OPRD2的申请特权级(RPL)去检查OPRD1的RPL。选 择子OPRD1和OPRD2的RPL分别由它们的最低2个位规定。如果OPRD1的RPL小于OPRD2的RPL，那么 零标志ZF被置1，并把OPRD2的RPL值赋予OPRD1的RPL(使两个操作数的最低2位相等)；否则，零 标志ZF被清0。OPRD1和OPRD2都可为空选择子。该指令只影响ZF标志。

4.装载存取权指令

装载存取权指令的格式如下：
    LAR     OPRD1,OPRD2
其中，操作数OPRD1可以是16位或32位通用寄存器，操作数OPRD2是16位通用寄存器或存储单 元，也可以是32位通用寄存器或存储单元。操作数OPRD1和OPRD2的尺寸必须一致。该指令把 操作数OPRD2视为选择子(当为32位时，仅使用低16位)，如果OPRD2所指示的描述符满足如下 条件，那么零标志ZF被置1，并把描述符内的属性字段装入OPRD1；否则，ZF清0，OPRD1保持 不变。
(1)在描述符表的范围内；
(2)是存储段描述符或系统段描述符或任务门描述符或调用门描述符；
(3)CPL和OPRD2的RPL都不大于DPL。
在满足条件的情况下，装入到OPRD1的由OPRD2所指示的描述符中的属性字段是指描述符的 高4字节和00FXFF00H相与的结果，其中X表示第16位到第19位无定义。注意，如果指令使 用16位操作数，那么只有高4字节中的低字被装入到OPRD1，即装入到OPRD1的属性字段不 包括G位和AVL位等。该指令只影响ZF标志。

5.装载段界限指令

装载界限指令的格式如下：
    LSL     OPRD1,OPRD2
其中，操作数OPRD1可以是16位或32位通用寄存器，操作数OPRD2是16位通用寄存器或存储单 元，也可以是32位通用寄存器或存储单元。操作数OPRD1和OPRD2的尺寸必须一致。该指令把 操作数OPRD2视为选择子(当为32位时，仅使用低16位)，如果OPRD2所指示的描述符满足如下 条件，那么零标志ZF被置1，并把描述符内的界限字段装入OPRD1；否则，ZF清0，OPRD1保持 不变。
(1)在描述符表的范围内；
(2)是存储段描述符或系统段描述符，而非门描述符；
(3)CPL和OPRD2的RPL都不大于DPL。
在满足条件的情况下，装入到OPRD1的由OPRD2所指示的描述符中的界限字段以字节位为单位。 如果描述符中的界限字段以4K字节为单位(G=1)，那么装入到OPRD1时被左移12位，空出的低位 全部填成1。注意，如果指令使用16位操作数，那么只有段界限的低16位被装入到OPRD1。该指 令只影响ZF标志。

6.读写检验指令

利用读检验指令和写检验指令可分别检查在当前特权级上指定的段能否读或写，从而避免引起 不必要的异常。

(1)读检验指令

读检验指令的一般格式如下：
    VERR    OPRD
其中，操作数OPRD可以是16位通用寄存器或存储单元，也可以是32位通用寄存器或存储单元。 该指令的功能是把OPRD的内容作为一个选择子(当32位时仅使用低16位)，判断在当前特权级上 该选择子所指示的段是否可读。如果该选择子指示合法的一个存储段描述符，并且在当前特权 级上可读所描述的段，那么零标志ZF被置为1，否则ZF被清0。该指令只影响ZF标志。

(2)写检验指令

    VERW    OPRD
其中，操作数OPRD可以是16位通用寄存器或存储单元，也可以是32位通用寄存器或存储单元。 该指令的功能是把OPRD的内容作为一个选择子(当32位时仅使用低16位)，判断在当前特权级上 该选择子所指示的段是否可写。如果该选择子指示合法的一个存储段描述符，并且在当前特权 级上可写所描述的段，那么零标志ZF被置为1，否则ZF被清0。该指令只影响ZF标志。

<四>显示关键寄存器内容的实例(实例八)

为了更好地说明操作系统类指令的使用，下面给出一个显示80386关键寄存器内容的实例。该实 例的逻辑功能是，显示系统中GDTR、IDTR、LDTR和TR等关键寄存器的当前内容。实例八的源程 序清单如下：
;名称:ASM8.ASM
;功能:显示关键寄存器内容及说明操作系统类指令的使用
;编译:TASM ASM8.ASM
;连接:TLINK ASM8.OBJ
;----------------------------------------------------------------------------
INCLUDE         386SCD.INC
;----------------------------------------------------------------------------
GDTSeg          SEGMENT PARA USE16                ;全局描述符表数据段(16位)
;----------------------------------------------------------------------------
GDT             LABEL   BYTE
                ;空描述符
DUMMY           Desc    <>
                ;规范段描述符及选择子
Normal          Desc    <0ffffh,,,ATDW,,>
Normal_Sel      =       Normal-GDT
;----------------------------------------------------------------------------
EFFGDT          LABEL   BYTE
                ;临时任务代码段描述符及选择子
TempCode        Desc    <0ffffh,TempCodeSeg,,ATCE,,>
TempCode_Sel    =       TempCode-GDT
                ;缓冲区段描述符及选择子
Buffer          Desc    <BufferLen-1,BufferSeg,,ATDW,,>
Buffer_Sel      =       Buffer-GDT
                ;测试描述符1及选择子
Test1           Desc    <1111h,,,92h,87h,>
Test1_Sel       =       Test1-GDT
TestR_Sel       =       Test1-GDT+RPL3
                ;测试描述符2及选择子
Test2           Desc    <2222h,,,82h,17h,>
Test2_Sel       =       Test1-GDT
;----------------------------------------------------------------------------
GDNum           =       ($-EFFGDT)/(SIZE Desc)    ;需特殊处理的描述符数
GDTLen          =       $-GDT                     ;全局描述符表长度
;----------------------------------------------------------------------------
GDTSeg          ENDS                              ;全局描述符表段定义结束
;----------------------------------------------------------------------------
BufferSeg       SEGMENT PARA USE16                ;缓冲区数据段
;----------------------------------------------------------------------------
GDTR_V          PDesc   <>                        ;存放GDTR
IDTR_V          PDesc   <>                        ;存放IDTR
;----------------------------------------------------------------------------
MSW_V           DW      0                         ;存放机器状态字
LDTR_V          DW      0                         ;存放LDTR选择子
TR_V            DW      0                         ;存放TR选择子
CR0_V           DD      0                         ;存放控制寄存器CR0
CR3_V           DD      0                         ;存放控制寄存器CR3
DR7_V           DD      0                         ;存放调试寄存器DR7
Test_RPL        DW      0
;----------------------------------------------------------------------------
Test1_SLD       DD      0                         ;演示用变量
Test1_ARD       DD      0
Test1_SLW       DW      0
Test1_ARW       DW      0
Test1_RF        DW      0
Test1_WF        DW      0
;----------------------------------------------------------------------------
Test2_SLD       DD      0                         ;演示用变量
Test2_ARD       DD      0
Test2_SLW       DW      0
Test2_ARW       DW      0
Test2_RF        DW      0
Test2_WF        DW      0
;----------------------------------------------------------------------------
BufferLen       =       $
BufferSeg       ENDS
;----------------------------------------------------------------------------
TempCodeSeg     SEGMENT PARA USE16                ;临时代码段
                ASSUME  CS:TempCodeSeg,DS:BufferSeg
;----------------------------------------------------------------------------
Virtual         PROC    FAR
                mov     ax,Buffer_Sel
                mov     ds,ax
                mov     eax,cr0                   ;存储CR0
                mov     CR0_V,eax
                mov     eax,cr3                   ;存储CR3
                mov     CR3_V,eax
                mov     eax,DR7                   ;存储DR7
                mov     DR7_V,eax
                str     TR_V                      ;存储TR
                sldt    LDTR_V                    ;存储LDTR
                mov     Test_RPL,Test1_Sel
                mov     ax,TestR_Sel
                arpl    Test_RPL,ax               ;说明调整申请特权及指令
                mov     bx,0
                mov     ax,Test1_Sel
Lab1:           mov     edx,0
                mov     cx,0
                lsl     edx,eax                   ;说明装载段界限指令
                lsl     cx,ax
                mov     Test1_SLD[bx],edx
                mov     Test1_SLW[bx],cx
                mov     edx,0
                mov     cx,0
                lar     edx,eax                   ;说明装载存取权指令
                lar     cx,ax
                mov     Test1_ARD[bx],edx
                mov     Test1_ARW[bx],cx
                mov     Test1_RF[bx],0
                verr    ax                        ;说明读检验指令
                jnz     Lab2
                mov     Test1_RF[bx],1
Lab2:           mov     Test1_WF[bx],0
                verw    ax                        ;说明写检验指令
                jnz     Lab3
                mov     Test1_WF[bx],1
Lab3:           add     bx,16
                mov     ax,Test2_Sel
                cmp     bx,32
                jb      Lab1
                ;准备返回实方式
                mov     ax,Normal_Sel
                mov     ds,ax
                mov     eax,cr0
                and     al,11111110b
                mov     cr0,eax                   ;返回实方式
                JUMP16  <SEG Real>,<OFFSET Real>
Virtual         ENDP
;----------------------------------------------------------------------------
TempCodeSeg     ENDS
;----------------------------------------------------------------------------
RCodeSeg        SEGMENT PARA USE16
                ASSUME  CS:RCodeSeg,DS:BufferSeg
;----------------------------------------------------------------------------
VGDTR           PDesc   <GDTLen-1,>
;----------------------------------------------------------------------------
Start           PROC
                mov     ax,BufferSeg
                mov     ds,ax
                sgdt    GDTR_V
                sidt    IDTR_V
                smsw    MSW_V
                ;准备转入保护方式
                push    cs
                pop     ds
                cld
                call    InitGDT
                mov     bx,OFFSET VGDTR
                lgdt    [bx]
                cli
                mov     eax,cr0
                or      al,1
                ;转入保护方式
                mov     cr0,eax
                JUMP16  <TempCode_Sel>,<OFFSET Virtual>
Real:           ;回到实方式
                sti
                ;为了简单，略去了显示相关变量内容的部分代码
                mov     ax,4c00h
                int     21h
Start           ENDP
;----------------------------------------------------------------------------
InitGDT         PROC
                push    ds
                mov     ax,GDTSeg
                mov     ds,ax
                mov     cx,GDNum
                mov     si,OFFSET EFFGDT
InitG:          mov     ax,[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR [si].BaseL,ax
                mov     BYTE PTR [si].BaseM,dl
                mov     BYTE PTR [si].BaseH,dh
                add     si,SIZE Desc
                loop    InitG
                pop     ds
                mov     bx,16
                mov     ax,GDTSeg
                mul     bx
                mov     WORD PTR VGDTR.Base,ax
                mov     WORD PTR VGDTR.Base+2,dx
                ret
InitGDT         ENDP
;----------------------------------------------------------------------------
RCodeSeg        ENDS
                END     Start

<五>特权指令

特权指令是指保护方式下只有当前特权级CPL=0时，才可执行的指令。如果CPL不等于0而 执行它们，那么会引起通用保护异常。从上面介绍的操作系统类指令可归纳出如下表所示 的80386特权指令。这些特权指令在构成完善的保护机制方面起了重要的作用。

特 权 指 令 | 指 令 | 功 能 | 指 令 | 功 能
 | CLTS | 清除CR0中的TS位 | LTR | 装入TR寄存器
 | HLT | 停机 | MOV CRn,reg | 装入控制寄存器
 | LGDT | 装入GDTR寄存器 | MOV reg,CRn | 保存控制寄存器
 | LIDT | 装入IDTR寄存器 | MOV DRn,reg | 装入调试寄存器
 | LLDT | 装入LDTR寄存器 | MOV reg,DRn | 保存调试寄存器
 | LMSW | 装入MSW寄存器(CR0的低16位) |  |

从上表可见，装入GDTR、IDTR、LDTR、TR和MSW的指令都是特权指令，而存储上述寄存器的 指令不是特权指令。这表示，保护模式下任何程序可获得这些寄存器的值，但只有特权 级0的程序才能够改变这些寄存器的值。从上表还可以看出，设置和存储控制寄存器及调试 寄存器的指令都是特权指令。


九.输入/输出保护

为了支持多任务，80386不仅要有效地实现任务隔离，而且还要有效地控制各任务的输入/输出， 避免输入/输出冲突。本文将介绍输入输出保护。 这里下载本文源代码。

<一>输入/输出保护

80386采用I/O特权级IPOL和I/O许可位图的方法来控制输入/输出，实现输入/输出保护。

1.I/O敏感指令

输入输出特权级(I/O Privilege Level)规定了可以执行所有与I/O相关的指令和访问I/O空间中 所有地址的最外层特权级。IOPL的值在如下图所示的标志寄存器中。

标 志 寄存器 | BIT31—BIT18 | BIT17 | BIT16 | BIT15 | BIT14 | BIT13—BIT12 | BIT11 | BIT10 | BIT9 | BIT8 | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0
 | 00000000000000 | V M | R F | 0 | N T | IOPL | OF | D F | I F | T F | S F | Z F | 0 | A F | 0 | P F | 1 | C F

I/O许可位图规定了I/O空间中的哪些地址可以由在任何特权级执行的程序所访问。I/O许可位图 在任务状态段TSS中。

I/O敏感指令 | 指令 | 功能 | 保护方式下的执行条件
 | CLI | 清除EFLAGS中的IF位 | CPL<=IOPL
 | STI | 设置EFLAGS中的IF位 | CPL<=IOPL
 | IN | 从I/O地址读出数据 | CPL<=IOPL或I/O位图许可
 | INS | 从I/O地址读出字符串 | CPL<=IOPL或I/O位图许可
 | OUT | 向I/O地址写数据 | CPL<=IOPL或I/O位图许可
 | OUTS | 向I/O地址写字符串 | CPL<=IOPL或I/O位图许可

上表所列指令称为I/O敏感指令，由于这些指令与I/O有关，并且只有在满足所列条件时才可以执行， 所以把它们称为I/O敏感指令。从表中可见，当前特权级不在I/O特权级外层时，可以正常执行所列的 全部I/O敏感指令；当特权级在I/O特权级外层时，执行CLI和STI指令将引起通用保护异常，而其它四 条指令是否能够被执行要根据访问的I/O地址及I/O许可位图情况而定(在下面论述)，如果条件不满足 而执行，那么将引起出错码为0的通用保护异常。
由于每个任务使用各自的EFLAGS值和拥有自己的TSS，所以每个任务可以有不同的IOPL，并且可以定 义不同的I/O许可位图。注意，这些I/O敏感指令在实模式下总是可执行的。

2.I/O许可位图

如果只用IOPL限制I/O指令的执行是很不方便的，不能满足实际要求需要。因为这样做会使得在特权 级3执行的应用程序要么可访问所有I/O地址，要么不可访问所有I/O地址。实际需要与此刚好相反， 只允许任务甲的应用程序访问部分I/O地址，只允许任务乙的应用程序访问另一部分I/O地址，以避免 任务甲和任务乙在访问I/O地址时发生冲突，从而避免任务甲和任务乙使用使用独享设备时发生冲突。
因此，在IOPL的基础上又采用了I/O许可位图。I/O许可位图由二进制位串组成。位串中的每一位依次 对应一个I/O地址，位串的第0位对应I/O地址0，位串的第n位对应I/O地址n。如果位串中的第位为0， 那么对应的I/O地址m可以由在任何特权级执行的程序访问；否则对应的I/O地址m只能由在IOPL特权级 或更内层特权级执行的程序访问。如果在I/O外层特权级执行的程序访问位串中位值为1的位所对应 的I/O地址，那么将引起通用保护异常。
I/O地址空间按字节进行编址。一条I/O指令最多可涉及四个I/O地址。在需要根据I/O位图决定是否可 访问I/O地址的情况下，当一条I/O指令涉及多个I/O地址时，只有这多个I/O地址所对应的I/O许可位 图中的位都为0时，该I/O指令才能被正常执行，如果对应位中任一位为1，就会引起通用保护异常。
80386支持的I/O地址空间大小是64K，所以构成I/O许可位图的二进制位串最大长度是64K个位，即位图 的有效部分最大为8K字节。一个任务实际需要使用的I/O许可位图大小通常要远小于这个数目。
当前任务使用的I/O许可位图存储在当前任务TSS中低端的64K字节内。I/O许可位图总以字节为单位 存储，所以位串所含的位数总被认为是8的倍数。从前文中所述的TSS格式可见，TSS内偏移66H的字 确定I/O许可位图的开始偏移。由于I/O许可位图最长可达8K字节，所以开始偏移应小于56K，但必须 大于等于104，因为TSS中前104字节为TSS的固定格式，用于保存任务的状态。

1.I/O访问许可检查细节

保护模式下处理器在执行I/O指令时进行许可检查的细节如下所示。
(1)若CPL<=IOPL，则直接转步骤(8)；
(2)取得I/O位图开始偏移；
(3)计算I/O地址对应位所在字节在I/O许可位图内的偏移；
(4)计算位偏移以形成屏蔽码值，即计算I/O地址对应位在字节中的第几位；
(5)把字节偏移加上位图开始偏移，再加1，所得值与TSS界限比较，若越界，则产生出错码 为0的通用保护故障；
(6)若不越界，则从位图中读对应字节及下一个字节；
(7)把读出的两个字节与屏蔽码进行与运算，若结果不为0表示检查未通过，则产生出错码 为0的通用保护故障；
(8)进行I/O访问。
设某一任务的TSS段如下：
TSSSEG                  SEGMENT PARA USE16
                        TSS     <>             ;TSS低端固定格式部分
                        DB      8 DUP(0)       ;对应I/O端口00H—3FH
                        DB      10000000B      ;对应I/O端口40H—47H
                        DB      01100000B      ;对用I/O端口48H—4FH
                        DB      8182 DUP(0ffH) ;对应I/O端口50H—0FFFFH
                        DB      0FFH           ;位图结束字节
TSSLen                  =       $
TSSSEG                  ENDS
再假设IOPL=1，CPL=3。那么如下I/O指令有些能正常执行，有些会引起通用保护异常：
                        in      al,21h  ;(1)正常执行
                        in      al,47h  ;(2)引起异常
                        out     20h,al  ;(3)正常实行
                        out     4eh,al  ;(4)引起异常
                        in      al,20h  ;(5)正常执行
                        out     20h,eax ;(6)正常执行
                        out     4ch,ax  ;(7)引起异常
                        in      ax,46h  ;(8)引起异常
                        in      eax,42h ;(9)正常执行
由上述I/O许可检查的细节可见，不论是否必要，当进行许可位检查时，80386总是从I/O许可位图 中读取两个字节。目的是为了尽快地执行I/O许可检查。一方面，常常要读取I/O许可位图的两个字 节。例如，上面的第(8)条指令要对I/O位图中的两个位进行检查，其低位是某个字节的最高位，高 位是下一个字节的最低位。可见即使只要检查两个位，也可能需要读取两个字节。另一方面，最多 检查四个连续的位，即最多也只需读取两个字节。所以每次要读取两个字节。这也是在判别是否越 界时再加1的原因。为此，为了避免在读取I/O许可位图的最高字节时产生越界，必须在I/O许可位 图的最后填加一个全1的字节，即0FFH。此全1的字节应填加在最后一个位图字节之后，TSS界限范 围之前，即让填加的全1字节在TSS界限之内。
I/O许可位图开始偏移加8K所得的值与TSS界限值二者中较小的值决定I/O许可位图的末端。当TSS的 界限大于I/O许可位图开始偏移加8K时，I/O许可位图的有效部分就有8K字节，I/O许可检查全部根 据全部根据该位图进行。当TSS的界限不大于I/O许可位图开始偏移加8K时，I/O许可位图有效部分 就不到8K字节，于是对较小I/O地址访问的许可检查根据位图进行，而对较大I/O地址访问的许可检 查总被认为不可访问而引起通用保护故障。因为这时会发生字节越界而引起通用保护异常，所以在 这种情况下，可认为不足的I/O许可位图的高端部分全为1。利用这个特点，可大大节约TSS中I/O许 可位图占用的存储单元，也就大大减小了TSS段的长度。

<二>重要标志保护

输入输出的保护与存储在标志寄存器EFLAGS中的IOPL密切相关，显然不能允许随便地改变IOPL，否 则就不能有效地实现输入输出保护。类似地，对EFLAGS中的IF位也必须加以保护，否则CLI和STI作 为敏感指令对待是无意义的。此外，EFLAGS中的VM位决定着处理器是否按虚拟8086方式工作。
80386对EFLAGS中的这三个字段的处理比较特殊，只有在较高特权级执行的程序才能执 行IRET、POPF、CLI和STI等指令改变它们。下表列出了不同特权级下对这三个字段的处理情况。

不同特权级对 标志寄存器特 殊字段的处理 | 特权级 | VM标志字段 | IOPL标志字段 | IF标志字段
 | CPL=0 | 可变(初POPF指令外) | 可变 | 可变
 | 0 | 不变 | 不变 | 可变
 | CPL>IOPL | 不变 | 不变 | 不变

从表中可见，只有在特权级0执行的程序才可以修改IOPL位及VM位；只能由相对于IOPL同级或更内 层特权级执行的程序才可以修改IF位。与CLI和STI指令不同，在特权级不满足上述条件的情况下， 当执行POPF指令和IRET指令时，如果试图修改这些字段中的任何一个字段，并不引起异常，但试图 要修改的字段也未被修改，也不给出任何特别的信息。此外，指令POPF总不能改变VM位，而PUSHF指 令所压入的标志中的VM位总为0。

<三>演示输入输出保护的实例(实例九)

下面给出一个用于演示输入输出保护的实例。演示内容包括：I/O许可位图的作用、I/O敏感指令 引起的异常和特权指令引起的异常；使用段间调用指令CALL通过任务门调用任务，实现任务嵌套。

1.演示步骤

实例演示的内容比较丰富，具体演示步骤如下：
(1)在实模式下做必要准备后，切换到保护模式；
(2)进入保护模式的临时代码段后，把演示任务的TSS段描述符装入TR，并设置演示任务 的堆栈；
(3)进入演示代码段，演示代码段的特权级是0；
(4)通过任务门调用测试任务1。测试任务1能够顺利进行；
(5)通过任务门调用测试任务2。测试任务2演示由于违反I/O许可位图规定而导致通用保 护异常；
(6)通过任务门调用测试任务3。测试任务3演示I/O敏感指令如何引起通用保护异常；
(7)通过任务门调用测试任务4。测试任务4演示特权指令如何引起通用保护异常；
(8)从演示代码转临时代码，准备返回实模式；
(9)返回实模式，并作结束处理。

2.源程序组织和清单

为了达到演示目的，实例除了演示任务外，还涉及四个测试任务和一个通用保护故障处理任务。 实例由如下几部分组成。
(1)全局描述符表GDT和中断描述符表IDT；
(2)其它中断/异常处理程序代码段；
(3)通用保护故障处理任务的任务状态段、堆栈段和代码段；
(4)四个测试任务合用的任务状态段、堆栈段和代码段；
(5)演示任务的任务状态段、堆栈段和代码段；
(6)演示任务的临时代码段；
(7)实模式下执行的启动与结束程序代码段和数据段。
实例九源程序清单如下：
;名称:ASM9.ASM
;功能:演示I/O保护及I/O敏感指令的作用
;编译:TASM ASM9.ASM
;连接:TLINK /32 ASM9.OBJ
;----------------------------------------------------------------------------
INCLUDE         386SCD.INC
;----------------------------------------------------------------------------
GDTSeg          SEGMENT PARA USE16                ;全局描述符表段(16位)
GDT             LABEL   BYTE
                ;空描述符
DUMMY           Desc    <>
                ;规范段描述符及选择子
Normal          Desc    <0ffffh,,,ATDW,,>
Normal_Sel      =       Normal-GDT
                ;视频缓冲区段描述符(DPL=3)及选择子(任何特权级可写)
VideoBuf        Desc    <07fffh,8000h,0bh,ATDW,,>
VideoBuf_Sel    =       VideoBuf-GDT
;----------------------------------------------------------------------------
EFFGDT          LABEL   BYTE
                ;演示任务TSS段描述符及选择子
DemoTSS         Desc    <DemoTSSLen-1,DemoTSSSeg,,AT386TSS,,>
DemoTSS_Sel     =       DemoTSS-GDT
                ;演示任务堆栈段描述符及选择子
DemoStack       Desc    <DemoStackLen-1,DemoStackSeg,,ATDW,D32,>
DemoStack_Sel   =       DemoStack-GDT
                ;演示代码段描述符及选择子
DemoCode        Desc    <DemoCodeLen-1,DemoCodeSeg,,ATCE,D32,>
DemoCode_Sel    =       DemoCode-GDT
                ;属于演示任务的临时代码段描述符及选择子
TempCode        Desc    <0ffffh,TempCodeSeg,,ATCE,,>
TempCode_Sel    =       TempCode-GDT
                ;指向GDT的存储段描述符及选择子
ToGDT           Desc    <GDTLen-1,GDTSeg,,ATDW,,>
ToGDT_Sel       =       ToGDT-GDT
                ;指向通用保护故障处理任务TSS的存储段描述符及选择子
ToGPTSS         Desc    <GPTSSLen-1,GPTSSSeg,,ATDW,,>
ToGPTSS_Sel     =       ToGPTSS-GDT
                ;指向测试任务TSS的存储段描述符及选择子
ToTestTSS       Desc    <TestTSSLen-1,TestTSSSeg,,ATDW,,>
ToTestTSS_Sel   =       ToTestTSS-GDT
                ;测试任务TSS段描述符及选择子
TestTSS         Desc    <TestTSSLen-1,TestTSSSeg,,AT386TSS,,>
TestTSS_Sel     =       TestTSS-GDT
                ;测试任务1堆栈段描述符(DPL=1)及选择子
Test1Stack      Desc    <TestStackLen-1,TestStackSeg,,ATDW+DPL1,D32,>
Test1Stack_Sel  =       Test1Stack-GDT+RPL1
                ;测试任务1代码段描述符(DPL=1)及选择子
Test1Code       Desc    <TestCodeLen-1,TestCodeSeg,,ATCE+DPL1,D32,>
Test1Code_Sel   =       Test1Code-GDT+RPL1
                ;测试任务2堆栈段描述符(DPL=2)及选择子
Test2Stack      Desc    <TestStackLen-1,TestStackSeg,,ATDW+DPL2,D32,>
Test2Stack_Sel  =       Test2Stack-GDT+RPL2
                ;测试任务2代码段描述符(DPL=2)及选择子
Test2Code       Desc    <TestCodeLen-1,TestCodeSeg,,ATCE+DPL2,D32,>
Test2Code_Sel   =       Test2Code-GDT+RPL2
                ;测试任务3堆栈段描述符(DPL=3)及选择子
Test3Stack      Desc    <TestStackLen-1,TestStackSeg,,ATDW+DPL3,D32,>
Test3Stack_Sel  =       Test3Stack-GDT+RPL3
                ;测试任务3代码段描述符(DPL=3)及选择子
Test3Code       Desc    <TestCodeLen-1,TestCodeSeg,,ATCE+DPL3,D32,>
Test3Code_Sel   =       Test3Code-GDT+RPL3
                ;通用保护故障处理任务的TSS段描述符及选择子
GPTSS           Desc    <GPTSSLen-1,GPTSSSeg,,AT386TSS,,>
GPTSS_Sel       =       GPTSS-GDT
                ;通用保护故障处理任务的堆栈段描述符及选择子
GPStack         Desc    <GPStackLen-1,GPStackSeg,,ATDW,D32,>
GPStack_Sel     =       GPStack-GDT
                ;通用保护故障处理任务的代码段描述符及选择子
GPCode          Desc    <GPCodeLen-1,GPCodeSeg,,ATCE,D32,>
GPCode_Sel      =       GPCode-GDT
                ;其它中断或异常处理程序代码段(一致可读)描述符及选择子
ErrCode         Desc    <ErrCodeLen-1,ErrCodeSeg,,ATCCOR,D32,>
ErrCode_Sel     =       ErrCode-GDT
;----------------------------------------------------------------------------
GDNum           =       ($-EFFGDT)/(SIZE Desc)    ;需处理基地址的描述符个数
;----------------------------------------------------------------------------
                ;指向测试任务的任务门
TestTask        Gate    <,TestTSS_Sel,,ATTaskGate,>
Test_Sel        =       TestTask-GDT
;----------------------------------------------------------------------------
GDTLen          =       $-GDT                     ;全局描述符表长度
GDTSeg          ENDS                              ;全局描述符表段定义结束
;----------------------------------------------------------------------------
IDTSeg          SEGMENT PARA USE16                ;中断描述符表段(16位)
IDT             LABEL   BYTE                      ;中断描述符表
                REPT    13
                Gate    <ErrBegin,ErrCode_Sel,,AT386TGate,>
                ENDM
                Gate    <,GPTSS_Sel,,ATTaskGate,> ;通用故障处理程序门描述符
                REPT    242
                Gate    <ErrBegin,ErrCode_Sel,,AT386TGate,>
                ENDM
;----------------------------------------------------------------------------
IDTLen          =       $-IDT
;----------------------------------------------------------------------------
IDTSeg          ENDS                              ;中断描述符表段定义结束
;----------------------------------------------------------------------------
;其它中断或异常处理程序的代码段(一致可读)
;----------------------------------------------------------------------------
ErrCodeSeg      SEGMENT PARA USE32
                ASSUME  CS:ErrCodeSeg
;----------------------------------------------------------------------------
ErrMess         DB      'Error!!!'
ErrMessLen      =       $-ErrMess
;----------------------------------------------------------------------------
ErrBegin        PROC    FAR
                cld
                mov     ax,ErrCode_Sel
                mov     ds,ax
                lea     esi,ErrMess
                mov     ax,VideoBuf_Sel
                mov     es,ax
                mov     edi,1992
                mov     ecx,ErrMessLen
                mov     ah,4eh
Err1:           lodsb
                stosw
                loop    Err1
                jmp     $
ErrBegin        ENDP
;----------------------------------------------------------------------------
ErrCodeLen      =       $
ErrCodeSeg      ENDS
;----------------------------------------------------------------------------
GPTSSSeg        SEGMENT PARA USE16                ;通用保护故障处理任务的TSS
GPTaskSS        LABEL   BYTE
                DD      0                         ;任务嵌套时的链接指针
                DD      0                         ;0级堆栈偏移
                DW      0,0                       ;0级堆栈选择子
                DD      0                         ;1级堆栈偏移
                DW      0,0                       ;1级堆栈选择子
                DD      0                         ;2级堆栈偏移
                DW      0,0                       ;2级堆栈选择子
                DD      0                         ;CR3
                DW      GPBegin,0                 ;EIP
                DD      0                         ;EFLAGS
                DD      0                         ;EAX
                DD      0                         ;ECX
                DD      0                         ;EDX
                DD      0                         ;EBX
                DD      GPStackLen                ;ESP
                DD      0                         ;EBP
                DD      0                         ;ESI
                DD      0                         ;EDI
                DW      VideoBuf_Sel,0            ;ES
                DW      GPCode_Sel,0              ;CS
                DW      GPStack_Sel,0             ;SS
                DW      ToTestTSS_Sel,0           ;DS
                DW      ToGPTSS_Sel,0             ;FS
                DW      0,0                       ;GS
                DW      0,0                       ;LDTR
                DW      0                         ;调试陷阱标志
                DW      $+2                       ;指向I/O许可位图的偏移
                DB      0ffh                      ;I/O许可位图结束标志
GPTSSLen        =       $
GPTSSSeg        ENDS
;----------------------------------------------------------------------------
GPStackSeg      SEGMENT PARA USE32                ;通用保护故障处理任务堆栈段
GPStackLen      =       512
                DB      GPStackLen DUP(0)
GPStackSeg      ENDS
;----------------------------------------------------------------------------
;通用保护故障处理程序代码段
;----------------------------------------------------------------------------
GPCodeSeg       SEGMENT PARA USE32
                ASSUME  CS:GPCodeSeg
;----------------------------------------------------------------------------
GPBegin         PROC    FAR
                ;在屏幕左上角显示故障点
                xor     edi,edi
                mov     ebx,OFFSET TestTaskSS
                mov     edx,DWORD PTR [ebx].TRCS
                call    EchoEDX
                mov     ax,(17h SHL 8)+':'
                stosw
                mov     edx,[ebx].TREIP
                call    EchoEDX
                ;演示以便看清故障点
                mov     ecx,1234567h
                loop    $
                ;调整任务链接指针,中止故障任务
                mov     ebx,OFFSET GPTaskSS
                mov     ax,DemoTSS_Sel
                mov     fs:[ebx].TRLink,ax
                add     esp,4
                iretd
                jmp     GPBegin
GPBegin         ENDP
;----------------------------------------------------------------------------
;显示edx内容的子程序
;----------------------------------------------------------------------------
EchoEDX         PROC
                mov     ah,17h
                mov     ecx,8
EchoEDX1:       rol     edx,4
                mov     al,dl
                call    HToASCII
                stosw
                loop    EchoEDX1
                ret
EchoEDX         ENDP
;----------------------------------------------------------------------------
;把4位二进制数转换成对应的ASCII码
;----------------------------------------------------------------------------
HToASCII        PROC
                and     al,0fh
                add     al,90h
                daa
                adc     al,40h
                daa
                ret
HToASCII        ENDP
;----------------------------------------------------------------------------
GPCodeLen       =       $
GPCodeSeg       ENDS
;----------------------------------------------------------------------------
;测试任务的TSS段
TestTSSSeg      SEGMENT PARA USE16
TestTaskSS      TSS     <>                        ;TSS的固定格式部分
IOMap           LABEL   BYTE                      ;I/O许可位图
                DB      8 DUP(0ffh)               ;端口00h--3fh
                DB      11111011b                 ;端口40h--47h
                DB      3 DUP(0ffh)               ;端口48h--5fh
                DB      11111101b                 ;端口60h--67h
                DB      0                         ;端口68h--6fh
                DB      0ffh                      ;I/O许可位图结束标志
TestTSSLen      =       $
TestTSSSeg      ENDS
;----------------------------------------------------------------------------
;测试任务的堆栈段
TestStackSeg    SEGMENT PARA USE32
TestStackLen    =       1024
                DB      TestStackLen DUP(0)
TestStackSeg    ENDS
;----------------------------------------------------------------------------
;测试任务的代码段
TestCodeSeg     SEGMENT PARA USE32
                ASSUME  CS:TestCodeSeg
;----------------------------------------------------------------------------
Test3Begin      PROC    FAR
                cli                               ;I/O敏感指令
                clts                              ;特权指令
                iretd
                jmp     Test3Begin
Test3Begin      ENDP
;----------------------------------------------------------------------------
TestBegin       PROC    FAR
                mov     al,0b6h                   ;使扬声器发出一长声
                out     43h,al
                mov     al,2
                out     42h,al
                mov     al,34h
                out     42h,al
                in      al,61h
                mov     ah,al
                or      al,3
                out     61h,al
                mov     ecx,1234567h
                loop    $
                mov     al,ah
                out     61h,al
                iretd
                jmp     TestBegin
TestBegin       ENDP
;----------------------------------------------------------------------------
TestCodeLen     =       $
TestCodeSeg     ENDS
;----------------------------------------------------------------------------
;演示任务TSS段
DemoTSSSeg      SEGMENT PARA USE16
DemoTaskSS      TSS     <>
                DB      0ffh                      ;I/O许可位图结束字节
DemoTSSLen      =       $
DemoTSSSeg      ENDS
;----------------------------------------------------------------------------
;演示任务的堆栈段
DemoStackSeg    SEGMENT PARA USE32
DemoStackLen    =       1024
                DB      DemoStackLen DUP(0)
DemoStackSeg    ENDS
;----------------------------------------------------------------------------
;演示任务的代码段
DemoCodeSeg     SEGMENT PARA USE32
                ASSUME  CS:DemoCodeSeg
;----------------------------------------------------------------------------
DemoBegin       PROC    FAR
                mov     ax,ToTestTSS_Sel
                mov     ds,ax
                mov     ebx,OFFSET TestTaskSS
                ;把测试任务1的入口点,堆栈指针和标志值(含IOPL)填入测试任务TSS
                mov     WORD PTR [ebx].TRSS,Test1Stack_Sel
                mov     DWORD PTR [ebx].TRESP,TestStackLen
                mov     WORD PTR [ebx].TRCS,Test1Code_Sel
                mov     DWORD PTR [ebx].TREIP,OFFSET TestBegin
                mov     DWORD PTR [ebx].TREFLAG,IOPL1
                ;通过任务门调用测试任务
                CALL32  Test_Sel,0
                ;把测试任务2的入口点,堆栈指针和标志值(含IOPL)填入测试任务TSS
                mov     WORD PTR [ebx].TRSS,Test2Stack_Sel
                mov     DWORD PTR [ebx].TRESP,TestStackLen
                mov     WORD PTR [ebx].TRCS,Test2Code_Sel
                mov     DWORD PTR [ebx].TREIP,OFFSET TestBegin
                mov     DWORD PTR [ebx].TREFLAG,IOPL1
                ;通过任务门调用测试任务
                CALL32  Test_Sel,0
                ;把测试任务TSS描述符内的属性置为"可用"
                mov     ax,ToGDT_Sel
                mov     fs,ax
                mov     fs:TestTSS.Attributes,AT386TSS
                ;把测试任务3的入口点,堆栈指针和标志值(含IOPL)填入测试任务TSS
                mov     WORD PTR [ebx].TRSS,Test3Stack_Sel
                mov     DWORD PTR [ebx].TRESP,TestStackLen
                mov     WORD PTR [ebx].TRCS,Test3Code_Sel
                mov     DWORD PTR [ebx].TREIP,OFFSET Test3Begin
                mov     DWORD PTR [ebx].TREFLAG,IOPL2
                ;通过任务门调用测试任务
                CALL32  Test_Sel,0
                ;把测试任务TSS描述符内的属性置为"可用"
                mov     ax,ToGDT_Sel
                mov     fs,ax
                mov     fs:TestTSS.Attributes,AT386TSS
                ;把测试任务4的入口点,堆栈指针和标志值(含IOPL)填入测试任务TSS
                mov     WORD PTR [ebx].TRSS,Test3Stack_Sel
                mov     DWORD PTR [ebx].TRESP,TestStackLen
                mov     WORD PTR [ebx].TRCS,Test3Code_Sel
                mov     DWORD PTR [ebx].TREIP,OFFSET Test3Begin
                mov     DWORD PTR [ebx].TREFLAG,IOPL3
                ;通过任务门调用测试任务
                CALL32  Test_Sel,0
                JUMP32  TempCode_Sel,<OFFSET ToDOS>
DemoBegin       ENDP
;----------------------------------------------------------------------------
DemoCodeLen     =       $
DemoCodeSeg     ENDS
;----------------------------------------------------------------------------
TempCodeSeg     SEGMENT PARA USE16                ;演示任务的临时代码段
                ASSUME  CS:TempCodeSeg
;----------------------------------------------------------------------------
Virtual         PROC    FAR
                ;置数据段寄存器为空
                mov     ax,0
                mov     ds,ax
                mov     es,ax
                mov     fs,ax
                mov     gs,ax
                ;置堆栈指针
                mov     ax,DemoStack_Sel
                mov     ss,ax
                mov     esp,DemoStackLen
                ;置任务寄存器
                mov     ax,DemoTSS_Sel
                ltr     ax
                ;转演示代码段
                JUMP16  DemoCode_Sel,DemoBegin
ToDOS:          clts
                mov     ax,Normal_Sel
                mov     ds,ax
                mov     es,ax
                mov     fs,ax
                mov     gs,ax
                mov     ss,ax
                mov     eax,cr0
                and     al,11111110b
                mov     cr0,eax
                JUMP16  <SEG Real>,<OFFSET Real>
Virtual         ENDP
;----------------------------------------------------------------------------
TempCodeSeg     ENDS
;============================================================================
RDataSeg        SEGMENT PARA USE16                ;实方式数据段
VGDTR           PDesc   <GDTLen-1,>               ;GDT伪描述符
VIDTR           PDesc   <IDTLen-1,>               ;IDT伪描述符
NORVIDTR        PDesc   <3ffh,>                   ;用于保存原IDTR值
SPVar           DW      ?                         ;用于保存实方式下的SP
SSVar           DW      ?                         ;用于保存实方式下的SS
RDataSeg        ENDS
;----------------------------------------------------------------------------
RCodeSeg        SEGMENT PARA USE16                ;实方式代码段
                ASSUME  CS:RCodeSeg,DS:RDataSeg
;----------------------------------------------------------------------------
Start           PROC
                mov     ax,RDataSeg
                mov     ds,ax
                cld
                call    InitGDT                   ;初始化全局描述符表GDT
                call    InitIDT                   ;初始化中断描述符表IDT
                lgdt    QWORD PTR VGDTR           ;装载GDTR
                mov     SSVar,ss                  ;保存堆栈指针
                mov     SPVar,sp
                sidt    QWORD PTR NORVIDTR        ;保存IDTR
                cli                               ;关中断
                lidt    QWORD PTR VIDTR           ;装载IDTR
                mov     eax,cr0
                or      al,1
                mov     cr0,eax
                JUMP16  <TempCode_Sel>,<OFFSET Virtual>
Real:           mov     ax,RDataSeg
                mov     ds,ax
                lss     sp,DWORD PTR SPVar        ;又回到实方式
                lidt    QWORD PTR NORVIDTR
                sti
                mov     ax,4c00h
                int     21h
Start           ENDP
;----------------------------------------------------------------------------
InitGDT         PROC
                push    ds
                mov     ax,GDTSeg
                mov     ds,ax
                mov     cx,GDNum
                mov     si,OFFSET EFFGDT
InitG:          mov     ax,[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR [si].BaseL,ax
                mov     BYTE PTR [si].BaseM,dl
                mov     BYTE PTR [si].BaseH,dh
                add     si,SIZE Desc
                loop    InitG
                pop     ds
                mov     bx,16
                mov     ax,GDTSeg
                mul     bx
                mov     WORD PTR VGDTR.Base,ax
                mov     WORD PTR VGDTR.Base+2,dx
                ret
InitGDT         ENDP
;----------------------------------------------------------------------------
InitIDT         PROC
                mov     bx,16
                mov     ax,IDTSeg
                mul     bx
                mov     WORD PTR VIDTR.Base,ax
                mov     WORD PTR VIDTR.Base+2,dx
                ret
InitIDT         ENDP
;----------------------------------------------------------------------------
RCodeSeg        ENDS
                END     Start

3.关于实例九的说明

为了节省篇幅，同时也反映任务状态段的作用，实例通过任务门调用的四个测试任务合用一个 任务状态段。从源程序可见，这种合用，实际上是串行的。先把测试任务1的入口点填入测试任 务状态段，同时填入堆栈指针，然后调用测试任务1。在测试任务1完成后，再填入测试任务2的 入口点，也填入堆栈指针，然后调用测试任务2。依次类推。
通用保护故障处理程序作为一个独立的任务出现。再发生通用保护故障后，通用保护故障处理程 序在屏幕上显示故障点的选择子和偏移，然后通过调整存放在任务状态段内的任务连接指针的方 法，中止引起故障的测试任务。


十.分页管理机制

80386开始支持存储器分页管理机制。分页机制是存储器管理机制的第3二部分。段管理机制实现 虚拟地址(由段和偏移构成的逻辑地址)到线性地址的转换，分页管理机制实现线性地址到物理地 址的转换。如果不启用分页管理机制，那么线性地址就是物理地址。本文将介绍80386的存储器分 页管理机制和线性地址如何转换为物理地址。 这里下载本文源代码。

<一>存储器分页管理机制

在保护模式下，控制寄存器CR0中的最高位PG位控制分页管理机制是否生效。如果PG=1，分页机制 生效，把线性地址转换为物理地址。如果PG=0，分页机制无效，线性地址就直接作为物理地址。 必须注意，只有在保护方式下分页机制才可能生效。只有在保证使PE位为1的前提下，才能够使PG位 为1，否则将引起通用保护故障。
分页机制把线性地址空间和物理地址空间分别划分为大小相同的块。这样的块称之为页。通过在 线性地址空间的页与物理地址空间的页之间建立的映射，分页机制实现线性地址到物理地址的转 换。线性地址空间的页与物理地址空间的页之间的映射可根据需要而确定，可根据需要而改变。 线性地址空间的任何一页，可以映射为物理地址空间中的任何一页。
采用分页管理机制实现线性地址到物理地址转换映射的主要目的是便于实现虚拟存储器。不象段 的大小可变，页的大小是相等并固定的。根据程序的逻辑划分段，而根据实现虚拟存储器的方便 划分页。
在80386中，页的大小固定为4K字节，每一页的边界地址必须是4K的倍数。因此，4G大小的地址 空间被划分为1M个页，页的开始地址具有“XXXXX000H”的形式。为此，我们把页开始地址的 高20位XXXXXH称为页码。线性地址空间页的页码也就是页开始边界线性地址的高20位；物理地址 空间页的页码也就是页开始边界物理地址的高20位。可见，页码左移12位就是页的开始地址，所 以页码规定了页。
由于页的大小固定为4K字节，且页的边界是4K的倍数，所以在把32位线性地址转换成32位物理地 址的过程中，低12位地址保持不变。也就是说，线性地址的低12位就是物理地址的低12位。假设 分页机制采用的转换映射把线性地址空间的XXXXXH页映射到物理地址空间的YYYYYH页，那么线性 地址XXXXXxxxH被转换为YYYYYxxxH。因此，线性地址到物理地址的转换要解决的是线性地址空间 的页到物理地址空间的页的映射，也就是线性地址高20位到物理地址高20位的转换。

<二>线性地址到物理地址的转换

1.映射表结构

线性地址空间的页到物理地址空间的页之间的映射用表来描述。由于4G的地址空间划分为1M个页， 因此，如果用一张表来描述这种映射，那么该映射表就要有1M个表项，若每个表项占用4个字节， 那么该映射表就要占用4M字节。为避免映射表占用如此巨大的存储器资源，所以80386把页映射表 分为两级。
页映射表的第一级称为页目录表，存储在一个4K字节的物理页中。页目录表共有1K个表项，其中， 每个表项为4字节长，包含对应第二级表所在物理地址空间页的页码。页映射表的第二级称为页 表，每张页表也安排在一个4K字节的页中。每张页表都有1K个表项，每个表项为4字节长，包含对 应物理地址空间页的页码。由于页目录表和页表均由1K个表项组成，所以使用10位的索引就能指 定表项，即用10位的索引值乘以4加基地址就得到了表项的物理地址。
下图显示了由页目录表和页表构成的页映射表结构。从图中可见，控制寄存器CR3指定页目录表； 页目录表可以指定1K个页表，这些页表可以分散存放在任意的物理页中，而不需要连续存放；每 张页表可以指定1K个物理地址空间的页，这些物理地址空间的页可以任意地分散在物理地址空间 中。需要注意的是，存储页目录表和页表的基地址是对齐在4K字节边界上的。

2.表项格式

页目录表和页表中的表项都采用如下图所示的格式。从图中可见，最高20位(位12—位31)包含物 理地址空间页的页码，也就是物理地址的高20位。低12位包含页的属性。下图所示的属性中内容 为0的位是Intel公司为80486等处理器所保留的位，在为80386编程使用到它们时必须设置为0。 在位9至位11的AVL字段供软件使用。表项的最低位是存在属性位，记作P。P位表示该表项是否有 效。P=1表项有效；P=0表项无效，此时表项中的其余各位均可供软件使用，80386不解释P=0的表 项中的任何其它的位。在通过页目录表和页表进行的线性地址到物理地址的转换过程中，无论在 页目录表还是在页表中遇到无效表项，都会引起页故障。其它属性位的作用在下文中介绍。

页目录表或页 表的表项格式 | BIT31—BIT12 | BIT11—BIT9 | BIT8 | BIT7 | BIT6 | BIT5 | BIT4 | BIT3 | BIT2 | BIT1 | BIT0
 | 物理页码 | AVL | 0 | 0 | D | A | 0 | 0 | U/S | R/W | P

3.线性地址到物理地址的转换

分页管理机制通过上述页目录表和页表实现32位线性地址到32位物理地址的转换。控制寄存器CR3的 高20位作为页目录表所在物理页的页码。首先把线性地址的最高10位(即位22至位31)作为页目录表 的索引，对应表项所包含的页码指定页表；然后，再把线性地址的中间10位(即位12至位21)作为所 指定的页目录表中的页表项的索引，对应表项所包含的页码指定物理地址空间中的一页；最后，把 所指定的物理页的页码作为高20位，把线性地址的低12位不加改变地作为32位物理地址的低12位。
为了避免在每次存储器访问时都要访问内存中的页表，以便提高访问内存的速度，80386处理器的 硬件把最近使用的线性—物理地址转换函数存储在处理器内部的页转换高速缓存中。在访问存储器 页表之前总是先查阅高速缓存，仅当必须的转换不在高速缓存中时，才访问存储器中的两级页表。 页转换高速缓存也称为页转换查找缓存，记为TLB。
在分页机制转换高速缓存中的数据与页表中数据的相关性，不是由80386处理器进行维护的，而必须 由操作系统软件保存，也就是说，处理器不知道软件什么时候会修改页表，在一个合理的系统中， 页表只能由操作系统修改，操作系统可以直接地在软件修改页表后通过刷新高速缓存来保证相关性。 高速缓存的刷新通过装入处理器控制寄存器CR3完成，实际过程可能用如下的两条指令实现：
    mov     eax,cr3
    mov     cr3,eax
一个重要的修改页表项的特殊情况不需要对页转换高速缓存刷新，这种情况是指修改不存在表项的 任一部分，即使P位本身从P=0改变为P=1时也一样，因为无效的表项不会存入高速缓存。因此，当 无效的表项被改变时，不需要刷新高速缓存。这表明在从磁盘上读入一页使其存在时，不必刷新高 速缓存。
在一个多处理器系统中，必须特别注意是否在一个处理器中执行的程序，会改变可能由另外的处理 器同时访问的页表。在80386处理器中，每当要更新页表项并设置D位和A位时，通过使用不可分的 读/修改/写周期支持多处理器的配置。对于页表项的软件更新需要借助于使用LOCK前缀，从而保证 修改页表的指令工作在不可分的读/修改/写周期中。在改变一个可能由另外的处理器使用的页表之 前，最好使用一条加锁的AND指令在一个不可分的操作中将P位清除为0，然后，该表项可根据要求进 行修改，并随后把P位置成1而使表项成为可用。当修改页表项时必须及时通知(通常使用中断方式)系 统中该表项已被高速缓存的所有处理器刷新各自的页转换高速缓存，以撤消该表项的旧拷贝。在表 项的旧拷贝被刷新之前，各处理器仍可继续访问旧的页，并可以设置正被修改的表项的D位。如果这 样做引起表项修改失败，则分页机制高速缓存最好在标记为不存在之后，并在对表项进行另外的修 改之前进行刷新。

4.不存在的页表

采用上述页映射表结构，存储全部1K张页表需要4M字节，此外还需要4K字节用于存储页目录表。这 样的两级页映射表似乎反而比单一的整张页映射表多占用4K字节。其实不然，事实上不需要在内存 中存储完整的两级页映射表。两级页映射表结构中对于线性地址空间中不存在的或未使用的部分不 必分配页表。除必须给页目录表分配物理页外，仅当在需要时才给页表分配物理页，于是页映射表 的大小就对应于实际使用的线性地址空间大小。因为任何一个实际运行的程序使用的线性地址空间 都远小于4G字节，所以用于分配给页表的物理页也远小于4M字节。
页目录表项中的存在位P表明对应页表是否有效。如果P=1，表明对应页表有效，可利用它进行地址 转换；如果P=0，表明对应页表无效。如果试图通过无效的页表进行线性地址到物理地址的转换，那 么将引起页故障。因此，页目录表项中的属性位P使得操作系统只需给覆盖实际使用的线性地址范围 的页表分配物理页。
页目录表项中的属性位P页可用于把页表存储在虚拟存储器中。当发生由于所需页表无效而引起的页 故障时，页故障处理程序再申请物理页，从磁盘上把对应的页表读入，并把对应页目录表项中的P位 置1。换言之，可以当需要时才为所要的页表分配物理页。这样页表占用的物理页数量可降到最小。

5.页的共享

由上述页映射表结构可见，分页机制没有全局页和局部页的规定。每一个任务可使用自己的页映射 表独立地实现线性地址到物理地址的转换。但是，如果使每一个任务所用的页映射表具有部分相同 的映射，那么也就可以实现部分页的共享。
常用的实现页共享的方法是线性地址空间的共享，也就是不同任务的部分相同的线性地址空间 的映射信息相同，具体表现为部分页表相同或页表内的部分表项的页码相同。例如，如果任务A和 任务B分别使用的页目录表A和页目录表B内的第0项中的页码相同，也就是页表0相同，那么任务A和 任务B的00000000H至003FFFFFH线性地址空间就映射到相同的物理页。再如，任务A和任务B使用的 页表0不同，但这两张页表内第0至第0FFH项的页码对应相同，那么任务A和任 务B的00000000H至000FFFFFH线性地址空间就映射到相同的物理页。
需要注意的是，共享的页表最好由两个页目录中同样的目录项所指定。这一点很重要，因为它保证 了在两个任务中同样的线性地址范围将映射到该全局区域。

<三>页级保护和虚拟存储器支持

1.页级保护

80386不仅提供段级保护，也提供页级保护。分页机制只区分两种特权级。特权级0、1和2统称为系 统特权级，特权级3称为用户特权级。在上图所示页目录表和页表的表项中的保护属性位R/W和U/S就 是用于对页进行保护。
表项的位1是读写属性位，记作R/W。R/W位指示该表项所指定的页是否可读、写或执行。若R/W=1， 对表项所指定的页可进行读、写或执行；若R/W=0，对表项所指定的页可读或执行，但不能对该指 定的页写入。但是，R/W位对页的写保护只在处理器处于用户特权级时发挥作用；当处理器处于系 统特权级时，R/W位被忽略，即总可以读、写或执行。
表项的位2是用户/系统属性位，记作U/S。U/S位指示该表项所指定的页是否是用户级页。若U/S=1， 表项所指定的页是用户级页，可由任何特权级下执行的程序访问；如果U/S=0，表项所指定的页是 系统级页，只能由系统特权级下执行的程序访问。下表列出了上述属性位R/W和U/S所确定的页级保 护下，用户级程序和系统级程序分别具有的对用户级页和系统级页进行操作的权限。

页级 保护 属性 | U/S | R/W | 用户级访问权限 | 系统级访问权限
 | 0 | 0 | 无 | 读/写/执行
 | 0 | 1 | 无 | 读/写/执行
 | 1 | 0 | 读/执行 | 读/写/执行
 | 1 | 1 | 读/写/执行 | 读/写/执行

由上表可见，用户级页可以规定为只允许读/执行或规定为读/写/执行。系统级页对于系统级 程序总是可读/写/执行，而对用户级程序总是不可访问的。于分段机制一样，外层用户级执行 的程序只能访问用户级的页，而内层系统级执行的程序，既可访问系统级页，也可访问用户级 页。与分段机制不同的是，在内层系统级执行的程序，对任何页都有读/写/执行访问权，即使 规定为只允许读/执行的用户页，内层系统级程序也对该页有写访问权。
页目录表项中的保护属性位R/W和U/S对由该表项指定页表所指定的全部1K各页起到保护作用。 所以，对页访问时引用的保护属性位R/W和U/S的值是组合计算页目录表项和页表项中的保护属 性位的值所得。下表列出了组合计算前后的保护属性位的值，组合计算是“与”操作。

组合页的 保护>属性 | 目录表项U/S | 页表项U/S | 组合U/S | 目录表项R/W | 页表项R/W | 组合R/W
 | 0 | 0 | 0 | 0 | 0 | 0
 | 0 | 1 | 0 | 0 | 1 | 0
 | 1 | 0 | 0 | 1 | 0 | 0
 | 1 | 1 | 1 | 1 | 1 | 1

正如在80386地址转换机制中分页机制在分段机制之后起作用一样，由分页机制支持的页级保护 也在由分段机制支持的段级保护之后起作用。先测试有关的段级保护，如果启用分页机制，那 么在检查通过后，再测试页级保护。如果段的类型为读/写，而页规定为只允许读/执行，那么 不允许写；如果段的类型为只读/执行，那么不论页保护如何，也不允许写。
页级保护的检查是在线性地址转换为物理地址的过程中进行的，如果违反页保护属性的规定，对 页进行访问(读/写/执行)，那么将引起页异常。

2.对虚拟存储器的支持

页表项中的P位是支持采用分页机制虚拟存储器的关键。P=1，表示表项指定的页存在于物理存储 器中，并且表项的高20位是物理页的页码；P=0，表示该线性地址空间中的页所对应的物理地址 空中的页不在物理存储器中。如果程序访问不存在的页，会引起页异常，这样操作系统可把该不 存在的页从磁盘上读入，把所在物理页的页码填入对应表项并把表项中的P位置为1，然后使引起 异常的程序恢复运行。
此外，表项中的访问位A和写标志位D也用于支持有效地实现虚拟存储器。
表项的位5是访问属性位，记作A。在为了访问某存储单元而进行线性地址到物理地址的转换过程 中，处理器总是把页目录表内的对应表项和其所指定页表内的对应表项中的A位置1，除非页表或 页不存在，或者访问违反保护属性规定。所以，A=1表示已访问过对应的物理页。处理器永不清 除A位。通过周期性地检测及清除A位，操作系统就可确定哪些页在最近一段时间未被访问过。当 存储器资源紧缺时，这些最近未被访问的页很可能就被选择出来，将它们从内存换出到磁盘上去。
表项的位6是写标志位，记作D。在为了访问某存储单元而进行线性地址到物理地址的转换过程中， 如果是写访问并且可以写访问，处理器就把页表内对应表项中的D位置1，但并不把页目录表内对 应表项中的D置1。当某页从磁盘上读入内存时，页表中对应对应表项的D位被清0。所以，D=1表示 已写过对应的物理页。当某页需要从内存换出到磁盘上时，如果该页的D位为1，那么必须进行写 操作(把内存中的页写入磁盘时，处理器并不清除对应页表项的D位)。但是，如果要写到磁盘上的 页的D位为0，那么不需要实际的磁盘写操作，而只要简单地放弃内存中该页即可。因为内存中的 页与磁盘中的页具有完全相同的内容。

<四>页异常

启用分页机制后，线性地址不再直接等于物理地址，线性地址要经过分页机制转换才成为物理地 址。在转换过程中，如果出现下列情况之一就会引起页异常：
(1)涉及的页目录表内的表项或页表内的表项中的P=0，即涉及到页不在内存；
(2)发现试图违反页保护属性的规定而对页进行访问。
报告页异常的中断向量号是14(0EH)。页异常属于故障类异常。在进入故障处理程序时，保存的指 令指针CS及EIP指向发生故障的指令。一旦引起页故障的原因被排除后，即可从页故障处理程序通 过一条IRET指令，直接地重新执行产生故障的指令。
当页故障发生时，处理器把引起页故障的线性地址装入CR2。页故障处理程序可以利用该线性地址 确定对应的页目录项和页表项。
页故障还在堆栈中提供一个出错码，出错码的格式如下图所示。其中，U位表示引起故障程序 的特权级，U=1表示用户特权级(特权级3)，U=0表示系统特权级(特权级0、1或2)；W位表示访 问类型，W=0表示读/执行，W=1表示写；P位表示异常类型，P=0表示页不存在故障，P=1表示 保护故障。页故障的响应处理模式同其它故障一样。

出错码 的格式 | BIT15—BIT3 | BIT2 | BIT1 | BIT0
 | 未使用 | U | W | P

<五>演示分页机制的实例(实例十)

下面给出一个演示如何启用分页管理机制的实例。该实例的逻辑功能是，在屏幕上显示一条表示 已启用分页管理机制的提示信息。该实例演示内容包括：初始化页目录表和部分页表；启用分页 管理机制；关闭分页管理机制等。该实例假设系统至少有4M字节物理内存。

1.演示步骤和源程序清单

为了简单化，实例只有一个任务，并且没有局部描述符表和中断描述符表，不允许中断，也不考 虑发生异常，甚至没有使用堆栈。实例执行步骤如下：
(1)在实模式下为进入保护模式作初始化；
(2)切换到保护模式后进入临时代码段，把部分演示代码传送到预定的内存，然后转演示代 码段；
(3)建立页目录表；
(4)建立页表；
(5)启用分页管理机制；
(6)演示在分页管理机制启用后的程序执行和数据存取；
(7)关闭分页管理机制；
(8)退出保护模式，结束。
实例十源程序清单如下：
;名称:ASM10.ASM
;功能:演示使用分页管理机制
;编译:TASM ASM10.ASM
;连接:TLINK ASM10.OBJ
;============================================================================
INCLUDE         386SCD.INC
;============================================================================
PDT_AD          =       200000h ;页目录表所在物理页的地址
PT0_AD          =       202000h ;页表0所在物理页的地址
PT1_AD          =       201000h ;页表1所在物理页的地址
PhVB_AD         =       0b8000h ;物理视频缓冲区地址
LoVB_AD         =       0f0000h ;程序使用的逻辑视频缓冲区地址
MPVB_AD         =       301000h ;线性地址0B8000H所映射的物理地址
PhSC_AD         =       303000h ;部分演示代码所在内存的物理地址
LoSC_AD         =       402000h ;部分演示代码的逻辑地址
;============================================================================
GDTSeg          SEGMENT PARA USE16                ;全局描述符表数据段(16位)
;----------------------------------------------------------------------------
                ;全局描述符表GDT
GDT             LABEL   BYTE
                ;空描述符
DUMMY           Desc    <>
                ;规范段描述符及选择子
Normal          Desc    <0ffffh,,,ATDW,,>
Normal_Sel      =       Normal-GDT
                ;页目录表所在段描述符(在保护方式下初始化时用)及选择子
PDT             Desc    <0fffh,PDT_AD AND 0ffffh,PDT_AD SHR 16,ATDW,,>
PDT_Sel         =       PDT-GDT
                ;页表0所在段描述符(在保护方式下初始化时用)及选择子
PT0             Desc    <0fffh,PT0_AD AND 0ffffh,PT0_AD SHR 16,ATDW,,>
PT0_Sel         =       PT0-GDT
                ;页表1所在段描述符(在保护方式下初始化时用)及选择子
PT1             Desc    <0fffh,PT1_AD AND 0ffffh,PT1_AD SHR 16,ATDW,,>
PT1_Sel         =       PT1-GDT
                ;逻辑视频缓冲区段描述符及选择子
LoVideo         Desc    <3999,LoVB_AD AND 0ffffh,LoVB_AD SHR 16,ATDW,,>
LoVideo_Sel     =       LoVideo-GDT
                ;逻辑上的部分演示代码段的描述符及选择子
LoCode          Desc    <SCodeLen-1,LoSC_AD AND 0ffffh,LoSC_AD SHR 16,ATCE,,>
LoCode_Sel      =       LoCode-GDT
                ;预定内存区域(用于部分演示代码)的段描述符及选择子
TPSCode         Desc    <SCodeLen-1,PhSC_AD AND 0ffffh,PhSC_AD SHR 16,ATDW,,>
TPSCode_Sel     =       TPSCode-GDT
;----------------------------------------------------------------------------
                ;以下是需额外初始化的描述符
EFFGDT          LABEL   BYTE
                ;临时代码段描述符及选择子
TempCode        Desc    <0ffffh,TempCodeSeg,,ATCE,,>
TempCode_Sel    =       TempCode-GDT
                ;演示代码段描述符及选择子
DemoCode        Desc    <DemoCodeLen-1,DemoCodeSeg,,ATCE,,>
DemoCode_Sel    =       DemoCode-GDT
                ;演示任务数据段描述符及选择子
DemoData        Desc    <DemoDataLen-1,DemoDataSeg,,ATDW,,>
DemoData_Sel    =       DemoData-GDT
                ;初始化时要移动的代码段描述符及选择子(移动时作为数据对待)
SCode           Desc    <SCodeLen-1,SCodeSeg,,ATDR,,>
SCode_Sel       =       SCode-GDT
;----------------------------------------------------------------------------
GDTLen          =       $-GDT                     ;全局描述符表长度
GDNum           =       ($-EFFGDT)/(SIZE Desc)    ;需特殊处理的描述符数
;----------------------------------------------------------------------------
GDTSeg          ENDS                              ;全局描述符表段定义结束
;============================================================================
;这部分代码在初始化时被复制到预定的内存区域,其功能是在屏幕上显示提示信息
;----------------------------------------------------------------------------
SCodeSeg        SEGMENT PARA USE16
                ASSUME  CS:SCodeSeg,DS:DemoDataSeg
;----------------------------------------------------------------------------
SBegin          PROC    FAR
                mov     ax,LoVideo_Sel
                mov     es,ax
                mov     di,0
                mov     ah,17h
                mov     cx,MessLen
S1:             lodsb
                stosw
                loop    S1
                JUMP16  DemoCode_Sel,Demo3
SBegin          ENDP
;----------------------------------------------------------------------------
MLen            =       $-SBegin
SCodeLen        =       $
SCodeSeg        ENDS
;============================================================================
DemoDataSeg     SEGMENT PARA USE16                ;演示任务数据段
Mess            DB      'Page is OK!'
MessLen         =       $-Mess
DemoDataLen     =       $
DemoDataSeg     ENDS
;============================================================================
DemoCodeSeg     SEGMENT PARA USE16                ;演示任务代码段
                ASSUME  CS:DemoCodeSeg
;----------------------------------------------------------------------------
DemoBegin       PROC    FAR
                mov     ax,PDT_Sel
                mov     es,ax
                xor     di,di
                mov     cx,1024
                xor     eax,eax                   ;先把全部表项置成无效
                rep     stosd                     ;再置表项0和表项1
                mov     DWORD PTR es:[0],PT0_AD OR (USU+RWW+PL)
                mov     DWORD PTR es:[4],PT1_AD OR (USU+RWW+PL)
                mov     ax,PT0_Sel                ;初始化页表0
                mov     es,ax
                xor     di,di
                mov     cx,1024
                xor     eax,eax
                or      eax,USU+RWW+PL
Demo1:          stosd
                add     eax,1000h                 ;先全部置成对应等地址的
                loop    Demo1                     ;物理页,再特别设置两广表项
                mov     di,(PhVB_AD SHR 12)*4
                mov     DWORD PTR es:[di],MPVB_AD+USS+RWW+PL
                mov     di,(LoVB_AD SHR 12)*4
                mov     DWORD PTR es:[di],PhVB_AD+USU+RWR+PL
                mov     ax,PT1_Sel                ;初始化页表1
                mov     es,ax
                xor     di,di
                mov     cx,1024
                mov     eax,400000h
Demo2:          stosd                             ;先把全部表项设置为无效
                add     eax,1000h
                loop    Demo2                     ;再特别设置1项
                mov     di,((LoSC_AD SHR 12)AND 3ffh)*4
                mov     DWORD PTR es:[di],PhSC_AD+USU+RWR+PL
                mov     eax,PDT_AD
                mov     cr3,eax
                mov     eax,cr0
                or      eax,80000000h
                mov     cr0,eax
                jmp     SHORT PageE
PageE:          mov     ax,DemoData_Sel
                mov     ds,ax
                mov     si,OFFSET Mess
                JUMP16  LoCode_Sel,SBegin
Demo3:          mov     eax,cr0
                and     eax,7fffffffh             ;关闭分页机制
                mov     cr0,eax
                jmp     SHORT PageD
PageD:          mov     ax,Normal_Sel
                JUMP16  TempCode_Sel,ToDOS
DemoBegin       ENDP
;----------------------------------------------------------------------------
DemoCodeLen     =       $
DemoCodeSeg     ENDS
;============================================================================
TempCodeSeg     SEGMENT PARA USE16                ;临时任务的代码段
                ASSUME  CS:TempCodeSeg
;----------------------------------------------------------------------------
Virtual         PROC    FAR
                cld                               ;为演示在启用分页机制后执
                mov     ax,SCode_Sel              ;行位于较高线性地址空间中
                mov     ds,ax                     ;的代码作准备
                mov     ax,TPSCode_Sel
                mov     es,ax
                mov     si,OFFSET SBegin
                mov     di,si
                mov     cx,MLen                   ;把分页演示代码复制到预定
                rep     movsb                     ;内存
                JUMP16  DemoCode_Sel,DemoBegin
ToDOS:          mov     ds,ax
                mov     es,ax
                mov     eax,cr0                   ;准备返回实模式
                and     al,11111110b
                mov     cr0,eax
                JUMP16  <SEG Real>,<OFFSET Real>
Virtual         ENDP
;----------------------------------------------------------------------------
TempCodeSeg     ENDS
;============================================================================
RCodeSeg        SEGMENT PARA USE16                ;实方式的初始化代码和数据
                ASSUME  CS:RCodeSeg,DS:RCodeSeg
;----------------------------------------------------------------------------
VGDTR           PDesc   <GDTLen-1,>
;----------------------------------------------------------------------------
Start           PROC
                push    cs
                pop     ds
                cld
                call    InitGDT                   ;初始化全局描述符表GDT
                EnableA20
                lgdt    QWORD PTR VGDTR           ;装载GDTR
                cli                               ;关中断
                mov     eax,cr0
                or      al,1
                mov     cr0,eax
                JUMP16  <TempCode_Sel>,<OFFSET Virtual>
Real:           DisableA20
                sti
                mov     ax,4c00h
                int     21h
Start           ENDP
;----------------------------------------------------------------------------
InitGDT         PROC
                push    ds
                mov     ax,GDTSeg
                mov     ds,ax
                mov     cx,GDNum
                mov     si,OFFSET EFFGDT
InitG:          mov     ax,[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR [si].BaseL,ax
                mov     BYTE PTR [si].BaseM,dl
                mov     BYTE PTR [si].BaseH,dh
                add     si,SIZE Desc
                loop    InitG
                pop     ds
                mov     bx,16
                mov     ax,GDTSeg
                mul     bx
                mov     WORD PTR VGDTR.Base,ax
                mov     WORD PTR VGDTR.Base+2,dx
                ret
InitGDT         ENDP
;----------------------------------------------------------------------------
RCodeSeg        ENDS
                END     Start

2.关于实例十的说明

上述演示程序的许多内容与其它实例相同，下面仅就演示分页管理机制方面的内容作些说明：

(1)部分演示代码的移动

为了充分说明分页机制所实现的线性地址到物理地址的转换，在初始化时把部分演示代码移动 到预定的内存区域。预定的内存区域从00303000H开始，即页码为00303H的物理页。该部分演示 代码的功能是显示指定的字符串。在进入保护模式后做此初始化工作的原因是预定的内存区域 在扩展内存中，注意初始化时还没有启用分页机制。

(2)页映射表的初始化

页目录表安排在页码为00200H的物理页中，页表0安排在页码为00202H的物理页中，页表1安排 在页码为00201H的物理页中。演示程序涉及的线性地址空间不超过007FFFFFH，所以只使用两张 页表，为此页目录表中的其它项被置为无效(P=0)。
页表0把线性地址空间中的00000000H—003FFFFFH映射到物理地址空间中。实例在初始化页 表0时，使该线性地址空间直接映射到相同地址的物理地址空间，除线性地址空间中页码 为000B8H和000F0H这两页以外。000B8H页被映射到页码为00301H的物理页，而000F0H页被映射 到页码为000B8H的物理页。
页表1把线性地址空间中的00400000H—007FFFFFH映射到物理地址空间中。实例在初始化页 表1时，似乎使该线性地址空间直接映射到相同地址的物理地址空间，但是处理对应线性地址空 间中00402H的表项被另外设置外，其它表项中的P位为0，也即表示对应物理页不存在。初始化 后，页表1的第2项把线性地址空间中的00402H页映射到页码为00303H的物理页，也就是存放部 分演示代码的指定内存区域。

(3)启动分页管理机制

在建立好页映射表后，启用分页机制所要做的操作是简单的，只要把控制寄存器CR0中的最高 位，也就是PG位置1。具体指令如下：
        mov     eax,cr0
        or      eax,80000000h
        mov     cr0,eax
        jmp     SHORT PageE
PageE:  ...
在启用分页机制前，线性地址就是物理地址；在启用分页机制后，线性地址要通过分页机制 的转换，才成为物理地址。尽管使用一条转移指令，可清除预取队列，但随后在取指令时使 用的线性地址就要经过分页机制转换才成为物理地址。为了顺利过渡，在启用分页机制之后 的过渡代码段，仍要维持线性地址等同于物理地址。为了作到这一点，在建立也映射表时， 必须使实现过渡的代码所在的线性地址空间页映射到具有相同地址的物理地址空间页。实例 中页表0就做到了这一点。

(4)关闭分页管理机制

只要把控制寄存器CR0中的PG位清0，便关闭了分页机制。在这一过渡阶段，也要保持地址转 换前后的一致。

(5)地址转换的演示

在启用分页机制之后，就转移到位于线性地址空间中00402000H处开始的代码，该部分代码的 功能是显示提示信息"Page is OK!"。实际上这部分代码存放在从物理地址00303000H开始的 物理内存区域中，是在初始化时被移到此区域的。
在显示提示信息时，要把显示的ASCII字符和显示属性填到线性地址空间中000F0000H开始的 区域中，而不是000B8000H开始的区域。从初始化时建立的映射表可见，线性地址空间中 的000F0H页，被映射到物理地址空间中的000B8H页。所以，向线性地址空间中的000F0H页 写，实际上是向物理地址空间中的000B8H页写，也就是真正显示。

(6)页级保护的说明

在进入保护模式之后，特权级一直是0，所以，无论系统级和用户级页，无论只能读/执行， 还是读/执行/写，总是可进行各种形式的访问。


十一.虚拟8086模式

继推出80386之后，Intel又推出了80386、Pentium和Pentium PRO。这些处理器都具有 实模式和保护模式两种工作方式。前面已介绍过，实模式与8086兼容，可以运行DOS及 以其为平台的几乎所有软件；但在实模式下，处理器不能发挥自身的优越性能，不能支 持多用户、多任务操作系统的运行。为了充分发挥处理器的功能，同时使DOS及以其为 平台的软件继续有效地运行，从80386开始增加了虚拟8086模式。本文将介绍虚拟8086模 式。这里下载本文源代码。

<一>V86模式

1.V86模式

虚拟8086模式是保护模式下的一种工作方式，也称为V8086模式，或者简称为V86模式。在 虚拟8086模式下，处理器类似于8086。寻址的地址空间是1M字节；段寄存器的内容作为段 值解释；20位存储单元地址由段值乘以16加偏移构成。在V86模式下，代码段总是可写的， 这与实模式相同，同理，数据段也是可执行的，只不过可能会发生异常。所以，在虚 拟8086模式下，可以运行DOS及以其为平台的软件。但V86模式毕竟是虚拟8086的一种方 式，所以不完全等同于8086。
当标志寄存器中的VM位为1时，处理器就处于V86模式。此时，其当前特权级由处理器自动 设置为3。

2.V86任务

8086程序可以直接在V86模式下运行，而V86模式受到称为V86监控程序的控制。V86监控程 序和在V86模式下的8086程序构成的任务称为虚拟8086任务，或者简称为V86任务。V86任务 形成一个由处理器硬件和属于系统软件的监控程序组成的“虚拟8086机”。V86监控程序控 制V86外部界面、中断和I/O。硬件提供该任务最低端1M字节线性地址空间的虚拟存储空间， 包含虚拟寄存器的TSS，并执行处理这些寄存器和地址空间的指令。
80386把V86任务作为与其它任务具有同等地位的一个任务。它可以支持多个V86任务，每 个V86任务是相对独立的。所以，通过V86模式这种形式，运行8086程序可充分发挥处理器 的能力和充分利用系统资源。

<二>进入和离开V86模式

保护模式和V86模式之间的切换情形如下图所示。图中左面部分为V86任务。从图中可 见，V86模式与保护模式的切换可发生在V86任务之内，这种切换是V86模式下的8086程序 与保护模式下的监控程序之间的转换；V86模式与保护模式的切换可发生在任务之间，这 种切换是V86任务与其它任务的切换。此外，V86监控程序与其它任务之间的切换是普通的 任务切换。
由于80386没有提供直接改变VM标志的指令，并且只有当前特权级CPL=0时，对VM的改变才 有效，所以V86模式与保护模式的切换不能简单地通过改变VM位而进行。下面介绍V86模式 与保护模式之间的切换，也就是如何进入和离开V86模式。为了方便，先介绍如何离开V86模式。

1.离开V86模式

在V86模式下，如果处理器响应中断/异常，那么就会退出当前V86任务的V86模式。
在V86模式下，处理器对中断/异常的响应处理不同于真正的8086，而仍然采用保护模式下对 中断/异常响应处理的方法。所以，在V86模式下，不是根据位于线性地址空间最低端的中断 向量表内的对应中断向量转入处理程序，而是根据中断描述符表IDT内的对应门描述符的指示 转入处理程序。

(1)在V86任务内离开V86模式

如果对应的门描述符是386中断门或386陷阱门，那么就发生在当前V86任务内从V86模式到保 护模式的转换。80386要求执行这种中断/异常处理程序时的CPL必须等于0。
由于V86模式下的CPL=3，而转换到保护模式后的CPL=0，所以这种转换包含了特权级的变换。 在转入处理程序之前，处理器先将V86模式下的段寄存器GS、FS、DS及ES压入0级堆栈，并在 进入保护模式下的处理程序之前装入空选择子。为保持使堆栈对齐，把段寄存器压入堆栈时 一律按32位值压入，低16位是段寄存器的值，高16位为空。于是，转换后的0级堆栈如下图所 示。其中，段寄存器SS和CS的值也是V86模式下的段值。图(a)是没有出错码的情形；图(b)是 有出错码的情形。
在这种V86任务内从V86模式转换到保护模式的过程中，为了保证中断/异常处理程序工作于 特权级0，对目标代码段描述符特权级进行检查，如果由目标代码段描述符特权级决定的CPL不 等于0，将引起通用保护异常。此外，标志寄存器EFLAGS中的VM位被清0，从而使得中断/异常 处理在保护模式下进行，也即离开V86模式。
这种情况下，相应的中断/异常处理在当前V86任务之内进行。中断异常处理程序可以检查保 存在堆栈中的EFLAGS映象，根据VM位的值来确定被中断程序的工作模式。如果VM=1，那么被 中断的程序工作于V86模式，是8086程序；否则，被中断的程序工作于保护模式，是V86监控 程序。

(2)任务切换离开V86模式

如果对应的门描述符是任务门，那么就发生从当前V86任务到其它任务的切换，也就离开了当 前V86任务的V86方式。象普通任务切换一样，V86模式的各通用寄存器、段寄存器、指令指针 和标志寄存器EFLAGS等保存到原V86任务的386TSS中。被保存的段寄存器的内容是V86模式下 的段值。被保存的EFLAGS内的VM=1。
这种情况下，相应的中断异常处理在另一个任务内进行。目标任务可以是普通任务，也可以是 另一个V86任务。如果目标任务TSS内的EFLAGS字段内的VM=1，那么就转入另一个V86任务 的V86模式。

2.进入V86模式

与离开V86模式的两条途径相对应，有两条进入V86模式的途径。

(1)通过IRET指令进入V86模式

通过在中断/异常处理结束时使用IRET指令返回被中断的程序继续执行。指令IRET的执行步骤 如下所示：
(1)若NT=1，则进行任务切换，然后转步骤6；
(2)否则从堆栈中弹出EIP、CS和EFLAGS；
(3)若VM=1且CPL=0，则恢复外层堆栈及其它段寄存器，然后转步骤6；
(4)若无特权级变换则转步骤6；
(5)否则恢复外层堆栈；
(6)结束
尽管上述步骤不够细致和没包括异常情况，但还是体现了指令IRET执行时所处理的三种情形。 第一种情形是当前EFLAGS中的NT=1，也即嵌套任务返回，那么就进行任务切换，指向目标任 务TSS的选择子在当前任务TSS的连接字段。NT=0表示当前中断/异常处理程序与被中断程序属 于同一任务，于是就从堆栈弹出EIP、CS和EFLAGS。第二和第三中情形是NT=0的条件下产生的。 第二种情形是弹出的EFLAGS中VM=0，表示被中断的程序是普通保护模式程序，那么就考虑特 权级变换，如果向外层返回，那么就恢复外层堆栈指针。不允许向内层返回，否则将会引起 通用保护异常。前文中介绍的IRET指令的动作只考虑了情形一和情形二，并不是IRET指令的 完整动作。
第三种情形是弹出的EFLAGS中VM=1且当前正运行程序的CPL=0，表示被中断的程序是V86模式 下的8086程序，当前是从同一V86任务下的中断/异常处理程序返回。由于V86模式的特权级 是3，所以要进行堆栈切换，也即从堆栈中弹出3级堆栈的指针(ESP和SS)。此外，还从堆栈中 弹出段寄存器ES、DS、FS和GS。在这种情形下，弹到各段寄存器(包括CS和SS)的内容都作为 段值，而非选择子。这种处理动作对应于上述第一种离开V86模式的情形，有关堆栈操作与上 图所示的堆栈内容相符。当然，如果产生异常时提供出错码，那么异常处理程序在利用IRET指 令返回时，必须确保堆栈指针指向如上图所示保存EIP的单元。简单的实现方法是，异常处理 程序在执行IRET前，先从堆栈中弹出出错码。
利用指令IRET处理的这第三种情形，可以方便地从V86任务下的中断/异常处理程序返回到V86模 式下的8086程序。利用这条途径还可以直接进入V86模式。为此，先在0级堆栈中形成如上图(a)所 示的栈顶。对应EIP值是V86模式下要执行的8086程序入口点的16位偏移；对应CS值是V86模式下要 执行的8086程序入口点的段值；对应EFLAGS值中的VM位必须是1；对应SS和ESP的值是要执行 的8086程序的堆栈指针；对应ES、DS、FS和GS的值是相应的段值。然后，在CPL=0和NT=0的情况 下，执行IRET指令。实际上，这种进入V86模式的途径是，先建立一个V86模式下执行的8086程序 被中断而离开V86模式的环境，然后再返回。需要注意的是，若当前正执行程序的CPL不为0， 则再执行IRET指令时不会进入V86模式，但也不产生异常，EFLAGS中的VM位被处理器自动清0。
不能通过RET指令进入V86模式，因为它不改变EFLAGS的内容。任何不能修改EFLAGS中VM位的指令 均不能切换到V86模式。

(2)通过任务切换进入V86模式

通过任务切换的途径，可以从其它任务进入V86任务内的V86模式。
利用在前文介绍的任务切换的方法可以进行任务切换。如果目标任务由386TSS描述，并且其 中EFLAGS字段内的VM位为1，那么在切换到目标任务时，也就进入了V86模式。在切换到V86模式 时，CPL被规定为3。目标任务TSS中的各段寄存器字段被解释为8086可以接受的段值，而不是选 择子。任务切换时也将装载LDTR和CR3。
程序在V86模式下执行时，EFLAGS寄存器中的NT位被忽略，所以，不能在V86模式下用IRET指令 完成一个任务切换，并使其工作于V86以外的工作模式。在V86模式下执行IRET指令时，将弹出 IP、CS及FLAGS寄存器，以恢复被中断的程序，而不考虑NT位的值。
如果利用这条途径建立V86任务并进入V86模式，那么主要是把对应386TSS中EFLAGS字段内的VM位 置1，把8086程序的有关段值填入对应386TSS中的相应段寄存器字段。此外，如果V86监控程序需 要用到LDT，那么还要填写LDTR字段；如果需要采用分页机制，那么还要填写CR3字段(当新任务 为V86模式的任务时，只装入段寄存器，而没有装入描述符投影寄存器的动作)。

<三>演示进入和离开V86模式的实例(实例十一)

下面给出一个用于演示进入和离开V86模式的实例。该实例的逻辑功能是，以驻留方式结束程序， 退出时已处于V86模式。该实例演示内容包括：两种方式进入V86模式和两种方式离开V86模式； V86模式下的8086程序如何调用实模式下的软中断处理程序。

1.演示步骤和源程序清单

为了便于演示，本实例含有三个任务：临时任务，V86任务和INTFF任务。在实模式下做必要的 初始化工作后切换到保护模式，也即进入临时任务，开始演示。演示分两个阶段：第一阶段进 入V86任务的V86模式，并驻留退出；第二阶段进入INTFF任务，切换到临时任务，并返回实模式。
第一阶段的演示步骤如下：
(1)开始临时任务后，作切换到V86任务的准备；
(2)切换到V86任务，由于V86任务TSS中的EFLAGS字段内的VM=1，所以伴随着任务切换就进入 了V86模式。
(3)进入V86任务的V86模式后，显示提示信息，驻留结束，出现DOS提示符，第一阶段至此结束。
在V86模式下，可进行各种操作，运行其它8086程序。如果8086程序引起通用保护异常，那么在 屏幕上显示提示信息，并中止该8086程序。如果在8086程序中执行“INT 0FFH”指令，开始第二 阶段。第二阶段的演示步骤如下：
(1)进入INTFF任务后，显示提示信息，切换到临时任务；
(2)在临时任务内切换到实模式；
(3)在实模式下中止发出“INT 0FFH”的程序
源程序由如下几部分组成：
(1)全局描述符表GDT；
(2)中断描述符表IDT(只适用于V86任务)；
(3)INTFF任务的TSS段、LDT段、0级堆栈段和代码段；
(4)V86任务的TSS段、LDT段、0级堆栈段、3级堆栈段及数据段，通用保护异常处理程序 段和其它中断/异常处理程序段，V86模式下的8086程序段；
(5)临时任务的TSS段和代码段；
(6)实模式下的初始化代码段及有关过程。
源程序清单如下：
;名称:ASM11.ASM
;功能:演示进入和离开V86方式
;编译:TASM ASM11.ASM
;连接:TLINK ASM11.OBJ
;============================================================================
INCLUDE         386SCD.INC
;============================================================================
GDTSeg          SEGMENT PARA USE16                ;全局描述符表数据段(16位)
;----------------------------------------------------------------------------
                ;全局描述符表
GDT             LABEL   BYTE
                ;空描述符
DUMMY           Desc    <>
                ;规范段描述符及选择子
Normal          Desc    <0ffffh,,,ATDW,,>
Normal_Sel      =       Normal-GDT
                ;显示缓冲区数据段描述符及选择子
Video           Desc    <07fffh,8000h,0bh,ATDW,,>
Video_Sel       =       Video-GDT
;----------------------------------------------------------------------------
EFFGDT          LABEL   BYTE
                ;V86任务TSS段描述符及选择子
V86TSS          Desc    <V86TSSLen-1,V86TSSSeg,,AT386TSS,,>
V86TSS_Sel      =       V86TSS-GDT
                ;V86任务局部描述符表的描述符及选择子
V86LDT          Desc    <V86LDTLen-1,V86LDTSeg,,ATLDT,,>
V86LDT_Sel      =       V86LDT-GDT
                ;IntFF任务TSS段描述符及选择子
IntFFTSS        Desc    <IntFFTSSLen-1,IntFFTSSSeg,,AT386TSS,,>
IntFFTSS_Sel    =       IntFFTSS-GDT
                ;IntFF任务局部描述符表的描述符及选择子
IntFFLDT        Desc    <IntFFLDTLen-1,IntFFLDTSeg,,ATLDT,,>
IntFFLDT_Sel    =       IntFFLDT-GDT
                ;临时任务的任务状态段描述符及选择子
TempTSS         Desc    <TempTSSLen-1,TempTSSSeg,,AT386TSS,,>
TempTSS_Sel     =       TempTSS-GDT
                ;临时任务代码段描述符及选择子
TempCode        Desc    <0ffffh,TempCodeSeg,,ATCE,,>
TempCode_Sel    =       TempCode-GDT
;----------------------------------------------------------------------------
GDNum           =       ($-EFFGDT)/(SIZE Desc)    ;需特殊处理的描述符数
GDTLen          =       $-GDT                     ;全局描述符表长度
GDTSeg          ENDS                              ;全局描述符表段定义结束
;============================================================================
IDTSeg          SEGMENT PARA USE16                ;V86任务使用的中断描述符表
IDT             LABEL   BYTE
                ;对应0--12号中断/异常的中断门描述符
                REPT    13
                Gate    <,TPCode_Sel,,AT386IGate+DPL3,>
                ENDM
                ;通用保护故障处理程序门描述符
                Gate   <GPBegin,GPCode_Sel,,AT386TGate+DPL3,>
                ;对应14--254号中断/异常的中断门描述符
                REPT    241
                Gate    <,TPCode_Sel,,AT386IGate+DPL3,>
                ENDM
                ;对应255号中断的任务门描述符
                Gate    <,IntFFTSS_Sel,,ATTaskGate+DPL3,>
;----------------------------------------------------------------------------
IDTLen          =       $-IDT
IDTSeg          ENDS                              ;中断描述符表段定义结束
;============================================================================
;IntFF任务的TSS段
IntFFTSSSeg     SEGMENT PARA USE16
                DD      0                         ;链接字
                DD      0                         ;0级堆栈指针(实例不使用)
                DW      0,0                       ;0级堆栈选择子(实例不使用)
                DD      0                         ;1级堆栈指针(实例不使用)
                DW      0,0                       ;1级堆栈选择子(实例不使用)
                DD      0                         ;2级堆栈指针(实例不使用)
                DW      0,0                       ;2级堆栈选择子(实例不使用)
                DD      0                         ;CR3
                DW      IntFFBegin,0              ;EIP
                DD      0                         ;EFLAGS
                DD      0                         ;EAX
                DD      0                         ;ECX
                DD      0                         ;EDX
                DD      0                         ;EBX
                DD      IntFFStackLen             ;ESP
                DD      0                         ;EBP
                DD      0                         ;ESI
                DD      0                         ;EDI
                DW      Normal_Sel,0              ;ES
                DW      IntFFCode_Sel,0           ;CS
                DW      IntFFStack_Sel,0          ;SS
                DW      Normal_Sel,0              ;DS
                DW      Normal_Sel,0              ;FS
                DW      Normal_Sel,0              ;GS
                DW      IntFFLDT_Sel,0            ;LDTR
                DW      0                         ;调试陷阱标志
                DW      $+2                       ;指向I/O许可位图
                DB      0ffh                      ;I/O许可位图结束标志
;----------------------------------------------------------------------------
IntFFTSSLen     =       $
IntFFTSSSeg     ENDS
;============================================================================
;IntFF任务的LDT段
IntFFLDTSeg     SEGMENT PARA USE16
FLDT            LABEL   BYTE
                ;0级堆栈段描述符及选择子
IntFFStack      Desc    <IntFFStackLen-1,IntFFStackSeg,,ATDWA,,>
IntFFStack_Sel  =       IntFFStack-FLDT+TIL
                ;代码段描述符及选择子
IntFFCode       Desc    <IntFFCodeLen-1,IntFFCodeSeg,,ATCER,,>
IntFFCode_Sel   =       IntFFCode-FLDT+TIL
;----------------------------------------------------------------------------
IntFFLDNum      =       ($-FLDT)/(SIZE Desc)
IntFFLDTLen     =       $
IntFFLDTSeg     ENDS
;============================================================================
;IntFF任务的堆栈
IntFFStackSeg   SEGMENT PARA USE16
IntFFStackLen   =       512
                DB      IntFFStackLen DUP(0)
IntFFStackSeg   ENDS
;============================================================================
;IntFF任务的代码段
IntFFCodeSeg    SEGMENT PARA USE16
                ASSUME  CS:IntFFCodeSeg
;----------------------------------------------------------------------------
IntFFMess       DB      'Return to real mode.'
IntFFMessLen    =       $-IntFFMess
;----------------------------------------------------------------------------
IntFFBegin      PROC    FAR
                mov     si,OFFSET IntFFMess
                mov     ax,Video_Sel
                mov     es,ax
                mov     di,0
                mov     ah,17h
                mov     cx,IntFFMessLen
                cld
INext:          mov     al,BYTE PTR cs:[si]
                inc     si
                stosw
                loop    INext
                JUMP16  TempTSS_Sel,0
IntFFBegin      ENDP
;----------------------------------------------------------------------------
IntFFCodeLen    =       $
IntFFCodeSeg    ENDS
;============================================================================
;V86任务的TSS段
V86TSSSeg       SEGMENT PARA USE16
                DD      0                         ;链接字
                DD      V86Stack0Len              ;0级堆栈指针
                DW      V86Stack0_Sel,0           ;0级堆栈选择子
                DD      0                         ;1级堆栈指针(实例不使用)
                DW      0,0                       ;1级堆栈选择子(实例不使用)
                DD      0                         ;2级堆栈指针(实例不使用)
                DW      0,0                       ;2级堆栈选择子(实例不使用)
                DD      0                         ;CR3
                DW      V86Begin,0                ;EIP
                DD      IOPL3 OR VMFL             ;EFLAGS(IO特权级为3,VM=1)
                DD      0                         ;EAX
                DD      0                         ;ECX
                DD      0                         ;EDX
                DD      0                         ;EBX
                DD      V86Stack3Len              ;ESP
                DD      0                         ;EBP
                DD      0                         ;ESI
                DD      0                         ;EDI
                DW      V86CodeSeg,0              ;ES
                DW      V86CodeSeg,0              ;CS
                DW      V86Stack3Seg,0            ;SS
                DW      V86CodeSeg,0              ;DS
                DW      V86CodeSeg,0              ;FS
                DW      V86CodeSeg,0              ;GS
                DW      V86LDT_Sel,0              ;LDTR
                DW      0                         ;调试陷阱标志
                DW      $+2                       ;指向I/O许可位图
                DB      4000h/8 DUP(0)            ;I/O许可位图
                DB      0ffh                      ;I/O许可位图结束标志
;----------------------------------------------------------------------------
V86TSSLen       =       $
V86TSSSeg       ENDS
;============================================================================
;V86任务的LDT段
V86LDTSeg       SEGMENT PARA USE16
VLDT            LABEL   BYTE
;----------------------------------------------------------------------------
                ;V86任务线性地址空间中最低端1M字节段的描述符及描述符
VAllMem         Desc    <0ffffh,,,ATDWA,0fh,>
VAllMem_Sel     =       VAllMem-VLDT+TIL
                ;V86任务0级堆栈段描述符及选择子
V86Stack0       Desc    <V86Stack0Len-1,V86Stack0Seg,,ATDWA,,>
V86Stack0_Sel   =       V86Stack0-VLDT+TIL
                ;V86任务数据段描述符及选择子
V86Data         Desc    <V86DataLen-1,V86DataSeg,,ATDR,,>
V86Data_Sel     =       V86Data-VLDT+TIL
                ;V86任务中断/异常处理程序代码段描述符及选择子
TPCode          Desc    <TPCodeLen-1,TPCodeSeg,,ATCE,,>
TPCode_Sel      =       TPCode-VLDT+TIL
                ;V86任务通用保护异常处理程序代码段描述符及选择子
GPCode          Desc    <GPCodeLen-1,GPCodeSeg,,ATCE,,>
GPCode_Sel      =       GPCode-VLDT+TIL
;----------------------------------------------------------------------------
V86LDNum        =       ($-VLDT)/(SIZE Desc)
V86LDTLen       =       $
V86LDTSeg       ENDS
;============================================================================
;V86任务的0级堆栈
V86Stack0Seg    SEGMENT PARA USE16
V86Stack0Len    =       512
                DB      V86Stack0Len DUP(0)
V86Stack0Seg    ENDS
;============================================================================
;V86任务的3级堆栈
V86Stack3Seg    SEGMENT PARA USE16
V86Stack3Len    =       1024
                DB      V86Stack3Len DUP(0)
V86Stack3Seg    ENDS
;============================================================================
;V86任务数据段
V86DataSeg      SEGMENT PARA USE16
GPErrMess       DB      '......General Protection Error......'
GPErrMessLen    =       $-GPErrMess
V86DataLen      =       $
V86DataSeg      ENDS
;============================================================================
;定义部分代表堆栈单元的符号
Perr            EQU     <WORD PTR [BP+0]>
Pip             EQU     <WORD PTR [bp+4]>
Pcs             EQU     <WORD PTR [bp+8]>
Pflag           EQU     <WORD PTR [bp+12]>
Psp             EQU     <WORD PTR [bp+16]>
Pss             EQU     <WORD PTR [bp+20]>
Pes             EQU     <WORD PTR [bp+24]>
Pds             EQU     <WORD PTR [bp+28]>
Pfs             EQU     <WORD PTR [bp+32]>
Pgs             EQU     <WORD PTR [bp+36]>
;============================================================================
;V86任务下的中断/异常处理程序代码段
TPCodeSeg       SEGMENT PARA USE16
                ASSUME  CS:TPCodeSeg
;----------------------------------------------------------------------------
TPBegin         PROC    FAR
Count           =       0
                REPT    256                       ;对应256个入口
                IF      Count EQ 21h
Ent21H          LABEL   BYTE                      ;在第21H项处定义标号Ent21H
                ENDIF
                push    bp
                mov     bp,Count                  ;置中断向量号到BP
                jmp     Process                   ;都转统一的处理程序
Count           =       Count+1
                ENDM
Process:        push    bp                        ;保存BP
                mov     bp,sp                     ;堆栈指针送BP
                push    eax
                push    ebx                       ;保存EAX、EBX
                ;在V86堆栈顶形成返回点的现场
                mov     ax,VAllMem_Sel            ;转载描述最低1M字节线性地址
                mov     ds,ax                     ;  空间的描述符选择子
                xor     eax,eax
                mov     ax,Psp                    ;修改在V86任务0级堆栈中保存
                sub     ax,3*2                    ;  的3级堆栈的指针，减3个字
                mov     Psp,ax                    ;  即在栈顶空出3个字
                xor     ebx,ebx
                mov     bx,Pss                    ;使EBX指向V86堆栈顶
                shl     ebx,4
                add     ebx,eax
                mov     ax,Pip                    ;把保存在0级堆栈中的返回地址
                mov     WORD PTR [ebx],ax         ;的偏移部分送V86堆栈
                mov     ax,Pcs
                mov     WORD PTR [ebx+2],ax       ;段值部分送V86堆栈
                mov     ax,Pflag
                mov     WORD PTR [ebx+4],ax       ;标志值送V86堆栈
                ;用对应的中断向量值代替返回地址
                mov     bx,[bp]                   ;取中断向量号
                shl     bx,2                      ;乘4
                mov     ax,[bx]                   ;取实模式下对应中断向量的偏移
                mov     Pip,ax                    ;代替0级堆栈中的EIP
                mov     ax,[bx+2]                 ;取实模式下对应中断向量的段值
                mov     Pcs,ax                    ;代替0级堆栈中的CS
                pop     ebx                       ;恢复现场
                pop     eax
                pop     bp
                pop     bp
                ;从保护方式返回V86方式
                ;先转入对应的中断处理程序,再返回中断发生处
                iretd
TPBegin         ENDP
;----------------------------------------------------------------------------
TPCodeLen       =       $
TPCodeSeg       ENDS
;============================================================================
;V86任务下的通用保护故障处理程序代码段
;----------------------------------------------------------------------------
GPCodeSeg       SEGMENT PARA USE32
                ASSUME  CS:GPCodeSeg
;----------------------------------------------------------------------------
GPBegin         PROC    FAR
                mov     ax,V86Data_Sel
                mov     ds,ax
                mov     si,OFFSET GPErrMess
                mov     ax,Video_Sel
                mov     es,ax
                mov     di,0
                mov     ah,17h
                mov     cx,GPErrMessLen
                cld
GNext:          lodsb
                stosw
                loop    GNext
                add     esp,4
                mov     ax,4c01h
                JUMP16  TPCode_Sel,Ent21H
GPBegin         ENDP
;----------------------------------------------------------------------------
GPCodeLen       =       $
GPCodeSeg       ENDS
;============================================================================
;V86方式执行的8086程序段
V86CodeSeg      SEGMENT PARA USE16
                ASSUME  CS:V86CodeSeg,DS:V86CodeSeg
Message         DB      'V86 is OK!',0dh,0ah,24h
V86Begin        PROC    FAR
                mov     ah,9
                mov     dx,OFFSET Message
                int     21h
                mov     ax,RCodeSeg
                sub     ax,GDTSeg
                mov     dx,OFFSET TSRLine+15
                shr     dx,4
                add     dx,ax
                add     dx,10h
                mov     ax,3100h
                int     21h
V86Begin        ENDP
V86CodeSeg      ENDS
;============================================================================
TempTSSSeg      SEGMENT PARA USE16                ;临时任务的TSS段
                TSS     <>
                DB      0ffh                      ;I/O许可位图结束标志
TempTSSLen      =       $
TempTSSSeg      ENDS
;============================================================================
TempCodeSeg     SEGMENT PARA USE16                ;临时任务的代码段
                ASSUME  CS:TempCodeSeg
;----------------------------------------------------------------------------
Virtual         PROC    FAR
                mov     ax,Normal_Sel
                mov     ds,ax
                mov     es,ax
                mov     fs,ax
                mov     gs,ax
                mov     ss,ax
                mov     ax,TempTSS_Sel            ;装载TR
                ltr     ax
                JUMP16  V86TSS_Sel,0              ;直接切换到演示任务
ToDos:          clts
                mov     eax,cr0                   ;准备返回实模式
                and     al,11111110b
                mov     cr0,eax
                JUMP16  <SEG Real>,<OFFSET Real>
Virtual         ENDP
;----------------------------------------------------------------------------
TempCodeSeg     ENDS
;============================================================================
RDataSeg        SEGMENT PARA USE16                ;实方式数据段
RDataSeg        ENDS
;============================================================================
RCodeSeg        SEGMENT PARA USE16
                ASSUME  CS:RCodeSeg,DS:RCodeSeg
;----------------------------------------------------------------------------
VGDTR           PDesc   <GDTLen-1,>               ;GDT伪描述符
VIDTR           PDesc   <IDTLen-1,>               ;IDT伪描述符
NORVIDTR        PDesc   <>                        ;用于保存原IDTR值
SPVar           DW      ?                         ;用于保存实方式下的SP
SSVar           DW      ?                         ;用于保存实方式下的SS
;----------------------------------------------------------------------------
Start           PROC
                mov     ax,RCodeSeg
                mov     ds,ax
                cld
                call    InitGDT                   ;初始化全局描述符表GDT
                call    InitIDT                   ;初始化中断描述符表IDT
                mov     ax,V86LDTSeg
                mov     fs,ax
                mov     cx,V86LDNum
                mov     si,OFFSET VLDT
                call    InitLDT
                mov     ax,IntFFLDTSeg
                mov     fs,ax
                mov     cx,IntFFLDNum
                mov     si,OFFSET FLDT
                call    InitLDT
                mov     SSVar,ss
                mov     SPVar,sp
                lgdt    QWORD PTR VGDTR           ;装载GDTR并切换到保护方式
                sidt    QWORD PTR NORVIDTR        ;保存IDTR
                cli                               ;关中断
                lidt    QWORD PTR VIDTR           ;装载IDTR
                mov     eax,cr0
                or      al,1
                mov     cr0,eax
                JUMP16  <TempCode_Sel>,<OFFSET Virtual>
Real:           mov     ax,cs
                mov     ds,ax
                lss     sp,DWORD PTR SPVar        ;又回到实方式
                lidt    QWORD PTR NORVIDTR
                sti
                mov     ax,4c00h
                int     21h
Start           ENDP
;----------------------------------------------------------------------------
TSRLine         LABEL   BYTE
;----------------------------------------------------------------------------
InitGDT         PROC
                push    ds
                mov     ax,GDTSeg
                mov     ds,ax
                mov     cx,GDNum
                mov     si,OFFSET EFFGDT
InitG:          mov     ax,[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR [si].BaseL,ax
                mov     BYTE PTR [si].BaseM,dl
                mov     BYTE PTR [si].BaseH,dh
                add     si,SIZE Desc
                loop    InitG
                pop     ds
                mov     bx,16
                mov     ax,GDTSeg
                mul     bx
                mov     WORD PTR VGDTR.Base,ax
                mov     WORD PTR VGDTR.Base+2,dx
                ret
InitGDT         ENDP
;----------------------------------------------------------------------------
;入口参数:FS:SI=第一个要初始化的描述符,CX=要初始化的描述符数
;----------------------------------------------------------------------------
InitLDT         PROC
                mov     ax,WORD PTR FS:[si].BaseL
                movzx   eax,ax
                shl     eax,4
                shld    edx,eax,16
                mov     WORD PTR fs:[si].BaseL,ax
                mov     BYTE PTR fs:[si].BaseM,dl
                mov     BYTE PTR fs:[si].BaseH,dh
                add     si,SIZE Desc
                loop    InitLDT
                ret
InitLDT         ENDP
;----------------------------------------------------------------------------
InitIDT         PROC
                push    ds
                mov     ax,IDTSeg
                mov     ds,ax
                mov     cx,256-1
                mov     si,OFFSET IDT
                mov     ax,OFFSET TPBegin
IIDT1:          cmp     cx,256-1-13
                jz      IIDT2
                mov     [si],ax
IIDT2:          add     si,8
                add     ax,7
                loop    IIDT1
                pop     ds
                mov     bx,16
                mov     ax,IDTSeg
                mul     bx
                mov     WORD PTR VIDTR.Base,ax
                mov     WORD PTR VIDTR.Base+2,dx
                ret
InitIDT         ENDP
;----------------------------------------------------------------------------
RCodeSeg        ENDS
                END     Start
用于演示INTFF任务的源程序如下：
;功能：用于演示V86模式下的INTFF任务
;编译：TASM INTFF.ASM
;连接：TLINK INTFF.OBJ
;====================================================================
Text            SEGMENT
                ASSUME  cs:Text,ds:Text
;---------------------------------------
Start           PROC
                int     0ffh
                mov     ax,4c00h
                int     21h
Start           ENDP
;---------------------------------------
Text            ENDS
                END     Start

2.说明

(1)对IDT表的初始化

为了方便地书写IDT表，采用了重复汇编。从采用重复汇编方式定义的IDT表可见，除对应通用 保护异常的13号陷阱门描述符和255号任务门描述符外，其它中断门描述符内的偏移均未设定。 为此，在实模式下初始化时，把相应的入口偏移填入这些门描述符。从源程序可见，这些处理 程序的入口偏移也用重复汇编书写，并且字节数相同，所以间隔等长。

(2)任务切换方式进入V86模式

实例从临时任务切换到V86任务时进入V86模式。在V86任务的TSS中，EFLAGS字段内的VM=1，所 以伴随着任务切换，就进入了V86模式。为此，TSS中对应各段寄存器字段内的初始值都是V86模 式下要执行的8086程序各段的段值，而非选择子。由于在发生中断/异常时要进入V86任务的特 权级0，所以初始化了0级堆栈指针。V86任务使用局部描述符表LDT，所以TSS中对应字段填有相 应的选择子。

(3)V86模式下对中断的处理

实例对V86模式下响应中断和执行软中断指令“INT n”的处理方法是，转实模式下的对应中断 处理程序。具体步骤如下：
1)在V86模式堆栈(V86任务的3级堆栈)顶形成返回点的现场；
2)用实模式下对应的中断向量值代替返回地址；
3)从保护模式返回V86模式。由于堆栈中保存的EFLAGS内的VM=1，所以在保护模式下执 行IRET指令时，返回V86模式；由于在0级堆栈中保存的返回地址(段值和偏移)已被修改成实模 式下的中断向量，所以这时的返回也就是转入实模式下的对应中断处理程序；由于在V86堆栈顶 已安排了返回地址，所以在执行实模式下对应中断处理程序时，遇到IRET指令就返回到V86任务 的被中断处。这种处理方法似乎绕了个弯。但就是利用这个方法有效地调用了DOS功能，方便地 显示了提示信息“V86 is OK.”，顺利地实现了驻留退出。
这种处理方法没有充分考虑异常，更没有考虑异常时的出错码。

(4)V86任务的通用保护异常处理

在演示的第一阶段和第二阶段，不会发生任何异常。但在这两个阶段之间，由于允许执行其它 程序，可能引起异常。为了简单，实例只考虑了通用保护异常，而未考虑其它异常。该可能发 生异常的阶段是在V86模式下。如果发生通用保护异常，那么就转入V86任务的通用保护异常处 理程序。通用保护异常处理程序在保护模式下显示提示信息 “......General Protection Error......”，然后再转21H号中断处理程序，通过设置入口 参数AX=4C01H，中止引起通用保护异常的程序。

(5)INTFF任务

为了演示以任务切换方式离开V86模式，在实例中安排了这一任务，并称之为INTFF任务。由于 中断描述符表IDT中的255(0FFH)号描述符是任务门描述符，所以当在V86模式下执行软中断指 令“INT 0FFH”时便从V86模式切换到INTFF任务。INTFF任务的TSS是初始化好的，在INTFF任 务下不发生特权级变换，不使用局部描述符表LDT。INTFF任务先显示提示信息 “Return to real mode.”，然后再切换到临时任务。

<四>V86模式下的敏感指令

在V86模式下，CPL=3，执行特权指令时，或者要引起出错码为0的通用保护故障，或者要引起 非法操作码故障。
在V86模式下，输入/输出指令的敏感条件有所变化。指令CLI和STI的敏感条件不变，由于CPL=3， 所以如果IOPL<3，那么执行CLI或STI指令将引起通用保护故障。输入/输出指令IN、INS、OUT或OUTS的 敏感条件仅仅是当前V86任务TSS内的I/O许可位图，而忽略EFLAGS中的IOPL。输入/输出指 令IN、INS、OUT或OUTS是否可以执行与CPL是否小于IOPL无关，而直接由I/O许可位图对应位决 定，如果输入/输出指令所使用I/O地址对应的I/O许可位图内的各位都位0，则输入输出指令可 正常执行，否则引起通用保护故障。
此外，在V86模式下，指令PUSHF、POPF、INT n和IRET却对IOPL敏感。也就是说，在V86模式下， 当IOPL<3时，执行指令PUSHF、POPF、INT n及IRET会引起出错码为0的通用保护故障。
采取上述措施的目的是使操作系统软件可以支持一个“虚拟EFLAGS”寄存器。