Linux80X86保护模式及其编程(四)
80X86保护模式及其编程
-
80X86基础知识
-
保护模式内存管理
-
各种保护措施
-
中断和异常处理
-
任务管理
-
保护模式编程的初始化
-
一个简单的多任务内核
4.1 80X86系统寄存器和系统指令
为了协助处理执行初始化和控制系统操作,80X86提供了一个标志寄存器EFLAGS和几个系统寄存器,除了一些通用状态标志外,EFLAGS中还包含了几个系统标志。这些系统标志用于控制任务切换、中断处理、指令跟踪以及访问权限。系统寄存器用于内存管理和控制处理器操作,含有分段和分页处理机制系统表的基地址、控制处理操作的比特标志位。
4.1.1标志寄存器
标志寄存器EFLAGS中的系统标志和IOPL字段用于控制I/O访问、可屏蔽硬件中断、调试、任务切换以及虚拟-8086模式,见下图。通过只允许操作系统代码有权修改这些标志。EFLAGS中的其他标志是一些通用标志。(进位CF、奇偶PF、辅助进位AF、零标志ZF、负号SF、方向DF、溢出OF)。这里我们仅对EFLAGS中的系统标志进行说明。
TF 位8是跟踪标志(TrapFlag)。当设置该位时可为调试操作启动单步执行方式;复位时则禁止单步执行。在单步执行方式下,处理器会在每个指令执行之后产生一个调试异常,这样我们就可以观察执行程序在执行每条指令后的状态。如果程序使用POPF、POPFD或IRET指令设置了TF标志,那么在随后指令之后处理器就会产生一个调试异常。
LOPL 位13-12是I/O特权级(I/O Privilege Level)字段。该字段指明当前运行程序或任务的I/O特权级LOPL。当前程序或任务的CPL(CPL是指 "Current Privilege Level",即"当前特权级)必须小于等于这个IOPL才能访问I/O地址空间。只有当CPL为特权级0时,程序才可以使用POPF或IRET指令修改这个字段。IOPL也是控制对IF标志修改的机制之一。
NT 位14是嵌套任务标志(Nested Task)。它控制着被中断任务和调用任务之间的链接关系。在使用CALL指令、中断或异常执行任务调用时,处理器会设置该标志。在通过使用IRET指令从一个任务返回时,处理器会检查并修改这个NT标志。使用POPF/POPFD指令也可以修改这个标志,但是应用程序中改变这个标志的状态会产生不可意料的异常。
RF 位16是恢复标志(Resume Flag)。该标志用于控制处理器对断点指令的响应。当设置时,这个标志会临时禁止断点指令产生的调试异常;当该标志复位时,则断点指令将会产生异常。RF标志的主要功能时允许在调试异常之后重新执行一条指令。当调试软件使用IRETD指令返回被中断程序之前,需要设置堆栈上EFLAGS内容中的RF标志,以防止指令断点造成另一个异常。处理器会在指令返回之后自动地清楚该标志,从而再次允许指令断点异常。
VM 位17是虚拟-8086方式(Virtual-8086Mode)标志。当设置该标志时,就开启虚拟-8086方式;当复位该标志时,则回到保护模式。
4.1.2内存管理寄存器
处理器提供了4个内存管理寄存器(GDTR、LDTR、IDTR和TR),用于指定分段内存所使用的系统表的基地址,见图 处理器为这些寄存器的加载和保存提供了特定的指令。
GDTR、LDTR、IDTR和TR都是段基址寄存器,这些段中含有分段机制的重要信息表。GDTR、IDTR和LDTR用于寻址存放描述符表的段。TR用于寻址一个特殊的任务状态段TSS(Task State Segment)。TSS段中包含着当前执行任务的重要信息。
1.全局描述符表寄存器GDTR
GDTR寄存器中用于存放全局描述符表GDT的32位线性基地址和16位表长度值。基地址指定GDT表中字节0在线性地址空间中的地址,表长指明GDT表的字节长度值。指令LGDT和SGDT分别用于加载和保存GDTR寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF。在保护模式初始化过程中必须给GDTR加载一个新值。
2.中断描述符表寄存器IDTR
与GDTR的作用类似,IDTR寄存器用于存放中断描述符表IDT的32位线性基地址和16位表长度值。指令LIDT和SIDT分别用于加载和保存IDTR寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为0,为长度值被设置为0xFFFF。
3.局部描述符表寄存器LDTR
LDTR寄存器中用于存放局部描述符LDT的32位线性基地址、16位段限长和描述符属性值。指令LLDT和SLDT分别用于加载和保存LDTR寄存器的段描述符部分。包含LDT表的段必须在GDT表中有一个段描述符项。当使用LLDT指令把含有LDT表段的选择符加载到LDTR时,LDT段描述符的段基地址、段限长度以及描述符属性被自动地加载到LDTR中。当进行任务切换时,处理器会把新任务LDT的段选择符和段描述符自动地加载进LDTR中。在机器加电或处理器复位后,段选择符和基地址被默认地设置为0,而段长度被设置成0xFFFF。
4.任务寄存器TR
TR寄存器用于存放当前任务TSS的16位段选择符、32位基地址、16位段长度和描述符属性值。它引用GDT表中的一个TSS类型的描述符。指令LTR和STR分别用于加载和保存TR寄存器的段选择符部分。当使用LTR指令把选择符加载进任务寄存器时,TSS描述符中的段基地址、段限长度以及描述符属性会被自动地加载到任务寄存器中。当执行任务切换时,处理器会把新任务TSS的段选择符和段描述符自动地加载进任务寄存器TR中
。
4.1.3控制器寄存器
控制器寄存器(CR0、CR1、CR2和CR3)用于控制二号确定处理器的操作模式以及当前执行任务的特性,见下图。CR0中含有控制处理器操作模式和状态的系统控制标志;CR1保留不用;CR2含有导致页错误的线性地址。CR3中含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器PDBR(Page-Directory Base address Register)
1.CR0中协处理器控制位
CR0的4个比特位:扩展类型位ET、任务切换位TS、仿真位EM和数学存在位MP用于控制80X86浮点(数学)协处理器的操作。CRO的ET位(标志)用于选择与协处理器进行通信所使用的协议,即指明系统中使用的是80387还是80287协处理器。TS、MP和EM位用于确定浮点指令或WAIT指令是否应该产生一个设备不存在DNA(Device Not Available)异常。这个异常可用来仅为使用浮点运算的任务保存和恢复浮点寄存器。对于没有使用浮点运算的任务,这样做可以加快它们之间的切换操作。
ET CR0的位4是扩展类型(Extension Type)标志。当该标志为1时,表示指明系统有80387协处理器存在,并使用32位协处理器协议。ET=0指明使用80278协处理器。如果仿真位EM=1,则该位将被忽略。在处理器复位操作时,ET位会被初始化指明系统中使用的协处理器类型。如果系统中有80387,则ET被设置成1,否则若有一个80287或者没有协处理器,则ET被设置成0。
TS CR0的位3是任务已切换(Task Switched)标志。该标志用于推迟保存任务切换时的协处理器内容,直到新任务开始实际执行协处理器指令。处理器在每次任务切换时都会设置该标志,并且在执行协处理器指令时测试该标志。
如果设置了TS标志并且CR0的EM标志为0,那么在执行任何协处理器指令之前会产生一个设备不存在DNA(Device Not Available)异常。如果设置了TS标志但没有设置CR0的MP和EM标志,那么在执行协处理器指令WAIT/FWAIT之前不会产生设备不存在异常。如果设置了EM标志,那么TS标志对协处理器指令的执行无影响。见下面的表
在任务切换时,处理器并不自动保存协处理器的上下文,而是会设置TS标志。这个标志会使得处理器在执行新任务指令流的任何时候遇到一条协处理器指令时产生设备不存在异常。设备不存在异常的处理程序可使用CLTS指令清楚TS标志,并且保存协处理器的上下文。如果任务从没有使用过协处理器,那么相应协处理器上下文就不用保存。
EM CR0的位2是仿真(EMulation)标志。当该位置设置时,表示处理器没有内部或外部协处理器,执行协处理器指令时引起设备不存在异常;当清楚时,表示系统有协处理器。设置这个标志可以迫使所有浮点指令使用软件来模拟。
MP CR0的位1是监控协处理器(Monitor Coprocessor或Math Present)标志。用于控制WAIT/FWAIT指令与TS标志的交互作用。如果MP=1、TS=1,那么执行WAIT指令将产生一个设备不存在异常;如果MP=0,则TS标志不会影响WAIT的执行。
2.CRO中保护控制位
PE CR0的位0是启用保护(Protection Enable)标志。当设置该位时即开启了保护模式;当复位时即进入实地址模式。这个标志仅开启段级保护,而并没有启用分页机制。若要启用分页机制,那么PE和PG标志都要置位。
PG CR0的位31是分页(Paging)标志。当设置该位时即开启了分页机制;当复位时则禁止分页机制,此时所有线性地址等同于物理地址。在开启这个标志之前必须已经或者同时开启PE标志。即若要启用分页机制,那么PE和PG标志都要置位。
WP 对于Intel80486或以上的CPU,CRO的位16是写保护(WriteProtect)标志。当设置该标志时,处理器会禁止超级用户程序(例如0的程序)向用户级读页面执行写操作;当该复位时则反之。该标志有利于UNIX类操作系统在创建进程时实现写时复制(Copy on write)技术。
NE 对于Intel80486或以上的CPU,CRO的位5是协处理器错误(Numeric Error)标志。当设置该标志时,就启用了X87协处理器错误报告机制。当NE为复位该标志,那么就使用PC机形式的X87协处理器错误报告机制。当NE为复位状态并且CPU的IGNNE输入引脚有信号时,那么非屏蔽的数学协处理器X87错误将导致处理器通过FERR引脚在外部产生一个中断,并且在执行下一个等待形式浮点指令或WAIT/FWAIT指令之前立刻停止指令执行。CPU的FERR引脚用于仿真外部协处理器80387的ERROR引脚,因此通常连接到中断控制器输入请求引脚上。NE标志、IGNNE引脚和FERR引脚用于利用外部逻辑来实现PC机形式的外部错误报告机制。
启用保护模式PE(Protected Enable)位(位0)和开启分页PG(Paging)位(位31)分别用于控制分段和分页机制。若果PE=1,处理器就工作在开启分段机制环境下,即运行在保护模式下。如果PE=0,则处理器关闭了分段机制,并如同8086工作于实地址模式下。PG用于控制分页机制。如果PG=1,则开启了分页机制。如果PG=0,分页机制被禁止,此时线性地址被直接作为物理地址使用。
如果PE=0、PG=0,处理器工作在实地址模式下;如果PG=0、PE=1,处理器工作在没有开启分页机制的保护模式;如果PG=1、PE=0,此时由于不在保护模式下不能启用分页机制,因此处理器会产生一个一般保护异常,即这种标志组合无效;如果PG=1、PE=1、则处理器工作在开启了分页机制的保护模式。
当改变PE和PG位时,我们必须小心。只有当执行程序起码有部分代码和数据线性地址空间和物理地址空间具有相同地址时,我们才能改变PG位的位置。此时这部分具有相同地址的代码在分页和未分页之间起着桥梁的作用。另外,在开启分页(PG=1)之前必须先刷新CPU中的页高速缓冲(或称为转换查找缓冲区TLB --Translation Lookaside Buffers)
在修改了PE位之后程序必须立刻使用一条跳转指令,以刷新处理器执行管道中已经获取的不同模式下的任何指令。在设置PE位之前,程序必须初始化几个系统段和控制寄存器。在系统刚加上电时,处理器被复位成PE=0、PG=0(即实模式状态),以允许引导代码在启动分段和分页机制之前能够初始化这些寄存器和数据结构
3. CR2和CR3
CR2和CR3用于分页机制。CR3含有存放页目录表页面的物理地址.因此CR3也被称为PDBR.因为页目录表面是页页对齐的。所以该寄存器只有高20位是有效的。而低12位保留供更高级处理器使用,因此在往CR3中加载一个新值是低12位必须设置为0。
使用MOV指令加载CR3时具有让页高速缓冲无效的副作用。为了减少地址转换所要求的总线周期数量,最近访问的页目录和页表会被存放在处理器的页高速缓冲器件中,该缓冲器件被称为转换查找缓冲区TLB(Tranlastion Lookaside Buffer)。只有当TLB中不包含页表项时才会使用额外的总线周期从内存中读取页表项。
即使CR0中的PG位处于复位状态(PG=0),我们也能先加载CR3。以允许对分页机制进行初始化。当切换任务时,CR3的内容也会随之改变。但是如果新任务CR3值与原任务的一样,处理器就无需刷新页高速缓冲。这样共享页表的任务可以执行得更快。
CR2用于出现页异常时报告出错信息。在报告页异常时,处理器会把引起异常得线性地址存放在CR2中。因此操作系统中得页异常处理程序可以通过检查CR2的内容来确定线性地址空间中哪一个页面引发了异常。
4.1.4系统指令
系统指令用于处理系统级功能,例如加载系统寄存器、管理中断等。大多数系统指令只能由处于特权级0的操作系统软件执行,其余一些指令可以在任何特权级上执行。因此应用程序也能使用。
4.2保护模式内存管理
4.2.1内存寻址
内存是指一组有序字节组成的数组,每个字节有唯一的内存地址。内存寻址则是指对存储在内存中的某个指定数据对象的地址进行定位。这里,数据对象是指存储在内存中的一个指定数据类型或字符串。80X86支持多种数据类型:1字节、2字节(1个字)或4字节(双字或长字)的无符号整型数或带符号整型数,以及多字节字符串等。通常字节中某一比特位的定位或寻址可以基于字节来寻址,因此最小数据类型的寻址是对1字节数据(数值或字符)的定位。通常内存地址从0开始编址,对于80X86CPU来说,其地址总线宽度为32位,因此一共有2^32个不同物理地址。即内存物理地址空间有4G,总共可以寻址4G字节的物理内存。对于多字节数据类型(例如2字节整数数据类型),在内存中这些字节相邻存放。80X86首先存放低值字节,随后地址处存放高值字节。因此80X86CPU是一种先存小值(Little Endium)的处理器。
对于80X86CPU,一条指令主要由操作码(Opcode)和操作对象即操作数(Oprand)构成。操作数可以位于一个寄存器中,也可以在内存中。若要定位内存中的操作数,就要进行内存寻址。80X86有许多指令的操作数涉及内存寻址,并且针对所寻址对象数据类型的不同,也有很多不同的寻址方案可供选择。为了进行内存寻址。
80X86使用了一种称为段(Segment)的寻址技术。这种寻址技术把内存空间分成一个或多个称为段的线性区域,从而对内存中一个数据对象的寻址就需要使用一个段的起始地址(即段地址)和一个段内偏移地址两部分构成。段地址部分使用16位的段选择符指定,其中14位可以选择2^14次方即16384个段。段内偏移地址部分使用32位的值来指定,因此段内地址可以是0到4G。即一个段的最大长度可达4G。程序中由16位的段和32位的偏移构成的48位地址或长指针称为一个逻辑地址(虚拟地址)。它唯一确定了一个数据对象的段地址和段内偏移地址。而仅由32位偏移地址或指针指定的地址是基于当前段的对象地址。
80X86为段部分提供了6个存放段选择符的段寄存器CS、DS、ES、SS、FS和GS。其中CS总是用于寻址代码段,而堆栈则专门用SS段寄存器。在任何指定时刻由CS寻址的段称为当前代码段。此时EIP寄存器中包含了当前代码段内下一条要执行指令的段内偏移地址。因此要执行指令的地址可表示成CS:[EIP]。后面将说明的段间控制转移指令可以被用来CS和EIP赋予新值,从而可以把执行位置改变到其他的代码段中,这就实现了在不同段中程序的控制传递。
由段寄存SS寻址的段称为当前堆栈段。栈顶由ESP寄存器内容指定。因此堆栈顶是SS:[EIP]。另外4个段寄存器是通用寄存器。当指令中没有指定所操作数据的段时,那么DS将是默认的数据段寄存器。
为了指定内存操作数的段内偏移地址,80X86指令规定了计算偏移量的很多方式,称为指令寻址方式。指令的偏移量由三部分相加组成:基地址寄存器、变址寄存器和一个偏移量。即:
** 偏移地址 = 基地址 +(变址x比例因子)+偏移量**
4.2.2地址变换
任何完整的内存管理系统都包含两个关键部分:保护和地址变量。提供保护措施是可以防止一个任务访问另一个任务或操作体系统的内存区域。地址变换能够让操作系统在给任务分配内存时具有灵活性,并且因为我们可以让某些物理地地址不被任何逻辑地址所映射,所在地址变换过程中同时也提供了内存保护功能。
计算机中的物理内存时字节的线性数组,每个字节具有一个唯一的物理地址;程序中的地址由有部分构成的逻辑地址。这种逻辑地址并不能直接用于访问物理内存,需要使用地址变换机制将它变换或映射到物理内存地址上。内存管理机制即用于将这种逻辑地址转换成物理内存地址。
为了减少确定地址变换所需要的信息,变换或映射通常以内存块作为操作单位。分段机制和分页机制是两种广泛使用的地址变换技术。它们的不同之处在于逻辑低地址是如何组织被映射的内存块、变换信息如何指定以及编程人员如何进行操作。分段和分页操作都使用驻留在内存中的表来指定它们各自的变换信息。这些表只能由操作系统访问,以防止应用程序擅自修改。
80X86在从逻辑地址到物理地址变换过程中使用分段和分页两种机制。第一阶段使用分段机制把程序的逻辑地址变换成处理器可寻址内存空间(称为线性地址空间)中的地址。第二阶段使用分页机制则是供选用的。如果没有启用分页机制,那么分段机制产生的线性地址空间就直接映射到处理器的物理地址空间上。物理地址空间定义为处理器在其地址总线上能够产生的地址范围。
- 分段机制
分段提供了隔绝各个代码、数据和堆栈区域的机制,因此多个程序(或任务)可以运行在同一个处理器而不会互相干扰。分页机制为传统需求页、虚拟内存提供了实现机制。其中虚拟内存用于实现程序代码按要求被映射到物理内存中。分页机制当然也能用于提供多任务之间的隔离措施。
分段提供一中机制,用于把处理器可寻址的线性地址空间划分成一些较小的称为段的受保护地址空间区域。段可以用来存放程序的代码、数据和堆栈,或者来存放系统数据结构(例如TSS或LDT)。如果处理器中有多个程序或任务在运行,那么每个程序可分配各自的一套段。此时处理器就可以加强这些段之间的界限,并且确保一个程序不会通过访问另一个程序的段而干扰程序的执行。分段机制还允许对段进行分类。这样,对特定类型段的操作能够受到限制。
一个系统中所有使用的段都包含在处理器线性地址空间中。为了定位指定段中的一个字节,程序必须提供一个逻辑地址。逻辑地址包括一个段选择符和一个偏移量。段选择符是一个段的唯一标识。另外,段选择符提供了段描述附表(例如全局描述符表GDT)中一个数据结构(称为段描述符)的偏移量。每个段都有一个段描述符。段描述符指明段的大小、访问权限和段的特权级、段类型以及段的第1个字节在线性地址空间中的位置(称为段的基地址)。逻辑地址的偏移量部分加到段的基地址上就可以定位段中某个字节的位置。因此基地址加上偏移量就形成了处理器线性地址空间中的地址。
线性地址空间与物理地址空间具有相同的结构。相对于两维的逻辑地址空间来说,它们两者都是一维地址空间。虚拟地址(逻辑地址)空间可包含最多16K的段,而每个段最长可达4GB,使得虚拟地址空间容量达到64TB(246)。线性地址空间和物理地址空间都是4GB(232)。实际上,如果禁用分页机制,那么线性地址空间就是物理地址空间。
- 分页机制
因为多任务系统通常定义的线性地址空间都要比其含有的物理内存容量大得多,所以需要使用某种"虚拟化"线性地址空间得方法,即使用虚拟存储技术。虚拟存储是一种内存管理技术,使用这种技术可让编程人员产生内存空间要比计算机中实际物理内存容量大很多得错觉。利用这种错觉,我们可以随意编制大型程序而无需考虑实际物理内存究竟有多少。
分页机制支持虚拟存储技术。在使用虚拟存储得环境中,大容量得线性空间需要使用小块得物理内存(RAM或ROM)以及某些外部存储空间(例如大容量硬盘)来模拟。当使用分页时,每个段被划分成页面(通常每页为4KB大小),页面会被存储于物理内存中或硬盘上。操作系统通过维护一个页目录和一些页表来留意这些也页面。当程序(或任务)试图访问线性地址空间中得一个地址位置时,处理器就会使用页目录和页表把线性地址换成一个物理地址,然后在该内存位置上执行所要求得操作(读或写)。
如果当前被访问的页面不在一个物理内存中,处理器就会中断程序的执行(通过产生一个页错误异常)。然后操作系统就可以从硬盘上把该页面读入物理内存中,并继续执行刚才被中断的程序。当操作系统严格实现了分页机制时,那么对于正确执行的程序来说页面在物理内存和硬盘之间的交换就是透明的。
80X86分页机制最适合支持虚拟存储技术。分页机制会使用大小固定的内存块,而分段管理则使用了大小可变的块来管理内存。无论在物理内存中还是在硬盘上,分页使用固定大小的块更为适合管理物理内存。另一方面,分段机制使用大小可变的块更适合处理复杂系统的逻辑分区。可以定义与逻辑块大小合适的内存单元而无需受到固定大小页面的限制。每个段都可以作为一个单元来处理,从而简化了段的保护和共享操作。
分段和分页是两种不同的地址变换机制,它们都对整个地址变换操作提供独立的处理阶段。尽管两种机制都使用存储在内存中的变换表,但所用的表结构不同。实际上,段表存储在线性地址空间,而页表则保存在物理地址空间。因而段变换表可由分页机制重新定位而无需段机制的信息或合作。段变换机制把虚拟地址(逻辑地址)变换成线性地址,并且在线性地址空间中访问自己的表,但是并不知晓分页机制把这些线性地址转换成物理地址的过程。类似地,分页机制也不知程序产生地址的虚拟地址空间。分页机制只是简单地把线性地址转换成物理地址,并且在物理内存中访问自己的转换表。
4.2.3保护
80X86支持两类保护。其一是通过给每个任务不同的虚拟地址(逻辑地址)空间来完全隔离各个任务。这是通过给每个任务逻辑地址到物理地址不同的变换映射来做到。另一个保护机制对任务进行操作,以保护操作系统内存段和处理器特殊系寄存器不被应用程序访问。
1.任务之间的保护
保护的一个重要方面是提供应用程序各任务之间的保护能力。80X86使用的方法是通过把每个任务放置在不同的虚拟地址空间中,并给予每个任务不同的逻辑地址到物理地址的变换映射。每个任务中的地址变换功能被定义成一个任务中的逻辑地址到映射到物理内存的一部分区域,而另一个任务中的逻辑地址映射到物理内存中的不同区域中。这样,因为一个任务不可能生成能够映射到其他任务逻辑地址对应使用的物理内存部分,所以所有任务都被隔绝开了。只需给每个任务各自独立的映射表,每个任务就会有不同的地址变换函数。在80X86中,每个任务都有自己的段表和页表。当处理器切换去执行一个新任务时,任务切换的关键部分就是切换到新任务的变换表。
通过在所有任务中安排具有相同的虚拟到物理地址映射部分, 并且把操作系统存储在这个公共的虚拟地址空间部分,操作系统可以被所有任务共享。这个所有任务都具有相同虚拟地址空间部分被称为全局地址空间(Global address space)。这也正是现代Linux操作系统使用虚拟地址空间的方式。
每个任务唯一的虚拟地址空间部分被称为局部地址空间(Local address space)。局部地址空间含有需要与系统中其他任务区别开的私有的代码和数据。由于每个任务中具有不同的局部地址空间,因此两个不同任务中对相同虚拟地址处的引用将转换到不同的物理地址处。这使得操作系统可以给予每个任务的内存相同的虚拟地址,但仍然能隔绝每个任务。另一方面,所有任务在全局地址空间中对相同虚拟地址的引用将被转换到同一个物理地址处。这给公共代码和数据(例如操作系统)的共享提供支持。
2.特权级保护
在一个任务中,定义了4个执行特权级(Privilege Levels),用于依据段中含有数据的敏感度以及任务中不同程序部分的受信程序,来限制对任务中各段的访问。最敏感的数据被赋予了最高特权级,它们只能被任务中最受信任的部分访问。不太敏感的数据被赋予较低的特权级,它们可以被任务中较低特权级的代码访问。
特权级用数字0到3表示,0具有最高特权级,而3则时最低特权级。每个内存段都与一个特权级相关联。这个特权级限制具有足够特权级的程序来访问一个段。我们知道,处理器从CS寄存器指定的段中取得和执行指令,当前特权级(Current Privilege Level),即CPL就是当前活动代码段的特权级,并且它定义了当前所执行程序的特权级别。CPL确定了哪些段能被程序访问。
每当程序企图访问一个段时,当前特权级就会与段的特权级进行比较,以确定是否有访问许可。在给定CPL级别上执行的程序允许访问同级别或低级别的数据段。任何对高级别段的引用都是非法的,并且会引发一个异常来通知操作系统。
每个特权级都有自己的程序栈,以避免使用共享栈带来的保护问题。当程序从一个特权级切换到另一个特权级上执行时,堆栈段也随之改换到新级别的堆栈中。
4.3分段机制
分段机制可用于实现多种系统设计。这些设计范围从使用分段机制的最小功能来保护程序的平坦模型,到使用分段机制创建一个可同时可靠地运行多个程序(或任务)的具有稳固操作环境的多段模型。
多端模型能够利用分段机制全部功能提供由硬件增强的代码、数据结构、程序和任务的保护措施。通常,每个程序(或任务)都使用自己的段描述符表以及自己的段。对程序来说段能够完全是私有的,或者是程序之间共享的。对所有段以及系统上运行程序各自执行环境的访问都由硬件控制。
访问检查不仅能够用来保护对段界限以外地址的引用,而且也能用来在某些段中防止执行不允许的操作。例如,因为代码段被设计成是只读形式的段,因此可以用硬件来防止对代码段执行写操作。段中的访问权限信息也可以用来设置保护环或级别。保护级别可用于保护操作系统程序不受应用程序非法访问。
4.3.1段的定义
在上一节概述中已经提到,保护模式中80X86提供了4GB的物理地址空间。这是处理器在其地址总线上可以寻址的地址空间。这个地址空间是平坦的,地址范围从0到0XFFFFFFFF。这个物理地址空间可以映射到读写内存、只读内存以及内存映射I/O中。分段机制就是把虚拟地址空间中的虚拟内存组织成以些长度可变的称为段的内存块单元。80386虚拟地址空间中的虚拟地址(逻辑地址)由一个段部分和一个偏移部分构成。段是虚拟地址到线性地址转换机制的基础。每个段由三个参数定义:
1.段基地址(Base address),指定段在线性地址空间中的开始地址。基地址是线性地址,对应于段中偏移0处。
2.段限长(Limit),是虚拟地址空间中段内最大可用偏移位置。它定义了段的长度。
3.段属性(Attributes),指定段的特性。例如该段是否可读、可写或可作为一个程序执行;段的特权级等。
段限长定义了在虚拟地址空间中段的大小。段基址和段限长定义了所映射的线性范围或区域。段内0到limit的地址范围对应线性地址中范围base到base+limit。偏移量大于段限长的虚拟地址是无意义的,如果使用则会导致异常。另外,若访问一个段并没有得到段属性许可则也会导致异常。例如,如果你试图写一个只读的段,那么80386就会产生一个异常。另外,多个映射到线性地址中的范围可以部分重叠或覆盖,甚至完全重叠,见下图所示。在本书介绍的Linux0.1x系统中,一个任务的代码和数据段的段限长相同,并被映射到线性地址完全相同而重叠的区域上。
段的基地址、段限长以及段的保护属性存储在一个称为段描述符(Segment Descriptor)的结构项中。在逻辑地址到线性地址的转换映射过程中会使用这个段描述符。段描述符保存在内存中的段描述附表(Descriptor table)中。段描述符表是包含段描述符项的一个简单数组。前面介绍的段选择符即用于通过指定表中一个段描述符的位置来指定相应的段。
即使利用段的最小功能,使用逻辑地址也能访问处理器地址空间中的每个字节。逻辑地址由16位的段选择符和32位的偏移量组成,见下图段选择符指定字节所在的段,而偏移量指定该字节段中相对于段基地址的位置。处理器会把每个逻辑地址转成线性地址。线性地址是处理器线性地址空间中的32位地址。与物理地址空间类似,线性地址空间也是平坦的4GB地址空间,地址范围从0到0xFFFFFFFF。线性地址空间中含有为系统定义的所有断喝系统表。
为了把逻辑地址转换成一个线性地址,处理器会执行以下操作:
1. 使用段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符(仅当一个新的段选择符加载到寄存器时才需要这一步。)
2. 利用段描述符检验段的访问权限和范围,以确保该段是可访问的并且偏移量位于段界限内。
3. 把段描述符中取得的段基地址加到偏移量上,最后形成一个线性地址。
如果没有开启分页,那么处理器直接把线性地址映射到物理地址(即线性地址被送到处理器地址总线上)。如果对线性地址空间进行了分页处理,那么就会使用二级地址转换成线性地址转换物理地址。页转换将在稍后进行说明。
4.3.2段描述符表
段描述符表是段描述符的一个数组,见图。描述符表的长度可变,最多可以8192个8个字节描述符。有两种描述符表:全局描述符表GDT(Global descriptor table);局部描述表LDT(Local descriptor table)。
描述符表存储在由操作系统维护着的数据结构中,并且由处理器的内存管理硬件来引用。这些特殊结构应该保存在仅由操作系统软件访问的受保护的内存区域中,以防止应用程序修改其中的地址转换信息。虚拟地址空间被分割成大小相等的两半。一半由GDT来映射变换的线性地址,另一半则由LDT来映射。整个虚拟地址空间共含有214个段:一半空间(213个段)是由GDT映射的全局虚拟地址空间,另一半是由LDT映射的局部虚拟地址空间。通过指定一个描述附表(GDT或LDT)以及表中描述符号,我们就可以定位一个描述符。
当发生任务切换时,LDT会更换成新任务的LDT,但是GDT并不会改变。因此,GDT所映射的一半虚拟地址空间是系统所有任务共有的,但是LDT所映射的另一半则在任务切换时被改变。系统中所有任务共享的段由GDT来映射。这样的段通常包括含有操作系统的段以及所有任务各自的包含LDT的特殊段。LDT段可以想象成属于操作系统的数据。
图出一个任务中的段如何能在GDT和LDT之间分开。图中共有6个段,分别用于两个应程序(A和B)以及操作系统。系统中每个应用程序对应一个任务,并且每个任务有自己的LDT。应用程序A在任务A中运行,拥有LDT(A),用来映射段Code(A)和Data(A)。类似地,应用程序B在任务B中运行,使用LDT(B)来映射Code(B)和Data(B)段。包含操作系统内核的两个段Code(os)和Data(os)使用GDT来映射,这样它们可以被两个任务所共享。两个LDT段:LDT(A)和LDT(B)也GDT来映射。
当任务A在运行时,可访问的段包括LDT(A)映射的CodeA和Data(A)段,加上GDT映射的操作系统的段Code(os)和Data(os)。当任务B在运行时,可访问的段包括LDT(B)映射二的Code(B)和Data(B)段,加上GDT映射的段。
这个例子通过让每个任务使用不同的LDT,演示了虚拟地址空间如何能够被组织成隔离每个任务。当任务A在运行时,任务B的段不是虚拟地址空间的部分,因此任务A没有办法访问任务B的内存。同样地,当任务B运行时,任务A的段也不能被寻址。这种使用LDT来隔离每个应用程序的方法,正是关键保护需求之一。
每个系统必须定义一个GDT,并可用于系统中所有程序或任务。另外,可选定义一个或多个LDT。例如,可以为每个运行任务定义一个LDT,或者某些或所有任务共享一个LDT。
GDT本身并不是一个段,而是线性地址空间中的一个数据结构。GDT的基线性地址和长度值必须加载进GDTR寄存器中。GDT的基地址应该进行内存8字节对齐,已得到最佳处理器性能。GDT的限长以字节为单位。与段类似,限长值加上基地址可得到最后表中最后一个字节的有效地址。限长为0表示有1个有效字节。因为段描述符总是8字节长,因此GDT的段长值应该设置总是8的倍数减一(即8N-1)。
处理器并使用GDT中的第1个描述符。把这个"空描述符"的段选择符加载进一个数据寄存器(DE、ES、FS或GS)并不会产生一个异常,但是若使用这些加载了空描述符的段选择符访问内存时就肯定会产生一般保护性异常。通常使用这个段选择符初始化段寄存器,那么意外引用未使用的段寄存器肯定会产生一个异常。
LDT表存放在LDT类型的系统段中。此时GDT必须含有LDT的段描述符。如果系统支持多LDT的话,那么每个LDT都必须在GDT中有一个段描述符和段选择符。一个LDT的段描述符可以存放在GDT表的任何地方。
访问LDT需要使用其段选择符。为了在访问LDT时减少地址转换次数,LDT的段选择符、基地址、段限长以及访问权限需要存放在LDTR寄存器中。
当保存GDTR寄存器内容时(使用SGDT指令),一个48位的"伪描述符"被存储在内存中。为了在用户模式(特权级3)避免对齐检查出错,伪描述符应该存放在一个奇字地址处(即地址MOD 4 =2)。这会让处理器先存放一个对齐的字,随后是一个对齐的双字(4字节对齐处)。用户模式程序通常不会保存伪描述符,但是可以通过使用这种对齐方式来避免产生一个对齐检查出错的可能性。当使用SIDT指令保存IDTR寄存器内容时也需要使用同样的对齐方式。然而,当保存LDTR或任务寄存器(分别使用SLTR或STR指令)时,伪描述符应该存放在双字对齐的地址处(即地址 MOD 4=0)
4.3.3段选择符
段选择符(或称段选择子)是段的一个16位标识符,见下图所示。段选择符并不直接指向段,而是指向段描述符表中定义段的段描述符。段选择符3个字段内容:
- 请求特权级RPL(Requested Privilege Level);
- 表指示标志TI(Table Index);
- 索引值(Index)
请求特权级字段RPL提供了保护信息,将在后面作详细说明。表索引字段T1用来指出包含指定段描述符的段描述符GDT或LDT。TI=0表示描述符在GDT中;TI=1表示描述符在LDT中。索引字段给出了描述符在GDT或LDT表中的索引项号。可见,选择符通过定位段表中的一个描述符来指定一个段,并且描述符中包含访问一个段的所有信息,例如段的基地址、段长度和段属性。
例如,图4-11(a)中选择符(0x08)指定了GDT中具有RPL=0的段1,其索引字段值是1,TI位是0,指定GDT表。图中4-11(b)选择符(0x10)指定了GDT中具有RPL=0的段2,其索引字段值是2,TI位是0,指定GDT表。图4-11(c)中选择符(0x0f)指定了LDT中具有RPL=3的段1,其索引字段值是1,TI位是1,指定LDT表。图4-11(d)中选择符(0x17)指定了LDT中具有RPL=3的段2,其索引字段值是2,TI位是1,指定LDT表。实际上,图4-11中的前4个选择符:(a)(b)(c)(d)分别就是Linux0.1x内核的内核代码段、内核数据段、任务代码段和任务数据段的选择符。图4-11(e)中的选择符(0xffff)指定LDT表中RPL=3的段8191。其索引字段值是0b11111111111(即8191),TI位等于1,指定LDT表。
另外,处理器不使用GDT表中的第1项。指向GDT该项的选择符(即索引值为0,TI标志为0的选择符)用作为"空选择符",见图4-11(f)所示。当把空选择符加载到一个段寄存器(除了CS和SS以外)中时,处理器并不产生异常。但是当使用含有空选择符的段寄存器用于访问内存时就会产生异常。当把空选择符加载到CS或SS段寄存器中时将会导致一个异常。
对应用程序来说段选择符时作为指针变量的一部分而可见,但选择符的值通常是由链接器编辑器或链接加载程序进行设置或修改,而非应用程序。
为减少地址转换时间和编程复杂性,处理器提供可存放最多6个段选择符的寄存器(见图4-12所示),即段寄存器。每个段寄存器支持特定类型的内存引用(代码、数据或堆栈)。原则上执行每个程序都起码需要把有效的段选择符加载到代码段(CS)、数据段(DS)和堆栈段(SS)寄存器中。处理器还另外提供三个辅助的数据段寄存器(ES、FS和GS),可被用于让当前执行程序(或任务)能够访问其他几个数据段。
对于访问某个段的程序,必须已经把段选择符加载到一个段寄存器中。因此,尽管一个系统可以定义很多的段,但同时只有6个段可供立即访问。若要访问其他段就需要加载这些段的选择符。
另外,为了避免每次访问内存时都去引用描述符表,去读和解码一个段描述符,每个段寄存器都有一个"可见"部分和一个"隐藏"部分(隐藏部分也被称为"描述符缓冲"或"影子寄存器")。当一个段选择符被加载到一个段寄存器可见部分中时,处理器也同时把这段选择符指向的段描述符中的段地址、段限长以及访问控制信息加载到段寄存器的隐藏部分中。缓冲在段寄存器(可见和隐藏部分)中的信息使得处理器可以在进行地址转换时不再需要花费时间从段描述符中读取基地址和和段限长值。
由于影子寄存器含有描述符信息的一个拷贝,因此操作系统必须确保对描述符表的改动应反映在影子寄存器中。否则描述符表中一个段的基地址或限长被修改过,但改动却没有反映到影子寄存器中。处理这中问题最简捷的方法是在对描述符表中描述符作过任何改动之后就立刻重新加载6个段寄存器。这将把描述符表中的相对应段信息重新加载到影子寄存器中。
为加载段寄存器,提供了两类加载指令: - 1.MOV POP LDS LES LSS LGS 以及LFS指令。这些指令显示地直接引用段寄存器
- 2.隐式加载指令,例如使用长指针CALL、JMP和RET指令 IRET、INTn、INTO和INT3等指令。这些指令在操作过程中会附带改变CS寄存器(和某些其他段寄存器)的内容。
MOV指令当然也可以用于把寄存器可见部分存储到一个通用寄存器中。
4.3.4段描述符
段选择符来定位描述符中的一个描述符。段描述符是GDT和LDT表中的一个数据结构项,用于向处理器提供有关一个段的位置和大小信息以及访问控制的状态信息。每个段描述符长度是8字节,含有三个主要字段:段基地址、段限长和段属性。段描述符通常由编译器、链接器、加载器或者操作系统来创建,但绝不是应用程序。下图示出了所有类型段描述符的一般格式。
一个段描述符中各字段和标志的含义如下:
-
段限长字段LIMIT(Segment limit field)
段限长Limit字段用于指定段的长度。处理器会把段描述符中两个段限长字段组合成一个20位的值,并根据颗粒度标志G来指定段限长Limit的实际含义。如果G=0,则段长度Limit范围可从1字节到1MB字节,单位是字节。如果G=1,则段长度Limit范围可从4KB到4GB,单位是4KB。
根据段类型中的段扩展方向标志为E,处理器以两种不同方式使用段限长Limit。对于向上扩展的段(简称上扩段),逻辑地址中的偏移值范围可以从0到段限长值Limit。大于段限长Limit的偏移值将产生一般保护性异常。对于向下扩展的段(简称下扩段),段限长Limit的含义相反。根据默认栈指针大小标志B的设置,偏移值范围可从段限长Limit到0XFFFFFFFF或0XFFFF。而小于段限长Limit的偏移值将产生一般保护性异常。对于下扩段,减小段限长字段中的值会在该段地址空间底部分配新的内存,而不是在顶部分配。80X86的栈总是向下扩展的,因此这种实现方式很适合扩展堆栈。 -
基地址字段BASE(Basee address field)
该字段定义在4GB线性地址空间中一个段字节0所处的位置。处理器会把3个分立的基地址字段组合形成一个32位的值。段基地址对齐16字节边界。虽然这不是要求的,但通过把程序的代码和数据段对齐在16字节边界上,可以让程序具有最佳性能。 -
段类型字段TYPE(Type field)
类型字段指定段或门(Gate)的类型、说明段的访问种类以及段的扩展方向。该字段的解释依赖于描述符类型标志S指明是一个应用(代码或数据)描述符还是一个系统描述符。TYPE字段的编码对代码、数据或系统描述符都不同,下图。 -
描述符类型标志S(Descriptor type flag)
描述符类型标志S指明一个段描述符是系统段描述符(当S=0)还是代码或数据段描述符(S=1)。 -
描述符特权级字段DPL(Descriptor privilege level)
DPL字段指明描述符的特权级。特权级范围从0到3。0级特权级最高,3级最低。DPL用于控制对段的访问。 -
段存在标志P(Segment persent)
段标志P指出一个段是在内存中(P=1)还是不在内存中(P=0)。当一个段描述符的P标志为0时,那么把指向这个段描述符的选择符加载进段寄存器将导致产生一个段不存在异常。内存管理软件可以使用这个标志来控制在某一给定时间实际需要把那个段加载进内存中。这个功能为虚拟存储提供了除分页机制以外的控制。图4-15给出了当P=0时的段描述符格式。当P标志为0时,操作系统可以自由使用格式中标注为可用(Available)的字段位置来保存自己的数据,例如有关不存在段实际在什么地方的信息。 -
D/B(默认操作大小/默认栈指针大小和/或上界限)标志(Default operation size /default stack pointer size and/or upper bound)
根据段描述符描述的是一个可执行代码、下扩数据段还是一个堆栈栈,这个标志具有不同的功能。(对于32位代码和数据段,这个标志应该总是设置为1;对于16位代码和数据段,这个标志被设置为0。)
可执行代码段。此时这个标志称为D标志并用于指出该段中的指令引用有效地址和操作数的默认长度。如果该标志位,默认是32位地址和32位或8位的操作数;如果该标志为0,则默认值是16位和16位或8位的操作数。指令前缀0x66可以用来选择非默认值的操作数大小;前缀0x67可用来选择非默认值的地址大小。
栈段(由SS寄存器指向的数据段)。此时该标志称为B(Big)标志,用于指明隐含堆栈操作(例如PUSH、POP或CALL)时的栈指针大小。如果该标志位,则使用32位栈指针并存放ESP寄存器中;如果该标志为0,则使用16位栈指针并存放在SP寄存器中。如果堆栈段被设置成一个下扩数据段,这个B标志也同时指定了堆栈段的上届限。 -
下扩数据段。此时该标志称为B标志,用于指明堆栈段的上界限。如果设置了该标志,则堆栈段的上界限时0xFFFFFFFF(4GB);如果没有该设置该标志,则堆栈段的上界限0xFFFF(64KB)。
-
颗粒度标志G(Granularity)
该字段用于确定段限长字段Limit值的单位。如果颗粒度标志为0,则段限长值得单位时字节;如果设置了颗粒度标志,则段限长值使用4KB单位。(这个标志不影响段基地址得颗粒度,基地址得颗粒度总是字节单位。)若设置了G标志,那么当使用段限长来检查偏移值时,并不会去检查偏移值的12位最低有效位。例如,当G=1时,段限长为0表明有效偏移值为0到4095。 -
可用和保留比特位(Available and reserved bits)
段描述符第2个双字的位20可供系统软件使用;位21是保留并应该总是设置为0。
4.3.5代码和数据段描述符类型
当段描述符S(描述符类型)标志被置位,则该描述符用于代码或数据段。此时类型字段中最高比特位(第2个双字的位11)用于确定是数据段的描述符(复位)还是代码段的描述符(置位)。
对于数据段的描述符,类型字段的低3位(位8、9、10)被分别用于表示已访问A(Accessed)、可写W(Write-enable)和扩展方向E(Expansion-direction),参见表4-3中有关代码和数据类型字段比特位的说明。根据可写比特位W的设置,一个数据段可以是只读的,也可以是可读可写的。
堆栈段必须是可读/写的数据段。若使用不可写数据段的选择符加载到SS寄存器中,将导致一个一般保护异常。如果堆栈段的长度需要动态地改变,那么堆栈段可以是一个向下扩展的数据段(扩展方向标志置位)。这里,动态改变段限长将导致栈空间被添加到栈底部。
已访问比特位指明从上次操作系统复位该位之后一个段是否被访问过。每当处理器把一个段的段选择符加载进段寄存器,它就会设置该位。该位需要明确地清楚,否则一直保持值位状态。该位可用于虚拟内存管理和调试。
代码段可以是一致性的或非一致性的。向更高特权级一致性代码段的执行控制转移,允许程序以当前特权级继续执行。向一个不同特权级的非一直性代码段的转移将导致一般保护异常,除非使用了一个调用门或任务门(有关一致性和非一致性代码段的知识可以参考"直接调用或跳转到代码段")。不访问保护设施的系统工具以及某些异常类型(例如除出错、溢出)的处理过程可以存放在一致性代码段中。需要防止低特权级程序或过程访问的工具应该存放在非一致性代码段中。
所有数据段都是非一致性的,即意味着它们不能被低特权级的程序或过程访问。然而,与代码段不同,数据段可以被更高特权级的程序或过程访问,而无须使用特殊的访问门。
如果GDT或LDT中一个段描述符被存放在ROM中,那么若软件或处理器试图更新(写)在ROM中的描述符时,处理器就会进入一个无线循环。为了防止这个问题,需要存放在ROM中的所有描述符的已访问位应该预先设置成置位状态,同时,删除操作系统中任何试图修改ROM中段描述符的代码。
4.3.6系统描述符类型
当段描述符中的S标志(描述符类型)是复位状态(0)的话,那么该描述符是一个系统描述符。处理器能够识别一下一些类型的系统段描述符:
- 局部描述符表(LDT)的段描述符;
- 任务状态段(TSS)描述符;
- 调用门描述符;
- 中断门描述符;
- 陷阱门描述符;
- 任务门描述符。
这些描述符类型可以分为两大类:系统段描述符和门描述符。系统段描述符指向系统段(如LDT和TSS段),门描述符就是一个"门",对于调用、中断或陷阱门,其中含有代码段的选择符和段中程序入口点的指针;对于任务门,其中含有TSS的段选择符。
有关TSS状态段和任务门的使用方法将任务管理一节中进行说明,调用门的使用方法将放在保护一节中说明,中断和陷阱门的使用方法将在中断和异常处理一个节中给予说明。
4.4分页机制
分页机制是80X86内存管理机制的第二部分。它在分段机制的基础上完成虚拟(逻辑)地址到物理地址转换的过程。分段机制把逻辑地址转换成线性地址,而分则把线性地址转物理地址。分页可以用于任务以分段模型。处理器分页机制会把线性地址空间(段已映射到其中)划分页面,然后这些线性地址空间页面被映射到物理地址空间的页面上。分页机制几种页面级保护措施,可和分段机制保护机制合用或替代分段机制的保护措施。例如,在基于页面的基础上可以加强读/写保护。另外,在页面单元上,分页机子还提供了用户-超级用户两级保护。
我们通过设置控制寄存器CR0的PG位可以启用分页机制。如果PG=1,则启用分页操作,处理器会使用本节描述的机制将线性地址转换成物理地址。如果PG=0,则禁用分页机制,此时分段机制产生的线性地址被直接用作物理地址。
前面介绍的分段机制在各种可变长度的内存区域上操作。与分段机制不同,分页机制对固定大小的内存块(称为也页面)进行操作。分页机制把线性和物理空间都划分成页面。线性空间中的任何页面可以被到物理地址空间的任何页面上。
分页机制是如何线性和物理地址空间都划分成各个页面,并在这两个空间之间提供了任意映射。图形的箭头把线性把线性地址空间中的页面与物理地址空间中的页面对应起来。
80X86使用4K(2的12次方)字节固定大小的页面。每个页面均是是4KB,并且对齐于4地址边界处。这表示分页机制把(2的32次方)(4GB)的线性地址空间划分成(2的20次方)(1M=1048576)个页面。分页机制通过把线性地址空间中的页面重新定位到物理地址空间中进行操作。由于4K大小的页面作为一个单元进行映射,并且对于4K边界,因此线性地址的低12比特位可作为页面内偏移量直接作为物理地址的低12位。分页机制执行的重定位功能可以看作是线性地址的高20位转换到物理地址的高20位。
另外,线性到物理地址的转换功能被扩展成允许一个线性地址被标注为无效的,而非让产生一个物理地址。在两种情况下一个页面可以被标注为无效的;操作系统不支持的线性地址;对应在虚拟内存系统中的页面在磁盘上而非在物理内存中。在第一种情况下,产生无效程序必须被终止。在第二种情况下,该无效地址实际上是请求操作系统内存管理对应页面从磁盘上加载到物理内存种,以供程序访问。因为无效页面通常与虚拟存储系统相关,因此它们被为不存在的页面,并且由页表中称为存在(present)的属性来确定。
在保护模式中,80X86允许线性地址空间直接映射到大容量的物理内存(例如4GB的RAM)上,或者(使用分页)间接地映射到较小容量的物理内存和磁盘存储空间中。这后一种映射线性地址空间的方法被称为虚拟存储或需求页(Demand-paged)虚拟存储。
当使用分页时,处理器会把线性地址空间划分成固定大小的页面(长度4KB),这些页面可以映射到物理内存中和/或磁盘存储空间。当一个程序(或任务)引用内存中的逻辑地址时,处理器会把该逻辑地址一个线性地址,然后使用分页机制把该线性地址转换成对应的物理地址。
如果包含线性地址的页面当前不在物理内存中,处理器就会产生一个页错误异常。页错误异常的处理程序通常会让操作系统从磁盘中把相应页面加载到物理内存中(操作过程中可能还会物理内存中不同的页面写到磁盘上)。当页面加载到物理内存中之后,从异常过程的返回操作会使导致异常的指令被重新执行。处理器用于把线性地址转换成物理地址和用于产生页错误异常(若必要的话)的信息包含在存储于内存中的页目录和页表中。
分页与分段的最大的不同之处在于分页使用固定长度的页面。段的长度通常与存放在其中的代码或数据结构具有相同的长度。与段不同,页面有固定的长度。如果仅使用分段地址转换,那么存储在物理内存中个一个数据结构将包含其所有的部分。但如果使用了分页,那么一个数据结构就可以一部分存储于物理内存中,而另一部分保存在磁盘中。
正如前述,为了减少地址转换索要求得总线周期数量,最近访问得页目录和页表被存放在处理器得缓冲器中,该缓冲器被称为转换查找缓冲区TLB(Translation Lookaside Buffer)。TLB可以满足大多数读页目录和页表得请求而无需使用总线周期。只有当TLB不包含要求得页表项才会使用额外得总线周期从内存中读取页表项,这通常在一个页表项很长时间没有访问过时才会出现这种情况。、
4.4.1页表结构
分页转换功能由驻留在内存中得表来描述,该表称为页表(page table),存放在物理地址空间中。页表可以看作时简单得(2的20次方)物理地址数组。线性到物理地址功能的物理(基)地址。线性地址的低12位给出了页面中的偏移量,加上页面的基地址最终形成对应的物理地址。由于页面基地址对齐在4K边界上,因此页面基地址的低12位肯定是0。这意味着高20的页面基地址和12位偏移量连接组合在一起就能得到对应的物理地址。
页表中每个页表项大小为32位。由于只需要其中的20位来存放页面的物理基地址,因此剩下的12位可用于存放诸如页面是否存在等的属性信息。如果线性地址索引的页表项被标注为存在的,则表示该项即有效,我们可以从中取得页面的的物理地址。如果项中表明不存在,那么当访问对应物理页面时就会产生异常。
4.4.4.1两级页表结构
页表含有(2的20次方)(1M)个表项,而每项占用4字节。如果作为一个表来存放的话,它们最多将占用4MB的内存。因此为了减少内存占用量,80X86使用两级表。由此,高20位线性地址到物理地址的转换也被分成两步来进行,每步使用(转换)其中10个比特。
第一级表称为页目录(page directory)。它被存放在1页4K页面中,具有(2的10次方)(1K)个4字节长度的表项。这些表项指向对应的二级表。线性地址的最高10位(位31-32)用作一级表(页目录)中的索引值来选择(2的10次方)个二级表之一。
第二级表称为页表(page table),它的长度也是1个也页面,最多含有1K个4字节的表项。每个4字节表项含有相关页面的20位物理基地址。二级页表使用线性地址中间10位(位21--12)作为表项索引值,以获取含有页面20位物理基地址的表项。该20位页面物理基地址和线性地址中的低12位(页内偏移)组合在一起就得到了分页转换过程的输出值,即对应的最终物理地址。
图4-17示出了二级表的查找过程。其中CR3寄存器指定页目录表的基地址。线性地址的高10位用于索引这个页目录表,以获得指向相关第二级页表的指针。线性地址中间10位用于索引二级页表,以获得物理地址的高20位。线性地址的低12位直接作为物理地址低12位,从而组成一个完整的32位物理地址。
4.4.1.2不存在的页表
通过使用二级表结构,我们还没解决需要使用4MB内存来存放页表的问题。实际上,我们把问题搞得有些复杂了。因为我们需要另增一个页面来存放目录表。然而,二级表结构允许页表被分散在内存各个页面中,而不需要保存在连续得4MB内存块中。另外,并不需要为不存在得或线性地址空间 未使用部分分配二级页表。虽然目录表页面必须总是存在物理内存中,但是二级页表可以在需要时再分配。这使得页表结构得大小对应于实际使用得线性地址空间大小。
页目录表中每个表项也有一个存在(present)属性,类似于页表中的表项。页目录表项中的存在属性指明对应的二级页表是否存在。如果目录表项指明对应的二级页表存在,那么通过访问二级表,表查找过程第2步将同如上描述继续下去。如果存在位表明对应的二级表不存在,那么处理器就会产生一个异常来通知操作系统。页目录表项的存在属性使得操作系统可以根据实际使用的线性地址范围来分配二级页表页面。
目录表项中的存在位还可以用于再虚拟内存中存放二级页表。这意味着在任何时候只有部分二级页表需要存放在物理内存中,而其余的可保存在磁盘上。处理物理内存中页表对应的页目录项将被标注为存在,以表明可用它们进行分页转换。处于磁盘上的页表对应的页目录项被标注为不存在。由于二级页表不存在而引发的异常会通知操作系统把缺少的页表从磁盘上加载进物理内存。把页表存储在虚拟内存中减少了保存分页转换表所需要的物理内存量。
4.4.2页表项格式
页目录和页表的表项格式见图4-18所示。其中位31--12含有物理地址的高20位,用于定位物理地址空间中一个页面(也称为页帧)的物理基地址。表项的低12位含有页属性信息。我们已经知道存在属性,这里简要说明其余属性的功能和用途。
- P--位0是存在(Present)标志,用于指明表项对地址转换是否有效。P=1表示有效;P=0表示无效在页转换过程中,如果说涉及的页目录或页表的表项无效,则会导致一个异常。如果P=0,那么除表示表项无效外,其余比特位可供程序自由使用,见图4-18(b)所示。例如,操作系统可以使用这些位来保存已储在磁盘上的页面的序号。
- R/W--位1是读/写(Read/Write)标志。如果等于1,表示页面可以被读、写或执行。如果为0,表示页面只读或可执行。当处理器运行在超级用户特权级(级别0、1或2)上的程序访问。页目录项中的U/S位对其所映射的所有页面起作用。
- U/S--位2是用户/超级用户(User/Supervisor)标志。如果为1,那么运行在任何特权级上的程序都可以访问该页面。如果为0,那么页面只能被运行在超级用户特权级(0、1或2)上的程序访问。页目录项中的U/S位对其所映射的所有页面起作用。
- A--位是已访问(Accessed)标志。当处理器访问页表项映射的页面时,页表表项的这个标志就会被置为1.当处理器访问页目录表项映射到任何页面时,页目录表项的这个标志就会被置为1。处理器只负责设置该标志,操作系统可通过定期地复位该标志来统计页面的使用情况。
- D--位6是页面已被修改(Dirty)标志。当处理器对一个页面执行写操作时,就会设置对应页表表项的D标志。处理器并不会修改页目录项中的D标志。
- AVL--该字段保留专供程序使用。处理器不会修改这几位,以后的升级处理器也不会。
4.4.3虚拟存储
页目录和页表表项中的存在标志P为使用分页技术的虚拟存储提供了必要的支持。若线性地址空间中的页面存在于物理内存中,则对应表项中的标志P=1,并且该表项中含有相应物理地址。页面不在物理内存中的表现其标志P=0。如果程序访问物理内存不存在的页面,处理器就会产生一个缺页异常。此时操作系统就可以利用这个异常处理过程把缺少的页面从磁盘上调入物理内存中,并把相应物理地址存放在表项中。最后在返回成功徐重新执行引起异常的指令之前设置标志P=1。
已访问标志A和已修改标志D可以用于有效地实现虚拟存储技术。通过周期性地检查和复位所有A标志,操作系统能够确定哪些页面最近没有访问过。这些页面可以成为移除到磁盘上的候选者。假设当一页面从磁盘上读入内存时,其脏标志D=0,那么当页面再次被移除到磁盘上时,若D标志还是为0,则该页面就无需被写入磁盘中。若此时D=1,则说明页面内容已被修改过,于是就 必须将该页面写到磁盘上。
4.5保护
保护机制是可靠的多任务运行环境所必须的。它可用于保护各个任务免受相互之间的干扰。在软件开发的任何阶段都可以使用段级和页级保护来协助寻找和检测设计问题和错误。当程序对错误内存空间执行了一次非期望的引用,保护机制可以阻止这种操作并且报告此类事件。
保护机制可以被用于分段和分页机制。处理器寄存器的2个比特位定义了当前执行程序的特权级,成为当前特权级CPL(Current Privilege Level)。在分段和分页地址转换过程中,处理器对CPL进行验证。
通过设置控制寄存器CP0的PE标志(位0)可以让处理器工作在保护模式下,从而也就开启了分段保护机制。一旦进入保护模式,处理器中并不存在明确的控制标志来停止或启用保护机制。不过基于特权级的保护机制部分可以通过把所有段选择符和段描述符的特权级都设置为0级来隐含地关闭。这种处理方式可以在段之间禁止特权级保护壁垒,但是其他段长度和段类型检查等保护机制仍然起作用。
设置控制寄存器CRO的PG标志(位31)可以开启分页机制,同时页开启了分页保护机制。同样,处理器中也没有相关的标志用来在分页开启条件下禁止或开启页级保护机制。设置这两个标志可以使得每个页面都可以被任意读/写,因此实际上也就禁止了页级保护。
对于分段级保护机制,处理器使用段寄存器中选择符(RPL和CPL)和段描述符各个字段执行保护验证。对于分页机制,则主要利用页目录和页表项中的R/W和U/S标志来实现保护操作。
4.5.1段级保护
在保护模式下,80X86提供了段级和页级保护机制。这种保护机制根据特权级(4级段保护和2级页保护)提供了对某些段和页面的访问限制能力。例如,操作系统代码和数据存放在要比普通应用程序具有高特权级的段中。此后处理器的保护机制将限制应用程序只能按照受控制的和规定的方式访问操作系统的代码和数据。
当使用保护机制时,每个内存引用都将受到检查以验证内存引用符合各种保护要求。因为检查操作是与地址变换同时并行操作,所以处理器性能并没有受到影响。所进行的保护检查可分为以下几类:
- 段界限检查
- 段类型检查
- 特权级检查
- 可寻址范围限制
- 过程入口点限制
- 指令集限制
所有违反保护的操作都将导致产生一个异常。下面各节描述保护模式下的保护机制。
4.5.1.1段限长Limit检查
段描述符的段限长(或称段界限)字段用于防止程序或过程寻址到段外内存位置。段限长的有效值依赖于颗粒度G标志的设置状态。对于数据段,段限长还与标志E(扩展方向)和标志B(默认栈指针大小和/或上界限)有关。E标志是数据段类型的段描述符中类型字段的一个比特位。
当G标志清理时(字节颗粒度),有效的段长度是20位的段描述符中段限长字段Limit的值。在这种情况下,Limit的范围从0到0XFFFF(1M)。当G标志置位时(4KB页颗粒度),处理器把Limit字段的值乘上一个因子4K。在这种情况下,有效的Limit是从0xFFF到0XFFFFFFFF(4GB)。请注意,当设置了G标志时,段偏移(地址)的低12位不会与Limit进行对照检查。例如,当段限长Limit等于0时,偏移值0到0XFFF仍然是有效的。
除了下扩段以外的所有段类型,有效Limit的值是段中允许被访问的最后一个地址,它要比段长度小1个字节。任何超出段限长字段指定的有效地址范围都将导致一个一般保护异常。
对于下扩数据段,段限长具有同样的功能,但其含义不同。这里,段限长指定了段中最后一个不允许访问的地址,因此在设置了B标志的情况下,有效偏移范围是从(有效段偏移+1)到0xFFFFFFFF;当B清零时,有效偏移值范围是从(有效段偏移+1)到0XFFFF。当下扩段的段限长为0时,段会有最大长度。
除了对段限长进行检查,处理器也会检查描述符表的长度。GDTR、IDTR和LDTR寄存器中包含有16位的限长值,处理器用它来防止程序在描述符表的外面选择描述符。描述符表的限长值指明了表中最后一个有效字节。因为每个描述符是8字节长,因此含有N个描述符项的表应该具有限长值8N-1。
4.5.1.2段类型TYPE检查
除了应用程序代码和数据段有描述符以外,处理器还有系统段和门两种描述符类型。这些数据结构用于管理任务以及异常和中断。请注意,并非所有的描述符都定义一个段,门描述符中存放有指向一个过程入口点的指针。段描述符在两个含有类型信息,即描述符中的S标志和类型字段TYPE。处理器利用这些信息对于非法使用段或门导致的编程错误进行检测。
S标志用于指出一个描述符是系统类型的还是代码或数据类型的。TYPE字段另外提供了4个比特位用于定义代码、数据和系统描述符的各种类型。上一节的表给出了代码和数据描述符TYPE字段的编码;另一个表给出了系统描述符TYPE字段的编码。
当操作段选择符和段描述符时,处理器会随时检查类型信息。主要在以下两种情况下检查类型信息:
- 当一个描述符的选择符加载进一个段寄存器中。此时某些段寄存器只能存放特定类型的描述符,
- CS寄存器中只能被加载进一个可执行段的选择符;
- 不可读可执行段的选择符不能被加载进数据段寄存器中;
- 只有可写数据段的选择符才能被加载进SS寄存器中。
- 当指令访问一个段,而该段的描述符已经加载段寄存器中。指令只能使用某些预定义的方法来访问某些段。
- 任何指令不能写一个可执行段;
- 任何指令不能写一个可写位没有置位的数据段;
- 任何指令不能读一个可执行段,除非可执行段设置了可读标志。
4.5.1.3特权级
处理器的段保护机制可以识别4个特权级(或特权层),0级到3级。数值越大,特权越小。图4-19示除了这些特权级如何能被解释成保护环形成。环中心(保留给最高的代码、数据和堆栈)用于含有最紧要软件的段,通常用于操作系统核心部分。终极那两个环用于较为紧要的软件。只使用2个特权级的系统应该使用特权级0和3。
处理器利用特权级来防止运行在较低特权级的程序或任务访问具有较高特权级的一个段,除非是在受控的条件下。当处理器检测到一个违反特权级的操作时,它就会产生一个一般保护性异常。
为了在各个代码段和数据段之间进行特权级检测处理,处理器可以识别以下三种类型的特权级:
- 当前特权级CPL(Current Privilege Level)。CPL是当前正在执行程序或任务的特权级。它存放在CS和SS段寄存器的位0和位1中。通常,CPL等于当前代码段的特权级。当程序把控制转移到另一个具有不同特权级的代码段中时,处理器就会改变CPL。当访问一个一致性(conforming)代码段时,则处理器对CPL的设置有些不同。特权级值高于(即低特权级)或等于一致代码段DPL的任何段都可以访问一致代码段。并且当处理器访问一个特权级不同CPL的一致代码段时,CPL并不会被修改成一致代码段的DPL。
- 描述符特权级DPL(Descriptor Privilege Level)。DPL是一个段或门的特权级。它存放在段或门描述符的DPL字段中。在当前执行代码段试图访问一个段或门时,段或门的DPL会用来与CPL以及段或门选择符中的RPL(见下面说明)。作比较根据被访问的段或门的类型不同,DPL也有不同的含义:
- 数据段(Data Segment)。其DPL指出允许访问本数据段的程序或任务应具有的最大特权级数值。例如,如果数据段的特权级DPL是1,那么只有运行在CPL为0或1的程序可以访问这个段。
- 非一致代码段(Nonconforming code segment)(不使用调用门)。其DPL指出程序或任务访问该段必须具有的特权级。例如,如果某个非一直代码段的DPL是0,那么只有运行在CPL为0的程序能够访问这个段。
- 调用门(Call Gate)。其DPL指出访问调用门的当前执行程序或任务可处于的最大特权级数值。(这与数据段的访问呢规则相同。)
- 一致和非一致代码段(通过调用门访问)。其DPL指出允许访问本代码段的程序或任务应具有的最小特权级数值。例如,如果一致代码段的DPL是2,那么运行在CPL为0的程序就不能访问这个代码段。
- 任务状态段TSS。其DPL指出访问TSS的当前执行程序或任务可处于的最大特权级数值。(这与数据段的访问规则相同。)
- 请求特权级RPL(Request Privilege Level)。RPL是一种赋予段选择符的超越特权级,它存放在选择符的位0和位1中。处理器会同时检查RPL和CPL,以确定是否允许 访问一个段。即使程序或任务具有足够的特权级(CPL)来访问一个段,但是如果提供的RPL特权级不足的话访问也将被拒绝。也即如果段选择符的RPL其数值大于CPL,那么RPL将覆盖CPL(而使用RPL作为检查比较的特权级),反之也然。即始终取RPL和CPL中数值最大的特权级作为访问段时的比较对象。因此,RPL用来确保高特权级的代码不会代表应用程序去访问一个段,除非应用程序自己具有访问这个段的权限。
当段描述符的段选择符被加载进一个段寄存器时就会进行特权级检查操作,但用于数据访问的检查方式和那么用于在代码段之间进行程序控制转移的检查方式不一样。
4.5.2访问数据段时的特权级检查
为了访问数据段中的操作数,数据段的段选择符必须被加载进数据段寄存器(DS、ES、FS或GS)或堆栈段寄存器(SS)中。(可以使用指令MOV、POP、LDS、LES、LFS、LGS和LSS来加载段寄存器)。在把一个段选择符加载进段寄存器中之前,处理器会进行特权级检查,见图4-20所示。它会把当前运行程序或任务的CPL、段选择符的RPL和段描述符的DPL进行比较。只有当段的DPL数值大于或等于CPL和RPL两者时,处理器才会把选择符加载进段寄存器中。否则就会产生一个一般保护异常,并且不加载段选择符。
可知一个程序或任务可寻址的区域随着其CPL改变而变化。当CPL是0时,此时所有特权级上的数据段都可被访问;当CPL是1时,只有在特权级1到3的数据段可被访问;当CPL是3时,只有处于特权级3的数据段可被访问。
另外,有可能会把数据保存在代码段中。例如,当代码和数据是在ROM中时。因此,有些时候我们会需要访问代码段中的数据。此时可以使用以下方法来访问代码段中的数据:
- 把非一致可读代码段的选择符加载进一个数据段寄存器中。
- 把一致可读代码段的选择符加载进一个数据段寄存器中。
- 使用代码段覆盖前缀CS来读取一个选择符已经在CS寄存器中的可读代码段。
访问数据段的相同规则也适用方法1.方法2则是总有效的,因为一致代码段中特权级等同于CPL,而不管岱庙段的DPL。方法3也总是有效的的。因为CS寄存器选择的代码段的DPL与CPL相同。
当使用堆栈段选择符加载SS段寄存器时也会执行特权级检查。这里与堆栈相关的所有特权级必须与CPL匹配。也即,CPL、堆栈段选择符的RPL以及堆栈段描述符的DPL都必须相同。如果RPL或DPL与CPL不同,处理器就会产生一个一般保护性异常。
4.5.3代码段之间转移控制时的特权级检查
对于将程序控制权从一个代码段转移到另一个代码段,目标代码段的段选择符必须加载进代码段寄存器(CS)中。作为这个加载的一部分,处理器会检测目标代码段的段描述符并执行各种限长、类型和特权级检查。如果这些检查都通过了,则目标代码段选择符就会加载进CS寄存器,于是程序的控制权就被转移到新代码段中,程序将从EIP寄存器指向的指令处开始执行。
程序的控制转移使用指令JMP、RET、INT和IRET以及异常和中断机制来实现。异常和中断是一些特殊实现,将在后面描述,本节主要说明JMP、CALL、和RET指令的实现方法。JMP或CALL指令可以利用一下四种方法之一来引用另外一个代码段:
- 目标操作数含有目标代码段的段选择符;
- 目标操作数指向一个调用门描述符,而该描述符中含有目标代码段的选择符;
- 目标操作数指向一个TSS,而该TSS中含有目标代码段的选择符;
- 目标操作数指向一个任务门,该任务们指向一个TSS,而该TSS中含有目标代码段的选择符;
4.5.3.1直接调用或跳转到代码段
JMP、CALL和RET指令的近转移形式只是当前代码段中执行程序控制转移,因此不会执行特权级检查。JMP、CALL或RET指令的远转移形式会把控制转移到另一个代码段中,因此处理器一定会之醒特权级检查。
当不通过调用门把程序控制权转移到另一个代码段时,处理器会验证4种特权级和类型信息,
- 当前特权级CPL。(这里,CPL是执行调用的代码段的特权级,即含有执行调用或跳转程序的代码段的CPL。)
- 含有被调用过程的目的代码段段描述符中的描述符特权级DPL。
- 目的代码段的段选择符中的请求特权级RPL。
- 目的代码段描述符中的一致性标志C。它确定了一代码段是非一致代码段还是一致代码段。
处理器检查CPL、RPL和DPL的规则依赖于一致标志C的设置状态。当访问非一致代码段时(C=0),调用者(程序)的CPL必须等于目的代码段的DPL,否则将会产生一般保护异常。指向非一致代码段的段选择符的RPL对检查所起的作用有限。RPL在数值上必须小于或等于调用者的CPL才能使得控制转移成功完成。当非一致代码段的段选择符被加载进CS寄存器中时,特权级字段不会改变,即它仍然是调用者的CPL。即使段选择符的RPL与CPL不同,这也是正确的。
当访问一致代码段时(C=1),调用者的CPL可以在数值上大于或等于目的代码段的DPL。仅当CPL<DPL时,处理器才会产生一般保护异常。对于访问一致代码段,处理器忽略对RPL的检查。对于访问一致代码段,DPL表示调用者对代码段进行成功调用可以处于的最低数值特权级。
当程序控制被转移到一个一致代码段中,CPL并不改变,即使目的代码段的DPL在数值上小于CPL。这是CPL与可能与当前代码段DPL不相同的唯一一种情况。同样,由于CPLL没有改变,因此堆栈也不会切换。
大多数代码段都是非一致代码段。对于这些段,程序的控制权只能转移到具有相同特权级的代码段中,除非转移是通过一个调用门进行,见下面说明。
4.5.3.2门描述符
为了对具有不同特权级的代码段提供受控的访问,处理器提供了称为门描述符的特殊描述符集。共有4种门描述符:
- 调用门(Call Gate),类型TYPE=12;
- 陷阱们(Trap Gate),类型TYPE=15;
- 中断门(Interrupt Gate),类型TYPE=14;
- 任务门(Task Gate),类型TYPE=5。
任务门用于任务切换,将在后面任务管理一节说明。陷阱门和中断门是调用门的特殊类,专门用于调用异常和中断的处理程序。
调用门用于在不同特权级之间实现受控的程序控制转移。它们通常仅用于使用特权级保护机制的操作系统中。图4-22给出了调用门描述符的格式。调用门描述符可以存放在GDT或LDT中,但是不能放在中断描述符表IDT中。一个调用门主要具有一下几个功能: - 指定要访问的代码段;
- 在指定代码段中定义过程(程序)的一个入口点;
- 指定访问过程的调用者需具备的特权级;
- 若会发生堆栈切换,它会指定在堆栈之间需要复制的可选参数个数;
- 指明调用门描述符是否有效。
调用门中的段选择字段指定要访问的代码段。偏移值字段指定段中入口点。这个入口点通常是指定过程的第一条指令。DPL字段指定调用门的特权级,从而指定通过调用门访问特定过程所要求的特权级。标志P指明调用门描述符是否有效。参数个数字段(Param Count)指明在发生堆栈切换时从调用者堆栈复制到新堆栈中的参数个数。Liunx内核中并没有用到调用门。
4.5.3.3通过调用门访问代码段
为了访问调用门,我们需要为CALL或JMP指令的操作数提供一个远指针。该指针中的段选择符用于指定调用门,而指针的偏移值虽然需要但CPU并不会用它。该偏移值可以设置为任意值。见图4-23所示。
当处理器访问调用门时,它会使用调用门中的段选择符来定位目的代码段的段描述符。然后CPU会把代码段描述符的基地址与调用门中的偏移值进行组合,形成代码段中指定程序入口点的线性地址。
通过调用门进行程序控制转移时,CPU会对4中不同的特权级进行检查,以确定控制转移的有效性,见图4-24所示。
- 当前特权级CPL
- 调用门选择符中的请求特权级RPL;
- 调用门描述符中的描述符特权级DPL;
- 目的代码段描述符中的DPL;
另外,目的代码段描述符中的一致性标志C也将受到检查。
使用CALL指令和JMP指令分别具有不同的特权级检测规则,见表4-5所示。调用门描述符的DPL字段指明了调用程序能够访问调用门的数值最大的特权级(最小特权级),即为了访问调用门,调用者程序的特权级CPL必须小于或等于调用门的DPL。调用段选择符的RPL也需同调用这的CPL遵守同样的规则,即RPL也必须小于或等于调用门的DPL。
如果调用这与调用门之间的特权级检查成功通过,CPU就会接着把调用者的CPL与代码段描述符的DPL进行比较检查。在这方面,CALL指令和JMP指令的检查规则就不同了。只有CALL指令可以通过调用门把程序控制转移到特权级更高的非一致性代码段中,即可以转移到DPL小于CPL的非一致性代码段中去执行。而JMP指令只能通过调用门把控制转移到DPL等于CPL的非一致性代码段中。但CALL指令和JMP指令都可以把控制转移到更高特权级的一致性代码段中,即转移到DPL小于或等于CPL的一致性代码段中。
如果一个调用把控制转移到了更高特权级的非一致性代码段中,那么CPL就会被设置为目的代码段的DPL值,并会引起堆栈切换。但是如果一个调用或跳转把控制转移到更高级别的一致性代码上,那么CPL并不会改变,并且也不会引起堆栈切换。
调用门可以让一个代码段中的过程被不同特权级的程序访问。例如,位于一个代码段中的操作系统代码可能含有操作系统自身和应用软件都允许访问的代码(比如处理字符I/O的代码)。因此可以为这些过程设置一个所有特权级代码都能访问的调用门。另外可以专门为仅用于操作系统的代码设置一些更高特权级的调用门。
4.5.3.4堆栈切换
每当调用门用于把程序控制转移到一个更高级别的非一致性代码段时,CPU会自动切换到目的代码段特权级的堆栈去。执行栈切换操作的目的是为了防止高特权级程序由于栈空间不足而引起崩溃,同时也为了防止低特权级程序通过共享的堆栈有意或无意地干扰高特权级的程序。
每个任务必须定义最多4个栈。一个用于运行在特权级3的应用程序代码,其他分别用于用到的特权级2、1和0。如果一个系统中只使用了3和0两个特权级,那么每个任务就只需设置两个栈。每个栈都位于不同的段中,并且使用段选择符和段中偏移值指定。
当特权级3的程序在执行时,特权级3的堆栈的段选择符和栈指针会被分别存放在SS和ESP中,并且在发生堆栈切换时被保存在被调用过程的堆栈上。
特权级0、1和2的堆栈的初始指针值都存放在当前运行任务的TSS段中。TSS段中这些指针都是只读值。在任务运行时CPU并不会修改它们。当调用更高特权级程序时,CPU才用它们来建立新堆栈。当从调用过程返回时,相应栈就不存在了。下一次再调用该过程时,就又会再次使用TSS中的初始指针值建立一个新栈。
操作系统需要负责为所有用到的特权级建立堆栈和堆栈段描述符,并且在任务的TSS中设置初始指针值。每个栈必须可读可写,并且具有足够的空间来存放以下一些信息:
- 调用过程的SS、ESP、CS和EIP寄存器内容;
- 被调用过程的参数和临时变量所需使用的空间。
- 当隐含调用一个异常或中断过程时标志寄存器EFLAGS和出错码使用的空间。
由于一个过程可调用其它过程,因此每个栈必须有足够大的空间来容纳多帧(多套)上述信息。
当通过调用门执行一个过程调用而造成特权级改变时,CPU就会执行以下步骤切换堆栈并开始在新的特权级上执行被调用过程(见图4-25所示): - 使用目的代码段的DPL(即新的CPL)从TSS中选择新栈的指针。从当前TSS中读取新栈的段选择符和栈指针。在读取栈段选择符、栈指针或栈段描述符过程中,任何违反段界限的错误都将导致产生一个无效TSS异常;
- 检查栈段描述符特权级和类型是否有效,若无效者同样产生一个无效TSS异常。
- 临时保存SS和ESP寄存器的当前值,把新栈的段选择符和栈指针加载到SS和ESP中。然后把临时保存的SS和ESP内容压入新栈中。
- 把调用门描述符中指定参数个数的参数从调用过程栈复制到新栈中。调用门中参数个数值最大为31,如果个数为0,则表示无参数,不需复制。
- 把返回指令指针(即当前CS和EIP内容)压入新栈。把新(目的)代码段选择符加载到CS中,同时把调用门中偏移值(新指令指针)加载到EIP中。最后开始执行被调用过程。
4.5.3.5从被调用过程返回
指令RET用于执行近返回(near return)、同特权级远返回(far return)和不同特权级的远返回。该指令用于从使用CALL指令调用的过程中返回。近返回仅在当前代码段中转移程序控制权,因此CPU仅进行界限检查。对于相同特权级的远返回,CPU同时从堆栈中弹出返回代码段的选择符和返回指令指针。由于通常情况下这两个指针是CALL指令压入栈中的,因此它们因该是有效的。但是CPU还是会执行特权级检查以应对当前过程可能修改指针值或堆栈出现问题时的情况。
会发生特权级改变的远返回仅允许返回到低特权级程序中,即返回到的代码段DPL在数值上要大于CPL。CPU会使用CS寄存器中选择符的RPL字段来确定是否要求返回到低特权级。如果RPL的数值要比CPL大,就会执行特权级之间的返回操作。当执行远返回到一个调用过程时,CPU会执行以下步骤:
1.检查保存的CS寄存器中RPL字段值,以确定在返回时特权级是否需要改变。
2.弹出并使用被调用过程堆栈上的值加载CS和EIP寄存器。在此过程中会对代码段描述符和代码段选择符的RPL进行特权级与类型检查。
3.如果RET指令包含一个参数个数操作数并且返回操作会改变特权级,那么就在弹出栈中CS和EIP值之后把参数个数值加到ESP寄存器值中,以跳过(丢弃)被调用者栈上的参数。此时ESP寄存器指向原来保存的调用者堆栈的指针SS和ESP。
4.把保存的SS和ESP值加载到SS和ESP寄存器中,从而切换回调用者的堆栈。而此时被调用者堆栈的SS和ESP值被抛弃。
5.如果RET指令包含一个参数个数操作数,则把参数个数值加到ESP寄存器值中,以跳过(丢弃)调用者栈上的参数。
6.检查段寄存器DS、ES、FS和GS的内容。如果其中有指向DPL小于新CPL的段(一致代码段除外),那么CPU就会用NULL选择符加载加载这个段寄存器。
4.5.4页级保护
页目录和页表表项中的读写标志R/W和用户/超级用户标志U/S提供了分段机制保护属性的一个子集。分页机制只识别两级权限。特权级0、1和2被归类为超级用户级,而特权级3被称为普通用户级。普通用户级的页面可以被标志成只读/可执行或可读/可写/可执行。超级用户级的页面对于超级用户来讲总是可读/可写/可执行的,但普通用户不可访问,见表4-6所示。对于分段机制,在最外层用户执行的程序只能访问用户级的页面,但是在任何超级用户层(0、1、2)执行的程序不仅可以访问用户层的页面,也可以访问呢超级用户层的页面。与分段机制不同的是,在内层超级用户级执行的程序对任何页面都具有可读/可写/可执行权限,包括那些在用户级标注为只读/可执行的页面。
正如在整个80X86地址转换中分页机制是在分段机制之后实施一样,页级保护也是在分段机制提供的保护措施之后发挥作用。首先,所有段级保护被检测和测试。如果通过检查,就会再进行页级保护检查。例如,仅当一个字节位于级别3执行的程序可访问的段中,并且处于标志为用户级页面中时,这个内存中的字节才可被级别3上的程序访问。仅当分段和分页都允许写时,才能对页面执行写操作。如果一个段是读/写类型的段,但是地址对应的相应页面被标注为只读/可执行,那么还是不能对页面执行写操作。如果段的类型是只读/可执行,那么不管对应页面被赋予任何保护属性,页面始终是没有写权限的。可见分段和分页的保护机制就像电子线路中的串行线路,其中那个开关没有合上线路都不会通。
类似地,一个页面的保护属性由页目录和页表中表项的"串行"或"与操作"构成,见表4-7所示。页表表项中的U/S和R/W标志应用于该表项映射的单个页面。页目录项中的U/S和R/W标志则对该目录项所映射的所有页面起作用。页目录和页表的组合保护属性由两者属性的"与"(AND)操作构成,因此保护措施非常严格。
4.5.4.1修改页表项的软件问题
本节提供了一些有关操作系统软件修改页表项内容所需遵守的规则。分页转换缓冲要求所有系统都必须遵守这些规则。为了避免每次内存应用都要访问驻留内存的页表,从而加快速度,最近使用的线性到物理地址的转换信息被保存再处理器内的页转换高速缓冲中。处理器在访问内存中的页目录和页表。页转换高速缓冲作用类似于前面描述的加速段转换的段寄存器的影子描述寄存器。页转换高速缓冲的另一个术语称为转换查找缓冲TLB(Translation Lookaside Buffer)。
80X86处理器并没有维护页转换高速缓冲和页表中数据的相关性,但是需要操作系统软件来确保它们一致。即处理器并不知道什么时候页表被软件修改过了。因此操作系统必须在改动过页表之刷新高速缓冲以确保两者一致。通过简单地重新加载寄存器CR3,我们就可以完成对高速缓冲的刷新操作。
有一种特殊情况,在这种情况下来修改页表项不需要刷新页转换高速缓冲。也即当不存在页面的表项被修改时,即使是把P标志从0改成1来标注表项对页转换有效,也不需要刷新高速缓冲。因为无效的表项不会被存入高速缓冲中。所以在把一个页面从磁盘调入内存以使页面存在时,我们不需要刷新页转换高速缓冲。
4.5.5组合页级和段级保护
当启用了分页机制,CPU会首先执行段级保护,然后再处理页级保护。如果CPU在任何一级检查到一个保护违规错误,则会放弃内存访问并产生一个异常。如果是段基址产生的异常,那么就不会再产生一个页异常。
页级保护不饿能替代或忽略段级保护。例如,若一个代码段被设定为不可写,那么代码段被分页后,即使页面的R/W标志被设置成可读可写也不会让页面可写。此时段级保护检查会阻止任何对页面的写操作企图。页级保护可被用来增强段级保护。例如,如果一个可读可写数据段被分页,那么页级保护机制可用来对个别页面进行写保护。
4.6中断和异常处理
中断(Interrupt)和异常(Exception)是指明系统、处理器或当前执行程序(或任务)的某处出现一个事件,该事件需要处理器进行处理。通常,这种事件会导致执行控制被强迫从当前运行程序转移到被称为中断处理程序(interrupt handler)或异常处理程序(exception handler)的特殊软件函数或任务中。处理器响应中断或异常所采取的行动被称为中断/异常服务(处理)。
通常,中断发生在程序执行的随机时刻,以响应硬件发出的信号。系统硬件使用中断来处理外部事件,例如要求为外部设备提供服务。当然,软件也能通过执行INT n 指令产生中断。
异常发生在处理器执行一条指令时,检测到一个出错条件时发生,例如被0出错条件。处理器可以检测到各种出错条件,包括违反保护机制、页错误一级机器内部错误。
对应用程序和操作系统来说,80X86的中断和异常处理机制可以透明地处理发生的中断和异常事件。当收到一个中断或检测到一个异常时,处理器会自动地把当前正在执行的程序或任务挂起,并开始运行中断或异常处理器程序。当处理程序执行完毕,处理器就会恢复并继续执行被中断的程序或任务。被中断程序的恢复过程并不会失去程序执行的连贯性,除非从异常中恢复是不可能的或者中断导致当前运行程序被终止。
4.6.1异常和中断向量
为了有助于处理异常和中断,每个需要被处理器进行特殊处理的处理器定义的异常和中断条件都被赋予了一个标识号,称为向量(vector)。处理器把赋予异常或中断的向量用作中断描述符表IDT(Interrupt Descriptor Table)中的一个索引号,来定位一个异常或中断的处理程序入口点位置。
允许的向量号范围是0到255。其中0到31保留用作80X86处理器定义的异常和中断,不过目前该范围内的向量号并非每个都已定义了功能,未定义功能的向量号将留作今后使用。
范围在32到255的向量号用于用户定义的中断。这些中断通常用于外部I/O设备,使得这些设备可以通过外部硬件中断机制向处理器发送中断。表4-8中给出了80X86定义的异常和NMI中断分配的向量。对于每个异常,该表给出了异常类型以及是否会产生一个错误码并保存在堆栈上。同时还给出了每个预先定义好的异常和NMI中断源。
4.6.2中断源和异常源
4.6.2.1中断源
处理器从两种地方接收中断:
- 外部(硬件产生)的中断;
- 软件产生的中断。
外部中断通过处理器芯片上两个引脚(INTR和NMI)接受。当引脚INTR接受到外部发生的中断信号时,处理器就会从系统总线上读取外部中断控制器(例如8259A)提供的中断向量号。当引脚NMI接收到信号时,就产生一个非屏蔽中断。它使用固定的中断向量号2。任何通过处理器INTR引脚接受的外部中断都被称为可屏蔽硬件中断,包括中断向量号0到255.标志寄存器EFLAGS中的IF标志可用来屏蔽所有这些硬件中断。
通过在指令操作数中提供中断向量号,INT n指令可用于从软件中产生中断。例如,指令INT0X80会执行Linux的系统中断调用中断0x80。向量0到255中的任何一个都可以用作INT指令的中断号。然而,如果使用了处理器预先定义的NMI向量,那么处理器对它的响应将与普通方式产生的该NMI中断不同。如果NMI的向量号2用于该INT指令,就会调用NMI的中断处理器程序,但是此时并不会激活处理器的NMI处理硬件。
注意,EFLAGS中的IF标志不能屏蔽使用INT指令从软件中产生的中断。
4.6.2.2异常源
处理器接受的异常页有两个来源:
- 处理器检测到的程序错误异常;
- 软件产生的异常。
在应用程序或操作系统执行期间,如果处理器检测到程序错误,就会 产生一个或多个异常。80X86处理器为其检测到的每个异常定义了一个向量。异常可以被细分为故障(faults)、陷阱(traps)和中止(aborts),见后面说明。
指令INTO、INT3和BOUND指令可以用来从软件中产生异常。这些指令可对指令流中指定点执行的特殊异常条件进行检查。例如,INT3指令会产生一个断点异常。
INTn指令可用于在软件中模拟指定的异常,但有一个限制。如果INT指令中的操作数n是80X86异常的向量号之一,那么处理器将为该向量号产生一个中断,该中断就会去执行与该向量有关的异常处理程序。但是,因为这实际上是一个 中断,因此处理器并不会把一个错误号压入堆栈,即使硬件产生的该向量相关的中断通常会产生一个错误码。对于那些会产生错误码的异常,异常的处理程序会试图从堆栈上弹出错误码。因此,如果使用INT指令来模拟产生一个异常,处理程序则会把EIP(正好处于缺少的错误码位置处)弹出堆栈,从而会造成返回位置错误。
4.6.3异常分类
根据异常被报告的方式以及导致异常的指令是否能够被重新执行,异常可被细分成故障(Fault)、陷阱(Trap)、和中止(Abort)
- Fault是一种通常可以被纠正的异常,并且一旦被纠正程序就可以继续运行。当出现一个Fault,处理器就会把机器状态恢复到产生Fault的指令之前的状态。此时异常处理程序的返回地址指令产生Fault的指令,而不是其后面一条指令。因此在返回后产生Fault的指令将被重新执行。
- Trap是一个引起陷阱的指令被执行后立刻会报告的异常。Trap也能够让程序或任务连贯地执行。Trap处理程序的返回地址指向引起陷阱指令的随后一条指令,因此在返回后会执行下一条指令。
- Abort是一种不会总是报告导致异常的指令的精确位置的异常,并且不允许导致异常的程序继续执行。Abort用于报告严重错误,例如硬件错误以及系统表中存在不一致性或非法值。
4.6.4程序或任务的重新执行
为了让程序或任务在一个异常或中断处理完之后能重新恢复执行,除了中止(Abort)之外的所有异常都能报告精确的指令位置,并且所有中断保证是在指令边界上发生。
对于故障类异常,处理器产生异常时保存的返回指针指向出错指令。因为,当程序或任务在故障处理程序返回后重新开始执行时,原出错指令会被重新执行。重新执行引发出错的指令通常用于处理访问指令操作数受阻的情况。Fault最常见的一个例子是页面故障(Page-fault)异常。当程序引用不在内存中页面上的一个操作数时就会出现这种异常。当页故障异常发生时,异常处理程序可以把该页面加载到内存中并通过重新执行出错指令来恢复程序执行。为了确保重新执行对于当前执行程序具有透明性,处理器会保存必要的寄存器和堆栈指针信息,以使得自己能够返回到执行出错指令之前的状态。
对于陷阱Trap类异常,处理器产生异常时保存的返回指针指向引起陷阱操作的后一条指令。如果在一条执行控制转移的指令执行期间检测到一个Trap,则返回指令指针会反映出控制的转移情况。例如,如果在执行JMP指令时检测到一个Trap异常,那么返回指令指针会指向JMP指令的目标位置,而非指向JMP指令随后的一条指令。
中止Abort类异常不支持可靠地重新执行程序或任务。中止异常的处理程序通常用来收集异常发生时有关处理器状态的诊断信息,并且尽可能恰当地关闭程序和系统。
中断会严格地支持被中断程序的重新执行而不会丢失任何连贯性。中断所保存的返回指令指针指向处理器获取中断时将要执行的下一条指令边界处。如果刚执行的指令有一个重复前缀,则中断会在当前重复结束并且寄存器已为下一次重复操作设置好时发生。
4.6.5开启和禁止中断
标志寄存器EFLAGS的中断允许标志IF(Interrupt enable Flag)能够禁止为处理器INTR引脚上收到的可屏蔽硬件中断提供服务。当IF=0时,处理器禁止发送到INTR引脚的中断;当IF=1时,则发送到INTR引脚的中断信号会被处理器进行处理。
IF标志并不影响发送到NMI引脚的非屏蔽中断,也不影响处理器产生的异常。如同EFLAGS中的其他标志一样,处理器在响应硬件复位操作时会清楚IF标志(IF=0)。
IF标志可以使用指令ST1和CL1来设置或清楚。只有当程序的CPL<=IOPL时才可执行这两条指令,否则将引发一般保护性异常。IF标志也会受一下操作影响:
- PUSHF指令会把EFLAGS内容存入堆栈中,并且可以在那里被修改。而POPF指令可用于把已修改过的标志内容放入EFLAGS寄存器中。
- 任务切换、POPF和IRET指令会加载EFLAGS寄存器。因此,它们可用来修改IF标志。
- 当通过中断门处理一个中断时,IF标志会被自动清楚(复位),从而会禁止可屏蔽硬件中断。但如果是通过陷阱门来处理一个中断,则IF标志不会被复位。
4.6.6异常和中断的优先级
如果在一条指令边界有多个异常或中断等待处理时,处理器会按规定的次序对它们进行处理。表4-9给出了异常和中断源类的优先级。处理器会首先处理最高优先级类中的异常或中断。低优先级的异常会被丢弃,而低优先级的中断则会保持等待。当中断处理程序返回到产生异常和/或中断的程序或任务时,被丢弃的异常会重新发生。
4.6.7中断描述符
中断描述符表IDT(Interrupt Descriptor Table)将每个异常或中断向量分别与它们的处理过程联系起来。与GDT和LDT类似,IDT也是由8字节长描述符组成的一个数组。与GDT不同的是,表中第1项可以包含描述符。为了构成IDT表中的一个索引值,处理器把异常或中断的向量号*8。因为最多只有256个中断或异常向量,所以IDT无需包含多于256个描述符。IDT中可以含有少于256个描述符,因为只有可能发生的异常或中断才需要描述符。不过IDT中所有空描述符应该设置其存在位(标志)为0。
IDT表可以驻留在线性空间的任何地方,处理器使用IDTR寄存器来定位IDT表的位置。这个寄存器中含有IDT表32位的基地址和16位的长度(限长)值,见图4-26所示。IDT表基地址应该对齐在8字节边界上以提高处理器的访问效率。限长值是以字节为单位的IDT表的长度。
指令LIDT和SIDT指令分别用于加载和保存IDTR寄存器的内容。LIDT指令把内存中的限长值和基地址操作数加载到IDTR寄存器中。该指令仅能由当前特权级CPL是0的代码执行,通常被创建IDT时的操作系统初始化代码中。SIDT指令用于把IDTR中的基地址和限长内容复制到内存中。该指令可在任何特权级上执行。
如果中断或异常向量引用的描述符超过了IDT的界限,处理器会产生一个一般保护性异常。
4.6.8IDT描述符
IDT表中可以存放三种类型的门描述符:
- 中断门(Interrupt gate)描述符
- 陷阱门(Trap gate)描述符
- 任务门(Task gate)描述符
图4-27给出了这三种门描述符的格式。中断门和陷阱门含有一个长指针(即段选择符和偏移值),处理器使用这个长指针把程序执行权转移到代码段中异常或中断的处理器过程中。这两个段的主要区别在于处理器操作EFLAGS寄存器IF标志上。IDT中任务门描述符的格式与GDT和LDT中任务门的格式相同。任务门描述符中含有一个任务TSS段的选择符,该任务用于处理异常和或中断。
4.6.9异常和中断处理
处理器对异常和中断处理过程的调用操作方法与使用CALL指令调用程序过程和任务的方法类似。当响应一个异常或中断时,处理器使用异常或中断的向量作为IDT表中的索引。如果索引值指向中断门或陷阱门,则处理器使用与CALL指令操作调用门类似的方法调用异常或中断处理器过程,见图4-28所示。门中的段选择符指向GDT或当前LDT的可执行代码段描述符。门描述符中对的偏移字段指向异常或中断处理过程的开始处。
当处理器执行异常或中断处理过程调用时会进行以下操作:
- 如果处理过程将在高特权级(例如0级)上执行时就会发生堆栈切换操作。堆栈切换过程如下:
处理器从当前执行任务的TSS段中得到中断或异常处理过程使用的堆栈的段选择符和栈指针(例如tss.ss0、tss.esp0)。然后处理器会把中断程序(或任务)的栈选择符和栈指针压入新栈中,见图4-29所示。
接着处理器会把EFLAGS、CS和EIP寄存器的当前值也压入新栈中。
如果异常会产生一个错误号,那么该错误号也会被最后压入新栈中。
如果处理过程将在被中断任务同一个特权级上运行,那么:
处理器把EFLAGS、CS和EIP寄存器的当前值保存在当前堆栈上。
如果异常产生一个错误号,那么该错误号也会被最后压入新栈中。
为了从中断处理过程中返回,处理过程必须使用IRET指令。IRET指令与RET指令类似,但IRET还会把保存的寄存器内容恢复到EFLAGS中。不过只有当CPL是0时才会恢复EFLAGS中的IOPL字段,并且只有当CPL<=IOPL时,IF标志才会被改变。如果当调用中断处理过程时发生了堆栈切换,那么在返回时IRET指令会切换到原来的堆栈。 - 异常和中断处理过程的保护
异常和中断处理过程的特权级保护机制与通过调用门调用普通过程类似。处理器不允许把控制转移到比CPL更低特权级代码段的中断处理过程中,否则将产生一个一般保护性异常。另外,中断和异常的保护机制在以下方面与一般调用门过程不同: - 因为中断和异常向量没有RPL,因此在隐式调用异常和中断处理过程时不会检查RPL。
- 只有当一个异常或中断是利用使用INT n、INT 3或INTO指令产生时,处理器才会检查中断或陷阱门中的DPL。此时CPL必须小于等于门的DPL。这个限制可以防止运行在特权级3的应用程序使用软件中断访问重要的异常处理过程,例如页错误处理过程,假设这些处理过程已被存放在更高特权级的代码段中。对于硬件产生的中断和处理器检测到的异常,处理器会忽略中断门和陷阱门的DPL。
因为异常和中断通常不会定期发生,因此这些有关特权级的规则有效地增强了异常和中断处理过程额能够运行的特权级限制。我们可以利用以下技术之一来避免违反特权级保护: - 异常或中断处理程序可以存放在一个一致代码段中。这个技术可以用于只需访问堆栈上数据的处理过程(例如,除出错异常)。如果处理程序需要数据段中的数据,那么特权级3必须能够访问这个数据段。但这样一来就没有保护可言了。
- 处理过程可以放在具有特权级0的非一致代码段中。这种处理过程总是可以执行的,而不管中断程序或任务的当前特权级CPL。
2.异常或中断处理过程的标志使用方式
当通过中断门或陷阱门访问一个异常或中断处理过程时,处理器会把EFLAGS寄存器保存到堆栈上之后清楚EFLAGS中的TF标志。清楚TF标志可以防止指令跟踪影响中断响应。而随后的IRET指令会用堆栈上的内容恢复EFLAGS的原TF标志。
中断门与陷阱门唯一的区别在于处理器操作EFLAGS寄存器IF标志的方法。当通过中断门访问一个异常或中断处理过程时,处理器会复位IF标志其他中断干扰当前中断处理过程。随后的IRET指令则会用保存在堆栈上的内容恢复EFLAGS寄存器的IF标志。而通过陷阱门访问处理过程并不会影响IF标志。
3.执行中断处理过程的任务
当通过IDT表中任务门访问异常或中断处理过程中时,就会导致任务切换。从而可以在一个专用任务中执行中断或异常处理过程。IDT表中的任务门引用GDT中的TSS描述符。切换到处理过程任务的方法与普通任务切换一样。
4.6.10中断处理任务
当通过IDT中任务门来访问异常或中断处理过程时就会导致任务切换。使用单独的任务来处理异常或中断有如下好处:
- 被中断程序或任务的完整上下文会被自动保存;
- 在处理异常或中断时,新的TSS可以允许处理过程使用新特权级0的堆栈。在当前特权级0的堆栈已毁坏时如果发生了一个异常或中断,那么在为中断过程提供一个新特权级0的堆栈条件下,通过任务门访问中断处理过程能够防止系统崩溃;
- 通过使用单独的LDT给中断或异常处理任务独立的地址空间,可以把它与其他任务隔离开来。
使用独立任务处理异常或终端的不足之处是:在任务切换时必须对大量机器状态进行保存,使得它比使用中断门的响应速度要慢,导致中断延时增加。
IDT中的任务门会引用GDT的TSS描述符,图4-30,切换到句柄任务的过程与普通任务切换过程相同。到被中断任务的反向链接会保存在句柄任务TSS的前以任务链接字段中。如果一个异常会产生一个出错码,则该出错码会被复制到新任务堆栈上。
当异常或中断句柄任务用于操作系统中时,实际上有两种分派调度任务的机制:操作系统软件调度和处理器中断机制的硬件调度。使用软件调度方法时需要考虑到中断开启时采用中断处理任务。
4.6.11错误码
当异常条件与一个特定的段相关时,处理器会把一个错误码压入异常处理过程的堆栈上。出错码的格式见图4-31所示。错误码很像一个段选择符,但是最低3比特不是T1和RPL字段,而是以下3个标志:
- 位0是外部事件EXT(Externale event)标志。当该位置位时,表示错误码的索引部分指向IDT中的一个门描述符。当该位复位时,表示索引部分指向GDT或LDT的一个段描述符。
- 位1是描述符位置IDT(Descriptor location)标志。当该位置位时,表示错误码的索引部分指向IDT中的一个门描述符。当该位复位时,表示索引部分指向GDT或LDT中的一个段描述符。
- 位2是GDT/LDT表选择标志TI。只有当位1的IDT=0才有用。当该TI=1时,表示错误码的索引部分指向LDT中的一个描述符。当TI=0时,说明错误码中的索引部分指向GDT表中的一个描述符。
段选择索引字段提供了错误码引用的IDT、GDT或者当前LDT中段或门描述符的索引值。在某些情况下错误码时是空的(即低16位全0)。空错误码表示错误不是由于引用某个特定段造成,或者是在操作中引用了一个空段描述符。
页故障(Page-fault)异常的错误码格式与上面的不同,见图4-32所示。只有最低3个比特位有用,它们的名称与页表项中的最后三位相同(U/S、W/R、P)。含义和作用分别是: - 位0(P),异常是由于页面不存在或违反访问特权而引发。P=0,表示页不存在;P=1表示违反页级保护权限。
- 位1(W/R),异常是由于内存读或写操作引起。W/R=0,表示由读操作引起;W/R=1,表示由写操作引起。
- 位2(U/S),发生异常时CPU执行的代码级别。U/S=0,表示CPU正在执行超级用户代码;U/S=1,表示CPU正在执行一般用户代码。
另外,处理器还会把引起页面故障异常所访问用的线性地址存放在CR2中。页出错异常处理程序可以使用这个地址来定位相关的页目录和页表项。
注意,错误不会被IRET指令自动地弹出堆栈,因此中断处理程序在返回之前必须清楚堆栈上的错误码。另外,虽然处理产生的某些异常会产生错误码并会自动地保存到处理过程的堆栈中,但是外部硬件中断或者程序执行INT n 指令产生的异常并不会把错误码压入堆栈中。
4.7任务管理
任务(Task)是处理器可以分配调度、执行和挂起的一个工作单元。它可用于执行程序、任务或进程、操作系统服务、中断或异常处理过程和内核代码。
80X86提供了一种机制,这种机制可以用来保存任务的状态、分派任务执行以及从一个任务切换到另一个任务。当工作在保护模式下,处理器所有运行都在任务中。即使是简单系统页必须起码定义一个任务。更为复杂的系统可以使用处理器的任务管理功能来支持多任务应用。
80X86提供了多任务的硬件支持。任务是一个正在运行的程序,或者是一个等待准备运行的程序。通过中断、异常、跳转或调用,我们可以执行一个任务。当这些控制转移形式之一和某个描述符表中指定项的内容一起使用时,那么这个描述符是一类导致新任务开始执行的描述符。描述符表中与任务相关的描述符有两类:任务状态段描述符和任务门。当执行权传给这任何一类描述符时,都会造成任务切换。
任务切换很像过程调用,但任务切换会保存更多的处理器状态信息。任务切换会把控制权转移到一个新的执行环境,即新任务的执行环境。这种转移操作要求保存处理器中几乎所有寄存器的当前内容,包括标志急促请你EFLAGS和所有段寄存器。与过程不过,任务不可重入。任务切换不会把任何信息压入堆栈中,处理器的状态信息都被保存在内存中称为任务状态段(Task state segment)的数据结构中。
4.7.1任务的结构和状态
一个任务由两部分构成:任务执行空间和任务状态段TSS(Task-state segment)。任务执行空间包括代码段、堆栈段和一个或多个数据段,见图4-33所示。如果操作系统使用了处理器的特权保护机制,那么任务执行空间就需要为每个特权级提供一个独立的堆栈空间。TSS指定了构成任务执行空间的各个段,并且为任务状态信息提供存储空间。在多任务环境中,TSS也为任务之间的链接提供了处理方法。
一个任务使用指向其TSS的段选择符来指定。当一个任务被加载进处理器中执行时,那么该任务的段选择符、基地址、段限长以及TSS段描述符属性就会被加载进任务寄存器TR(Task Register)中。如果使用了分页机制,那么任务使用的页目录表基地址就会被加载进控制寄存器CR3中。当前执行任务的状态由处理器中的以下所有内容组成:
- 所有通用寄存器和段寄存器信息;
- 标志寄存器EFLAGS、程序指针EIP、控制寄存器CR3、任务寄存器和LDTR寄存器;
- 段寄存器指定的任务当前执行空间;
- I/O映射位图基地址和I/O位图信息(在TSS中);
- 特权级0、1和2的堆栈指针(在TSS中);
- 链接至前一个任务的链指针(在TSS中)。
4.7.2任务的执行
软件或处理器可以使用以下方法之一来调度执行一个任务:
- 使用CALL指令明确地调用一个任务;
- 使用JMP指令明确地跳转到一个任务(Linux内核使用的方式);
- (由处理器)隐含地调用一个中断句柄处理任务;
- 隐含地调用一个异常句柄处理任务;
所有这些调度任务执行的方法都会使用一个指向任务门或任务TSS段的选择符来确定一个任务。当使用CALL或JMP指令调度一个任务时,指令中的选择符既可以直接选择任务的TSS,也可以选择存放有TSS选择符的任务门。当调度一个任务来处理要给中断或异常时,那么IDT中该中断或异常表项必须是一个任务门,并且其中含有中断或异常处理任务的TSS选择符。
当调度一个任务执行时,当前正在运行任何和调度任务之间会自动地发生任务切换操作。在任务切换期间,当前运行任务的执行环境(称为任务的状态或上下文)会被保存到它的TSS中并且暂停该任务的执行。此后新调度任务的上下文会被加载进处理器中,并且从加载的EIP指向的指令开始执行新任务。
如果当前执行任务(调用者)调用了被调度的新任务(被调用者),那么调用者的TSS段选择符会被保存在被调用者TSS中,从而提供一个返回调用者的链接。对于所有80X86处理器,任务是不可递归调用的,即任务不饿能调用或跳转到自己。
中断或异常可以通过切换到一个任务来进行处理。在这种情况下,处理器不仅能够执行任务切换来处理中断或异常,而且也会在中断或异常处理任务返回是自动地切换回被中断的任务中去。这中操作方式可以处理在中断任务执行时发生的中断。
作为任务切换操作的一部分,处理器也会切换到另一个LDT,从而允许每个任务对基于LDT的段具有不同逻辑到物理地址的映射。同时,页目录寄存器CR3也会在切换时被重新加载,因此每个任务可以有自己的一套页表。这些保护措施能够用来隔绝各个任务并且防止它们相互干扰。
使用处理器的任务管理功能来处理多任务应用是任选的。我们也可以使用软件来实现多任务,使得每个软件定义的任务在一个80X86体系的任务上下文中执行。
4.7.3任务管理数据结构
处理器定义了一下一些支持多任务的寄存器和数据结构:
- 任务状态段TSS;
- TSS描述符;
- 任务寄存器TR;
- 任务门描述符;
- 标志寄存器EFLAGS中的NT标志。
使用这些数据结构,处理器可以从一个任务切换到另一个任务,同时保存原任务的上下文,以允许任务重新执行。
4.7.3.1任务状态段
用于恢复一个任务执行的处理器状态信息被保存在称为任务段TSS(Task state segment)的段中。图4-34给出了32位CPU使用的TSS的格式。TSS段中个字段可分成两大类:动态字段和静态字段。
- 动态字段。当任务切换而被挂起时,处理器会更新动态字段的内容。这些字段包括:
- 通过寄存器字段。用于保存EAX,ECX、EDX、EBX、ESP、EBP、ESI和EDI寄存器的内容。
- 段选择符字段。用于保存ES、CS、SS、DS、FS和GS寄存器的内容。
- 标志寄存器EFLAGS字段。在切花之前保存EFLAGS。
- 指令指针EIP字段。在切换之前保存EIP寄存器内容。
- 先前任务连接字段。含有前一个任务TSS段选择符(在调用、中断或异常激发的任务切换时更新)。该字段(通常也称为后连接字段(Back link field))允许任务使用IRET指令切换到前一个任务。
2.静态字段:处理器会读取静态字段的内容,但通常不会改变它们。这些字段内容是任务被创建时设置的。这些字段有: - LDT段选择符字段。含有任务的LDT段的选择符。
- CR3控制寄存器字段。含有任务使用的页目录物理基地址。控制寄存器CR3通常也被称为页目录基地址寄存器PDBT(Page directory base register)。
- 特权级0、1和2的堆栈指针字段。这些堆栈指针由段选择符(SS0、SS1和SS2)和栈中偏移量指针(ESP0、ESP1和ESP2)组成。注意,对于指定的一个任务,这些字段的值时不变的。因此,如果任务中发生堆栈切换,寄存器SS和ESP的内容将会改变。
- 调试陷阱(Debug Trap)T标志字段。该字段位于字节0x64比特0处。当设置了该位时,处理器切换到该任务的操作将产生一个调试异常。
- I/O位图基地址字段。该字段含有从TSS段开始处到I/O许可位图处的16位偏移值。
如果使用了分页机制 ,那么在任务切换期间应该避免处理操作的TSS段中(前1024字节中)含有内存页边界。如果TSS这部分包含内存页边界,那么该边界处两边的页面都必须同时并且连续存在于内存中。另外,如果使用了分页机制,那么与原任务TSS和新任务TSS相关的页面,以及对应的描述符表项应该时可读写的。
4.7.3.2描述符
与其他段一样,任务状态段TSS也是使用段描述符来定义。图4-35给出了TSS描述符的格式。TSS描述符只能存放在GDT中。
类型字段TYPE中的忙标志B用于指明任务是否处于忙状态。忙状态的任务是当前正在执行的任务或等待执行(被挂起)的任务。任务是不可以递归执行的,因此处理器使用忙标志B来检测任何企图被中断执行任务的调用。
其中基地址、段限长、描述符特权等级DPL、颗粒度G和存在位具有与数据段描述符中相应字段同样的功能。当G=0时,限长字段必须具有等于或大于103(0x67)的值,即TSS段的最小长度不得小于104字节。如果TSS段中还包含I/O许可位图,那么TSS段长度需要大一些。另外,如果操作系统还想在TSS段中存放其他一些信息,那么TSS段就更需要更大的长度。
使用调用和跳转指令,人恶化可以访问TSS描述符的程序都能够造成任务切换。可以访问TSS描述符的程序器CPL数值必须小于或等于TSS描述符的DPL。在大多数系统中,TSS描述符的DPL字段值应该设置成小于3.这样,只有具有特权级的软件可以执行任务切换操作。然而在多任务应用中,某些TSS的DPL可以设置成3,以使得在用户特权级上也能进行任务切换操作。
可以访问一个TSS段描述符并没有给程序读写该描述符的能力。若想读或修改一个TSS段描述符,可以使用映射到内存相同位置的数据段描述符(即别名描述符)来操作。把TSS描述符加载进任何段寄存器将导致一个异常。企图使用TI标志置位的选择符(即当前LDT中的选择符)来访问TSS段业将导致异常。
4.7.3.3任务寄存器
任务寄存器TR(Task Register)中存放着16位的段选择符以及当前任务TSS段的整个描述符(不可见部分)。这些信息是从GDT中当前任务的TSS描述符中复制过来的。处理器使用任务寄存器TR的不可见部分来缓冲TSS段描述符内容。
指令LTR和STR分别用于加载和保存任务寄存器的可见部分,即TSS段的选择符。LTR指令只能被特权级0的程序执行。LTR指令通常用于系统初始化期间给TR寄存器加载初值(例如,任务0的TSS段选择符),随后在系统运行期间,TR的内容会在任务切换时自动地被改变。
4.7.3.4任务门描述符
任务门描述符(Task gate descriptor)提供对一个任务间接、受保护地的引用,其格式见图所示。任务描述符可以被存放在GDT、LDT或IDT表中。
任务门描述符中的TSS选择符字段指向GDT中的一个TSS段描述符。这个TSS选择符字段中的RPL域不用。任务门描述符中的DPL用于在任务切换时控制对TSS段的访问。当程序通过任务门调用或跳转到一个任务时,程序的CPL以及指向任务门的门选择符的RPL值必须小于或等于任务门描述符中的DPL。请注意,当使用任务门时,目标TSS段描述符的DPL忽略不用。
程序可以通过任务门描述符或者TSS段描述符来访问一个任务。图4-36示出了LDT、GDT和IDT表中的任务门如何都指向同一个任务。
4.7.4任务切换
处理器可使用以下4种方式之一执行任务切换操作:
-
当前任务对GDT中的TSS描述符执行JMP或CALL指令;
-
当前任务对GDT或LDT中的任务门描述符执行JMP或CALL指令;
-
中断或异常向量指向IDT表中的任务门描述符;
-
当EFLAGS中的NT标志置位时当前任务执行IRET指令。
JMP、CALL和IRET指令以及中断和异常都是处理器的普通机制,可用于不发生任务切换的环境中。对于TSS描述符或任务门的引用(当调用或跳转到一个任务),或者NT标志的状态(当执行IRET指令时)确定了是否发生任务切换。
为了进行任务切换,JMP或CALL指令能够把控制转移到TSS描述符或任务门上。使用这两种方式的作用相同,都会导致处理器把控制转移到指定的任务中,见图4-37所示。
当中断或异常的向量索引的是IDT中的一个任务门时,一个中断或异常就会造成任务切换。如果向量索引的是IDT中的一个中断或陷阱门,则不会造成任务切换。
中断服务过程总是把执行权返回到被中断的过程中,被中断的过程可能在另一个任务中。如果NT标志处于复位状态,则执行一般返回处理。如果NT标志是置位状态,则返回操作会产生任务切换。切换到新任务由中断服务过程TSS中的TSS选择符(前一任务链接字段)指定。
当切换到一个新任务时,处理器会执行以下操作:
-
从作为JMP或CALL指令操作数中,或者从任务门中,或者从当前TSS的前一任务链接字段(对于由IRET引起的任务切换)中取得新任务的TSS段选择符。
-
检查当前任务是否允许切换到新任务。把数据访问特权级规则应用到JMP和CALL指令上。当前任务的CPL和新任务段选择符的RPL必须小于或等于TSS段描述符的DPL,或者引用的是一个任务门。无论目标任务门或TSS段描述符的DPL是何值,异常、中断(除了使用INT n指令产生的中断)和IRET指令都允许执行任务切换。对于INT n指令产生的中断将检查DPL。
3.检查新任务的TSS描述符是标注为存在的(P=1),并且TSS段长度有效(大于0x67)。当试图执行会产生错误的指令时,都会恢复对处理器状态的任何改变。这使得异常处理过程的返回地址指向出错指令,而非出错指令随后的一条指令。因此异常处理过程可以处理出错条件并且重新执行任务。异常处理过程的介入处理对应应用程序来说是完全透明的。
-
如果任务切换产生自JMP或IRET指令,处理器就会把当前任务(老任务)TSS描述符中的忙标志B复位;如果任务切换是由CALL指令、异常或中断产生,则忙标志B不动。
-
如果任务切换由IRET产生,则处理器会把临时保存的EFLAGS映像中的NT标志复位;如果任务切换由CALL、JMP指令或者异常或中断产生,则不用改动上述NT标志。
-
把当前任务的状态保存到当前任务的TSS中。处理器会从任务寄存器中取得当前任务TSS的基地址,并且把寄存器内容复制到当前TSS中:所有通用寄存器、段寄存器中的段选择符 、标志寄存器EFLAGS以及指令指针EIP。
-
如果任务切换是由CALL指令、异常或中断产生,则处理器就会把从新任务中加载的EFLAGS中的NT标志置位。如果任务切换产生自JMP或IRET指令,就不改动新加载EFLAGS中的标志。
-
如果任务切换由CALL、JMP指令或者异常或中断产生,处理器就会设置新任务TSS描述符中的忙标志B。如果任务切换由IRET产生,则不去改动B标志。
-
使用新任务TSS的段选择符和描述符加载任务寄存器TR(包括隐藏部分)。设置CR0寄存器的TS标志。
-
把新任务的TSS状态加载进处理器。这包括LDTR寄存器、PDBR(CR3)寄存器、EFLAGS寄存器、EIP寄存器以及通用寄存器和段选择符。在此期间检测到的任何错误都将出现在新任务的上下文中。
-
开始执行新任务(对于异常处理过程,新任务的第一条指令显现出还没有执行)。
当成功地进行了任务切换操作,当前执行任务的状态总是会被保存起来。当任务恢复执行时,任务将从保存的EIP指向的指令开始执行,并且所有寄存器都恢复到任务挂起时的值。
当执行任务切换时,新任务的特权级与原任务的特权级没有任何关系。新任务在CS寄存器的CPL字段指定的特权级上开始运行。因为各个任务通过它们独立的地址空间和TSS段相互隔绝,并且特权级规则已经控制对TSS的访问,所以在任务切换时软件不需要再进行特权级检查。
每次任务切换都会设置控制寄存器CR0中的任务切换标志TS。该标志对系统软件非常有用。系统软件可用TS标志来协调处理器和浮点处理器之间的操作。TS标志表明协处理器中的上下文内容可能与当前正在执行任务的不一致。
4.7.5任务链
TSS的前一任务连接(Backlink)字段以及EFLAGS中的NT标志用于返回到前一个任务操作中。NT标志指出了当前执行的任务是否是嵌套在另一个任务中执行,并且当前任务的前一任务连接字段中存放着嵌套层中更高任务的TSS选择符,若有的话(见图4-38所示)
当CALL指令、中断或异常造成任务切换,处理器把当前TSS段的选择符复制到新任务TSS段的前一任务链接字段中,然后在EFLAGS中设置NT标志。NT标志指明TSS的前一任务链接字段中存放有保存的TSS段选择符。如果软件使用IRET指令挂起新任务,处理器就会使用前一任务链接字段中值和NT标志返回到前一个任务。也即如果NT标志是置位的话,处理器会切换到前一任务链接字段指定的任务去执行。
注意,当任务切换是由JMP指令造成,那么新任务就不会是嵌套的。也即,NT标志会被设置为0,并且不使用前一任务链接字段。JMP指令用于不希望出现嵌套的任务切换中。
表4-10总结了任务切换期间,忙标志B(在TSS段描述符中)、NT标志、前一任务链接字段和TS标志(在CR0中的)用法。注意,运行于任何特权级上的程序都可以修改NT标志,因此任何程序都可以设置NT标志并执行IRET指令。这中做法会让处理器去执行当前任务TSS的前一任务链接字段指定的任务。为了避免这种伪造的任务切换执行成功,操作系统应该把每个TSS的该字段初始化为0。
4.7.6任务地址空间
任务的地址空间由任务能过访问的段构成。这些段包括代码段、数据段、堆栈段、TSS中引用的系统段以及任务代码能够访问的任何其他段。这些段都被映射到处理器的线性地址空间中,并且随后被直接地或者通过分页机制映射到处理器的物理地址空间中。
TSS中的LDT字段可以用于给出每个任务自己的LDT。对于一个给定的任务,通过把与任务相关的所有段描述符放入LDT中,任务的地址空间就i可以与其他任务的隔绝开来。
当然,几个任务也可以使用同一个LDT。这是一种简单而有效的允许某些任务互相通信或控制的方法,而无须抛弃整个系统的保护屏障。
因为所有任务都可以访问GDT,所以也同样可以创建通过此表访问的共享段。
如果开启了分页机制,则TSS中的CR3寄存器字段可以让每个任务有它自己的页表。或者,几个任务能够共享相同页表集。
4.7.6.1把任务映射到线性和物理地址空间
有两种方法可以把任务映射到线性地址空间和物理地址空间:
-
所有任务共享一个线性到物理地址空间的映射。当没有开启分页机制时,就只能使用这个办法。不开启分页时,所有线性地址映射到相同的物理地址上。当开启了分页机制,那么通过让所有任务使用一个页目录,我们就可以使用这种从线性到物理地址空间的映射形式。如果支持需求页虚拟存储技术,则线性地址空间可以超过现有物理地址空间的大小。
-
每个任务有自己的线性地址空间,并映射到物理地址空间。通过让每个任务使用不同的页目录,我们就可以使用这种映射形式。因为每次任务切换都会加载PDBR(控制寄存器CR3),所以每个任务可以有不同的页目录。
不同任务的线性地址空间可以映射到完全不同的物理地址上。如果不同页目录的条目(表项)指向不同的页表,而且页表页指向物理地址中不同的页面上,那么各个任务就不会共享任何物理地址。
对于映射任务线性地址空间的这两种方法,所有任务的TSS都必须存放在共享物理地址空间区域中,并且所有任务都能访问这个区域。为了让处理器执行任务切换而读取或更新TSS时,TSS地址的映射不会改变,就需要使用这种映射方式。GDT所映射的线性地址空间也应该映射到共享的物理地址空间中。否则就丧失了GDT的作用。
4.7.6.2任务逻辑地址空间
为了在任务之间共享数据,可是使用下列方法之一来为数据段建立共享的逻辑到物理地址空间的映射:
通过使用GDT中的段描述符。所有任务必须能够访问GDT中的段描述符。如果GDT中的某些段描述符指向线性地址空间中的一些段,并且这些段被映射到所有任务共享的物理地址空间中,那么所有任务都可以共享这些段中的代码和数据。
通过共享的LDT。两个或多个任务可以使用相同的LDT,如果它们TSS中LDT字段指向同一个LDT。如果一个共享的LDT中某些段描述符指向映射到物理地址空间公共区域的段,那么共享LDT的所有任务可以共享这些段中的所有代码和数据。这种共享方式要比通过GDT来共享好,因为这样做可以把共享局限于指定的一些任务中。系统中有与此不同的LDT的其他任务没有访问这些共享段的权力。
通过映射到线性地址空间公共地址区域的不同LDT中的段描述符。如果线性地址空间中的这个公共区域对每个任务都映射到物理地址空间的相同区域,那么这些段描述符就允许任务共享这些段。这样的段描述符通常称为别名段。这个共享方式要比上面给出的方式来得更好,因为LDT中的其他段描述符可以指向独立的未共享线性地址区域。
4.8保护模式编程初始化
我们知道,80X86可以工作在几种模式下。当机器上或硬件复位时,处理器工作在8086处理器兼容的实地址模式下,并且从物理地址0xFFFFFFF0开始执行软件初始化代码(通常在EPROM中)。软件初始化代码首先必须设置基本系统功能操作必要的数据结构信息,例如处理中断或异常的实模式IDT表(即中断向量表)。如果处理器将仍然工作在实模式下,那么软件必须加载操作系统模块和相应数据以允许应程序能在实模式下可靠地运行。如果处理器将要工作在保护模式下,那么操作系统软件就必须加载保护模式操作必要的数据结构信息,然后切换到保护模式。
4.8.1进入保护模式时的初始化操作
保护模式所需要的一些数据结构由处理器内存管理功能确定。处理器支持分段模型,可以使用从单个、统一的地址空间平坦模型到每个任务都具有受几个保护地址空间的高度结构化的多端模型。分页机制能够用来部分在内存、部分在磁盘的大型数据结构信息。这两种地址转换形式都需要操作系统在内存中为内存管理硬件设置所要求的数据结构。因此在处理器能够被切换到保护模式下运行之前,操作系统加载和初始化软件(bootsect.s、setup,s和head.s)必须在内存中先设置好保护模式下使用的数据结构的基本信息。
这些数据结构包括以下几种:
- 保护模式中断描述符表IDT;
- 全局描述符表GDT;
- 任务状态段TSS;
- 局部描述符表LDT;
- 若使用分页机制,则起码需要设置一个页目录和一个页表;
- 处理器切换到保护模式下运行的代码段;
- 含有中断和异常处理程序的代码模块。
在能够切换到保护模式之前,软件初始化代码还必须设置以下系统寄存器: - 全局描述符表基地址寄存器GDTR;
- 中断描述符表基地址寄存器IDTR;
- 控制寄存器CR1--CR3
在初始化这些数据结构、代码模块和系统寄存器之后,通过设置CR0寄存器的保护模式标志PE(位0),处理器就可以切换到保护模式下运行。
4.8.1.1保护模式系统结构表
软件初始化期间在内存中设置的保护模式系统表主要依赖于操作系统将要支持的内存管理类型,平坦的、平坦并支持分页的、分段的或者分段并支持分页的。
为了实现无分页的平坦内存模型,软件初始化代码必须起码设置具有一个代码段和一个数据段的GDT表。当然GDT表第1项还需要放置一个空描述符。堆栈可以放置在普通读写数据段中,因此并不需要专门的堆栈描述符。支持分页机制的平坦内存模型还需要一个页目录和至少一个页表。在可以使用GDT表之前,必须使用LGDT指令把GDT表的基地址和长度值加载到GDTR寄存器中。
而多段模型则还需要用于操作系统的其他段,以及用于每个应用程序的段和LDT表段。LDT表的段描述符要求放在GDT表中。某些操作系统会为应用程序另行分配新段和新的LDT段。这种做法为动态编程环境提供了最大灵活性,例如Linux操作系统就使用了这种方式。过程控制器那样的嵌入系统可以预先为固定数量的应用程序分配固定数量的段和LDT,这是实现实时系统软件环境结构的一种简单而有效的方法。
4.8.1.2保护模式异常和中断初始化
软件初始化代码必须设置一个保护模式IDT,其中最少需要含有处理器可能产生的每个异常向量对应的门描述符。如果使用了中断或陷进门,那么门描述符可以都指向中断和异常处理过程的同一个代码段。若使用了任务门,那么每个使用任务门的异常处理过程都需要一个TSS以及相关的代码、数据和堆栈段。如果允许硬件产生中断,那么必须在IDT中为一个或多个处理过程设置门描述符
在可以使用IDT之前,必须使用LIDT指令把IDT表基地址和长度加载到IDTR寄存器中。
4.8.1.3分页机制初始化
分页机制由控制寄存器CR0中的PG标志设置。当这个标志被清0时,(即硬件复位时的状态),分页机制被关闭;当设置了PG标志,就开启分页机制。在设置PG标志之前,必须先初始化以下数据结构和寄存器:
- 软件必须在物理内存中建立至少一个页目录和一个页表。如果页目录表中含有指向自身的目录项时,可以不使用页表。此时页目录表和页表被存放在同一页面中。
- 把页目录表的物理基地址加载到CR3寄存器中(也称为PDBR寄存器)。
- 处理器处于保护模式下。如果满足所有其他限制,则PG和PE标志可以同时设置。
为保持兼容性,设置PG标志(以及PE标志)时必须遵守以下规则: - 设置PG标志的指令应该立刻跟随一条JMP指令。MOV CR0指令后面的JMP指令会改变执行流,所以它会清空80X86处理器已经取得或已译码的指令。然而,Pentium及以上处理器使用了分支目标缓冲器BTB(Branch Target Buffer)为分支代码定向,因此减去了为分支指令刷新队列的需要。
- 设置PG标志跳转到指令JMP之间的代码必须来自对等映射(即跳转之前的线性地址与开启分页后的物理地址相同)的一个页面上。
4.8.1.4多任务初始化
如果将要使用多任务机制,并且/或者允许改变特权级,那么软件初始化代码必须至少设置一个TSS及相应的TSS段描述符(因为特权级0、1和2的各栈段指针需要从TSS中取得)。在创建TSS描述符时不要将其标注为忙(不要设置忙标志),该标志仅由处理器在执行任务切换时设置。与LDT段描述符相同,TSS的描述符也存放在GDT中。
在处理器切换到保护模式之后,可以用LTR指令把TSS段描述符的选择符加载到任务寄存器TR中。这个指令会把TSS标记成忙状态(B=1),但是并不执行任务切换操作。然后处理器可以使用这个TSS来定位特权级0、1和2的堆栈。在保护模式下,软件进行第一次任务切换之前必须首先加载TSS段的选择符,因为任务切换会把当前任务状态复制到该TSS中。
在LTR指令执行之后,随后对任务寄存器的操作由任务切换进行。与其他的段和LDT类似,TSS段和TSS段描述符可以预先设置好,也可以在需要时进行设置。
4.8.2模式切换
为了让处理器工作在保护模式下,必须从实地址模式下进行模式切换操作。一旦进入保护模式,软件通常不会再需要回到实地址模式。为了还能运行为实地址模式编制的程序,通常在虚拟-8086模式中运行比再切换回实模式下运行更为方便。
4.8.2.1切换到保护模式
在切换到保护模式之前,必须首先加载一些起码的系统数据结构和代码块下进行模式切换 操作。一旦建立了这些系统表,软件初始化代码就可以切换到保护模式中。通过执行在CR0寄存器中设置PE标志的MOV CR0指令,我们就可以进行保护模式。(在同一指令中,CR0的PG标志就可用于开启分页机制。)刚进入保护模式中运行时,特权级是0。为了保证程序的兼容性,切换操作应该按照以下步骤进行:
-
禁止中断。使用CLI指令可以禁止可屏蔽硬件中断。NMI会由硬件电路来禁止。同时软件应该确保在模式切换操作期间不会产生异常和中断。
-
执行LGDT指令把GDT表的基地址加载进GDTR寄存器。
-
执行在控制寄存器CR0中 设置PE标志,(可选同时设置PG标志)的MOV CR0指令。
-
在MOVCR0指令之后立刻执行一个远来JMP或远调用CALL指令。这个操作通常是远跳转到或远调用指令流中的下一条指令。
-
若要使用局部描述符表,则执行LLDT指令把LDT段的选择符加载到LDTR寄存器中。
-
执行LTR指令,用初始保护模式任务的段选择符或者可写内存区域的段描述符加载任务寄存器TR。这个可写内存区域用于在任务切换时存放任务的TSS信息。
-
在进入保护模式后,段寄存器仍然含有实地址模式时的内容。第4步中对的JMP或CALL指令会重置CS寄存器。执行以下操作之一可以更新其余段寄存器的:其余段寄存器的内容可通过重新加载或切换到一个新任务来更新。
-
执行LIDDT指令把保护模式IDT表的基地址和长度加载到IDTR寄存器中。
-
执行ST1指令开启可屏蔽硬件中断,并且执行必要的硬件操作开启NMI中断。
另外,MOV CR0指令之后紧接着的JMP或CALL指令会改变执行流。如果开启了分页机制,那么MOV CR0指令到JMP或CALL指令之间的代码必须来自对等映射(即跳转之前的线性地址与开启分页后的物理地址相同)的一个页面上。而JMP或CALL指令跳转到目标指令并不需要处于对等映射页面上。
4.8.2.2切换回实地址模式
若想切换回实地址模式,则可以使用MOV CR0指令把控制寄存器CR0中的PE标志清0。重新进入实地址模式的过程应该按照以下步骤进行:
- 禁止中断。使用CLI指令可以禁止可屏蔽硬件中断。NMI会由硬件电路来禁止。同时软件应该确保护模式切换操作期间不产生异常和中断。
- 如果已开启分页机制,那么需要执行:
- 把程序的控制转移到对等映射的线性地址处(即线性地址等于物理地址)。
- 确保GDT和IDT在对等映射的页面上。
- 清楚CR0中的PG标志。
- CR3寄存器置为0x00,用于刷新TLB缓冲。
- 把程序的控制转移到长度为64KB(0XFFFF)的可读段中。这步操作使用实模式要求的段长度加载CS寄存器。
- 使用指向含有以下设置值的描述符的选择符来加载SS、DS、ES、FS和GS段寄存器。
- 段限长Limit=64KB
- 字节颗粒度(G=0)。
- 向上扩展(E=0)。
- 可写(W=1)。
- 存在(P=1)。
- 执行LIDT指令来指向在1MB实模式地址范围内的实地址模式中断表。
- 清楚CR0中的PE标志来切换到实地址模式。
- 执行一个远跳转指令跳转到一个实模式程序中。这步操作会刷新指令队列并且为CS寄存器加载合适的基地址和访问权限值。
- 加载实地址模式程序代码会使用的SS、DS、ES、FS和GS寄存器。
- 执行ST1指令开启可屏蔽硬件中断,并且执行必要的硬件操作开启NMI中断。
另外,MOV CR0指令之后紧接着的JMP或CALL指令会改变执行流。如果开启了分页机制,那么MOV CR0指令到JMP或CALL指令之间的代码必须来自对等映射(即跳转之前的线性地址与开启分页后的物理地址相同)的一个页面上。而JMP或CALL指令跳转到目标指令并不需要处于对等映射页面上。
4.8.2.2切换回实地址模式
若想切换回实地址模式,则可以使用MOV CR0指令把控制寄存器CR0中的PE标志清0。重新进入实地址模式的过程应该按照以下步骤进行:
1.禁止中断。使用CLI指令可以禁止可屏蔽硬件中断。NMI会由硬件电路来禁止。同时软件应该明保在模式切换操作期间不产生异常和中断。
2.如果已开启分页机制,那么需要执行:
- 把程序的控制转移到对等映射的线性地址处(即线性地址等于物理地址)。
- 确保GDT和IDT在对等映射的页面上。
- 清楚CR0中的PG标志。
- CR3寄存器置为0x00,用于刷新TLB缓冲。
3.把程序的控制转移到长度为64KB(0xFFFF)的可读段中。这步操作使用实模式要求的段长度加载CS寄存器。
4.使用指令含有以下设置的描述符的选择符来加载SS、DS、ES、FS和GS段寄存器。 - 段限长Limit = 64KB
- 字节颗粒度(G = 0)
- 向上扩展(E = 0)
- 可写(W = 1)
- 存在(P=1)
5.执行LIDT指令来指向在1MB实模式地址范围内的实模式中断表
6.清楚CR0中的PE标志来切换到实地址模式。
7.执行一个远跳指令跳转到一个实模式程序中。这步操作会刷新指令队列并且为CS寄存器加载合适的基地址和访问权限值。
8.加载实地址模式程序代码会使用的SS、DS、ES、FS和GS寄存器
9.执行STI指令开启可屏蔽硬件中断,并且执行必要的硬件操作开启NMI中断。