在IA32下,CPU有两种工作模式:实模式和保护模式。两种模式都使用Segment:Offset的方式进行寻址。但是两种模式下,段和偏移的含义是完全不一样的。

实模式

计算机启动之后首先进入实模式。在实模式下遵循Intel8086的16位CPU模式,具有16位寄存器,16位数据总线,20位地址总线物理地址遵循如下计算公式:

Physical_Address = Segment << 4 + Offset

保护模式

保护模式工作在Intel80386之后的CPU上。在保护模式下我们有32位地址线,和32位寄存器,单纯使用一个寄存器就可以寻址4GB的空间。但是在保护模式下,寻址方式依然采用Segment:Offset的方式,但是此时的段和偏移的含义与实模式是完全不同的。此时的段和偏移仅仅作为索引指向内存中的一个数据结构的一个表项。这个数据结构称为GDT(Global Descriptor Table)。GDT可为系统提供段式存储机制

以下是《Orange’s 一个操作系统的实现》中一个GDT表的样例:

1
; usage: Descriptor Base, Limit, Attr
;		 Base: dd
;		 Limit: dd (low 20 bits available)
;		 Attr: dw
%macro Descriptor 3
	dw	%2	&	0FFFFh
	dw	%1	&	0FFFFh
	db	(%1	>> 16) & 0FFh
	dw	((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)
	db	(%1	>> 24) & 0FFh
%endmacro

[Section .gdt]

LABEL_GDT:         Descriptor        0,                 0,            0
LABEL_DESC_CODE32: Descriptor        0,  SegCode32Len - 1, DA_C + DA_32
LABEL_DESC_VIDEO:  Descriptor  0B8000h,            0ffffh,       DA_DRW

GdtLen  equ	$ - LABEL_GDT
GdtPtr  dw  GdtLen - 1
           dd  0

SelectorCode32	equ	LABEL_DESC_CODE32	- LABEL_GDT
SelectorVideo	equ	LABEL_DESC_VIDEO	- LABEL_GDT
; END of [SECTION .gdt]

概括来说,GDT的每个表项用三部分内容描述一个段:段基址,段界限,段属性。

GdtPtr的48位内存用来指向GDT,其结构为:[32位基地址|16位界限]。可通过指令lgdt [GdtPtr]加载到48位寄存器gdtr

代码中Selector*称作选择子,当选择子低位的TI位是0的时候可以简单看做是表项在GDT中的偏移索引,后续可以被加载到段寄存器中,与gdtr寄存器中得GDT基址一起来定位一个段。否则就是LDT中的一个表项。

此时Segment:Offset构成一个逻辑地址。段机制将逻辑地址(Logical Address)转化成线性地址(Linear Address)的基本过程如下:

  1. 首先由段寄存器中存储的Segment(也是就一个Seletor)与gdtr存储的GDT表项的基址定位到GDT中的段表项.
  2. 之后由该表项中的段基址与Offset定位到线性地址空间。

模式切换

从实模式切换到保护模式,有两个关键的位需要设置。

  1. 设置A20地址线位:8086下只有20位地址线,如果试图访问超过1M的地址,则会对地址进行回卷。80286之后可以访问到得地址空间更大,但是为了兼容8086,则用A20地址位来控制访问超过1M的地址空间时是否对地址进行回卷。

  2. cr0寄存器的第0位。当该位为0时,CPU工作在实模式,否则工作在保护模式。保护模式下,所有寻址相关的指令,都使用逻辑地址的寻址方式,而不再是实模式下的物理地址的寻址方式。

描述符属性

在GDT表项中,每一个段描述符都有其属性描述位,保护模式也正是依赖这些属性实现操作系统的代码权限的保护及内存管理。例如:

  • P位(Present)表示该段是否在内存中存在。
  • DPL特权级(Descriptor Privilege Level)描述了该段代码的特权级别。
  • S位指明描述符是数据段/代码段,还是系统段/门描述符。
  • TYPE描述了该段的类型(读写,只读,只执行,一致代码段等)。

转移到一致代码段的时候,当前的特权级会延续下去。通过calljmp指令在不同特权级代码段之间跳转的规则如下:






【目标代码段】 【低→高】 【高→低】 【相同特权级】 【适用何种代码】
一致代码段 YES NO YES 不访问受保护的资源的系统代码
非一致代码段 NO NO YES 避免低特权程序访问的系统代码
数据段(总是非一致) NO YES YES -

门描述符

门描述符描述了一个由一个选择子和一个偏移地址所指定的线性地址,程序可以通过这个地址进行转移。通过调用门可以实现从第特权级到高特权级的转移,无论代码段是一致的还是非一致的。

假设代码A通过一个调用门G转移到代码B,即调用门G中的目标选择子指向代码B的段。需要涉及到:CPL、RPL、代码B的DPL_B、调用门G的DPL_G。

  1. 当A访问G的调用门时,规则相当于访问一个数据段,要求CPL和RPL特权级都不低于DPL_G。
  2. 系统还比较CPL和DPL_B,如果是一致代码段,则要求DPL_B特权级不低于CPL;如果是非一致特权级,调用call指令要求DPL_B特权级不低于CPL,调用jmp指令要求DPL_B特权级与CPL相同。

注意对比描述符属性中直接跳转的权限规则。会发现这里可以由低权限代码跳转到高权限的非一致代码段,而直接跳转是不允许的。同时,调用门也只能实现特权级由低到高的转移。

特权级规则

处理器通过CPL、DPL、RPL三种特权级进行特权级检验。

  1. CPL(Current Privilege Level) CPL是当前执行的程序或任务的特权级,存储在csss的第0位和第1位。通常情况下CPL等于代码段所在段的特权级,当程序转移到不同的特权级代码段时,处理器将改变CPL。但是当跳转到一个一致代码段时,CPL不会改变。
  1. DPL(Descriptor Privilege Level) DPL表示段或门的特权级,存储在段或门的描述符字段中。当代码尝试访问一个段或者门时,DPL将与CPL或门选择子的RPL比较,以判断是否可以访问。

  2. RPL(Requested Privilege Level) RPL存储在选择子(一个段可以有多个选择子)的第0位和第1位。处理器通过检查RPL和CPL确认访问请求是否合法,在判断权限时,取RPL和CPL中权限最低的那个。

程序从一个代码段转移到另外一个代码段之前,目标代码段的选择子会被加载到cs中。在加载过程中,处理器会检查描述符的界限、类型、特权级等内容。检验成功后cs才会被加载。

通过jmpcall可实现的四种转移:

  1. 目标操作数包含目标代码段的段选择子。
  2. 目标操作数指向一个包含目标代码段的选择子的调用门描述符
  3. 目标操作数指向一个包含目标代码段选择子的TSS(Task-State Stack)
  4. 目标操作数指向一个任务门,这个任务门指向一个包含目标代码段选择子的TSS。

特权级转移

特权级转移的同时,堆栈也要随之变化,以避免高特权级的过程受到干扰。jmp指令仅仅涉及指令跳转,而call指令会影响堆栈。在段内跳转时,只要将参数和调用者的eip入栈即可。而段间跳转涉及到得还有cs的入栈,以及特权级变化的时候相应堆栈的切换。一个任务之多可以在4个特权级之间切换,因此需要4个堆栈,当特权级发生转移时,堆栈内的入栈参数会复制到目标特权级对应的堆栈中。

TSS(Task-State Stack)是一个数据结构,可以记录一个任务不同特权级的ssesp地址。

整个转移过程CPU所做的工作如下:

  1. 根据目标代码段的DPL(新的CPL)从TSS中选择应该切换到哪个ssesp
  2. 从TSS中读取新的ssesp,并检验是否越界等。
  3. 暂时保存当前ssesp,并加载新ssesp
  4. 将刚刚保存起来的ssesp压栈。
  5. 从调用者堆栈中将参数复制到被调用者的堆栈。
  6. 将当前cseip压栈。
  7. 加载调用门中指定的cseip,并开始执行被调用者过程。

call对应的ret过程也同样涉及相同的过程,CPU的工作包括:

  1. 检查保存的cs中的RPL以判断返回时是否需要变换特权级。
  2. 加载被调用者堆栈上的cseip(此时会进行代码段描述符和选择子类型和特权级检验)。
  3. 如果ret指令含有参数,则增加esp的值以跳过参数。然后esp指向被保存过的调用者ssesp
  4. 加载ssesp,切换到调用者堆栈。并检查ss描述符、ds、es、fs、gs等得值,如果某个寄存器指向的段的DPL小于CPL,则置空。

页式存储

页式存储是由CPU的保护模式支持的。前面已经说到保护模式下,由Segment:Offset描述了逻辑地址,再经过段机制将其转换为线性地址。在未打开分页机制时,线性地址等同于物理地址。但是当分页开启时,线性地址要通过分页机制才能转化为物理地址。分页机制是否生效的开关位于cr0寄存器的PG位。

正如以往操作系统教材里描述的,使用两级页表来进行线性地址到物理地址的变换。第一级称作页目录其表项称为PDE(Page Directory Entry);第二级为页表,其表项称为(Page Table Entry)。

进行转换时,首先由cr3指定的页目录中根据线性地址的高10位得到页表地址,之后在页表中根据线性地址的12到21位得到物理页首地址,将这个首地址加上线性地址的低12位便得到了物理地址。

利用CPU提供的页式存储机制,操作系统才能够为进程统一的提供互不干扰的线性地址,并且无需考虑物理内存在不同平台下得差异。在进程切换的时候,只需要更改cr3切换对应的页表即可。

中断与异常

保护模式下的中断与实模式下也不同,实模式下得中断向量表和BIOS中断在保护模式下都不可用。保护模式下通过IDT(Interrupt Descriptor Table)实现中断调用。中断门和陷阱门都是特殊的调用门,作用机理几乎一样,只是由中断和异常触发而非call指令。

保护模式小结

  1. 在GDT、LDT以及IDT中,每个描述符都有自己的界限和属性,对描述符所描述的对象进行了限定和保护。
  2. 分页机制的PDE和PTE包含的R/W和U/S等提供了页级的保护。
  3. 页式存储的使用使得应用程序使用的是线性地址空间而不是物理地址,保护了物理内存,也避免了程序之间的相互干扰。
  4. 中断也提供了特权检验等内容。
  5. I/O指令也不再随便使用,提供了端口的保护。
  6. 不同特权级之间的切换伴随着CPL、PRL、DPL、IOPL等检验,同时伴随堆栈切换,对不同层级的程序进行了保护。

操作系统所能提供的大多保护其实都是依赖于处理器的硬件才能实现的。