细说80286

x86 CPU有实模式、保持模式、虚拟8086模式、系统管理模式等的分别。 x86 CPU只有在启动的时候才能进入实模式,一旦切换到保持模式就无法退出回到实模式。

实模式与保护模式

简单的讲,实模式就是8086使用CPU的模式。当然那时还没有实模式的叫法。只有后面有保护模式,旧的模式才有了实模式的称呼。

简要介绍实模式如下:

  • 16位寄存器
  • 20位地址线,可访问1MB内存
  • 通过CS/DS寄存器左移4位+IP寄存器的值生成20位访问地址

这里有两个问题值得注意:

  1. CS«4 + IP理论上来讲,最大可以表示的数值是0xFFFF0 + 0xFFFF = 0x10FFEF,即大约1M+64KB-16Bytes,然而由于地址线只有20根(A0~A19),这个地址最前面的1无法被表示,当CS=0xFFFF时,实际访问的地址0x10FFEF就变成了0xFFEF。即地址由卷回了0地址到64KB-16Bytes处。
  2. 由于程序可以任意修改当前的CS/DS值,所有程序可以使用全部1MB的内存,所以这个CPU几乎没有办法有效地支持多任务,因为两个程序一起运行的话很容易互相踩到内存。所以当时的使用的方式系统中同时运行的只有一个应用程序和一个DOS操作系统。操作系统和应用规定了各自能使用的内存地址范围,比如说DOS只使用高64KB的内存,其它的内存给应用程序使用。这样就可以互不影响。要想运行另一个应用程序必须先退出当前运行的应用程序。

上面的第二个问题其实就是保护模式"保护"二字的来源。保护模式的初衷就是保护一个进程的内存不被其他进程非法访问。 上面的第一个问题之所以被提到是因为它在80286设计的时候需要做特殊的处理。

#80286的保护模式 实模式即实际地址模式,用户写的代码中CS与IP寄存器组合成的地址即为实际的物理内存地址。这种方式非常的不安全。不方便实现多任务系统。在设计80286的时候要解决的主要就是这个问题。 在80286中,段式访存得到的改进,原来段寄存器+IP得到的地址不在是实际的物理地址,而是要经过一个转换层转换才变成一个物理地址。CS里面的内存不再是20位物理地址的高16位,而变成了段描述符表中的索引。

如图所示: 原来16位的段寄存器改名叫段选择子。现在是如何工作的,如何控制一个程序可以访问的内存范围呢?

答案: DOS系统在启动用户应用程序时先在内存中设置两个表,一个叫全局描述符表,GDT,一个叫局部描述符表LDT。表中的每个条目实际代表一段内存地址,包含这段内存的启始地址和段的长度,即base和limit。一个表的多个条目即代码这个用户程序可以访问的多块内存。

DOS系统把这两个表的内存启始地址写到LDTR和GDTR两个寄存器里。并在CS、DS寄存器中设置好一个初始的索引。

应用程序在访问内存时,CS:IP组合称为逻辑地址,CPU拿CS中的3~15位所组成的数据作为索引去LDT或GDT中找到一个描述符,拿这个描述符的base + IP做为实际的物理内存地址去访存。LDT和GDT只有DOS操作系统才能修改,因而应用程序只能访问DOS为它设置好的内存范围,而不能随意访问全部物理内存。

应用程序内存不够用时,需要调用一些系统调用,让DOS分配一段内存,把将这段内存的base, limit做成一个描述符加入到GDT或LDT中。

应用程序如果胡乱修改CS,造成无法索引到一个有效的段描述符就会发一个"段错误, segmentation fault"。

段选择子 CS/DS/ES/FS/GS/SS

如图所示,在80286中,CS/DS/ES/FS寄存器的意义发生了变化。它的3~15位是描述符表的索引。TI用来表示索引的是哪一张表,是GDT还是LDT。RPL称为请求权限级别。RPL对应描述符中的DPL,只要RPL的级别高于(值小于)DPL时,才有权限访问这段内存。 描述符中的DPL如下图所示:

80286的多任务

借助段描述符表,系统可以为应用程序分配内存,并且限制用户对内存的访问。这时候,多任务的方式汇总如下图:

基于这种内存管理方式,用户应用程序可以实现动态链接。比如说一个程序分为代码段、数据段、零初始化段等,它依赖的库也是分段的,系统在加载程序时,只需为每个段分配一段内存,并为每个段设置一个描述符即可。 每个段的起始地址可以在加载时根据实际情况修改。

代码段设置成只读只需要其描述符的TYPE字段即可。

A20 Gate和16MB内存

80286的寄存器仍然是16位宽,但它的内存地址总线是24位宽。可以访问16MB内存。在保护模式下,由于访存方式发生了变化,要访问16MB内存,只需在描述符中的base位宽可以达到24位即可。

然而在实地址模式下,访存方式为CS«4 +IP,最多只能表示0x10FFEF地址,即1MB+64KB-16Byte。这种访问方式已经没有任何优势,但是为了兼容旧的程序,80286以到最新的x86体系CPU在启动的时候都支持实模式,只能访问1MB+64KB-16Bytes的内存。

对于8086而言,由于地址线只有A0~A19,访问不了大于1MB的内存。然而80286由于有24根地址线,0x10FFEF这个数字中最高位的1可以体现在A20地址线上,从而访问1MB~ 1MB+64KB-16Bytes之间的内存。 这个1要不要体现在A20上,体现在A20上是一种很自然的想法,intel也是这样做的。然而这成了一个bug。

在8086上,超过1MB的地址访问会卷回0地址。这个是偶然的现像,本来就不是一个feature,而更像一个考虑不周的bug。然而实际程序开发中,很多为8086开发的程序利用了这一特性去访问0开头的低64KB内存。在80286上,利用了这个bug的程序就不工作了。于是这成了80286的一个bug。真让人无语。

为了让80286在实模式的行为和8086一样,IBM想出一个work around,即在A20总上加一个与逻辑门,与逻辑门的另一个输入信号接在键盘控制器芯片8042上,在8042中增加一组键盘控制组合键,可以控制A20线上信号的最终输出:

这个与逻辑门称为A20 Gate。

在现在CPU中,8042已经被集成在南桥上,A20 gate也被做成了标准功能,称为Fast A20 Gate,对A20的控制变成了对0x92端口上的bit 1的读写。打开A20 Gate的代码如下:

openA20:
	in   al,  92h
	or   al,  00000010b
	out 92h,  al

80286描述符表

在80286中有三种类型的描述符表:全局描述符表GDT(Global Descriptor Table)、局部描述符表LDT(Local Descriptor Table)和中断描述符表IDT(Interrupt Descriptor Table)。

在整个系统中,全局描述符表GDT和中断描述符表IDT只有一张,局部描述符表可以有若干张,每个任务可以有一张。任务切换时,局部描述符表也跟着切换,具体表现就是将LDTR中的值修改为当前任务对应的LDT的基地址。在任务切换时,切换LDT,并不切换GDT。通过LDT可以使各个任务私有的各个段与其它任务相隔离,从而达到受保护的目的。通过GDT可以使各任务都需要使用的段能够被共享。通过GDT在多个任务间共享内存非常方便。

每个描述符表本身形成一个特殊的数据段。这样的特殊数据段最多可包含有8K(8192)个描述符.

描述符表寄存器

描述符表可以放在内存中的任意位置,但CPU必须知道他们在哪里。因而每个描述符表都对应一个寄存器。即: GDTR LDTR IDTR

细说LDT

局部描述符表寄存器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的选择子装入到段寄存器,将引起异常。

细说IDT

中断描述符表本质上是不是段描述符表,而是一个中断向量表。 中断描述符表寄存器IDTR指向中断描述符表IDT。 IDTR长48 位,其中32位的基地址规定IDT的基地址,16位的界限规定IDT的段界限。

由于80386只支持256个中断/异常,所以IDT表最大长度是2K,以字节位单位的段界限为7FFH。IDTR 指示IDT的方式与GDTR指示GDT的方式相同。

任务状态段

系统中的每一个进程(任务)都有一个描述进程的信息需要保存,在x86中,专门将这些数据保存在一个段里。每个任务都有一个段,这些段的描述符都保存在GDT中,且类型为TSS。代码段描述符的选择子是CS/ES之类的寄存器,数据段的描述符的选择子是DS寄存器。任务状态段的描述符选择子也有一个单独的寄存器叫TR。一个进程可以随时通过TR得到该进程存在任务状态段中的信息。

段选择子

CS/DS/ES/SS/GS/FS在8086中是16位寄存器,用来表示一个段的高16位地址。 在80286中,它们代表在段描述符表中的索引。在访问内存时,先从段描述符表中查到对应描述符,取到描述符的base, limit,再与IP相加得到物理内存地址。段描述符表本身是在内存里的,每次访问内存都要去内存里找段描述符,要当于把一个访存操作变成了两个访问操作,效率较低。因而实际设计芯片时为CS/DS…等关联了些隐形的不可访问的寄存器,用来暂存该段选择子对应的描述符。这样在不进行段切换的时候,就不必每次访存都去读取一个段描述符。

CR0 模式切换

控制寄存器CR0中的位0用PE标记,控制分段管理机制的操作,所以把它们称为保护控制位。 PE控制分段管理机制。 PE=0,处理器运行于实模式; PE=1,处理器运行于保护方式。 从实模式切换到保护模式只需要将CR0的bit 0设为1即可。x86不支持从保护模式返回到实模式。

切换到保护模式的代码如下:

; switch to protection mode
switch_proMode:
	mov eax,    cr0
	or  eax,    1   ; set CR0's PE bit
	mov cr0,    eax