80X86保护模式内存管理

转自:http://www.cnitblog.com/ygb/articles/8872.html

Intel X86 CPU 系列的寻址方式

        在 X86 系列中, 8086 和 8088 是 16 位处理器,而从 80386 开始为 32 位处理器, 80286 则是系列从 8088 到 80386, 也就是从 16 位到 32 位过渡的一个中间步骤。 80286 虽然仍是 16 位处理器,但是在寻址方式上开始了从“初地址模式”到“保护模式”的过渡。


实地址模式

       当我们说一个 CPU 是“ 16 位”或“ 32 位”时,指的是处理器中“自述逻辑单元” (ALU) 的宽度。系统总线中的数据线部分,称为“数据总线”,通常与 ALU 具有相同的宽度 ( 但有例外 ) 。那么“地址总线”的宽度呢?最自然的地址总线宽度是与数据总线一致。这是因为从程序设计的角度来说,一个地址,也就是一个指针,最好是与一个整数的长度一致。但是如果从 8 位 CPU 寻址能力的角度来考虑,则实际上是不现实的,因为一个 8 位的地址只能用来寻访 256 个不同的地址单元,这显然太小了。所以,一般 8 位 CPU 的地址总线都是 16 位的。但 16 位还是太小。 Intel 决定在其 16 位 CPU ,即 8086 中采用 1M 字节的内存地址空间,地址总线的宽度也就相应地确定了,那就是 20 位。但这样就出现了一个问题,虽然地址总线的宽度是 20 位,但 CPU 中 ALU 的宽度却只有 16 位,也就是说直接加以运算的指针长度是 16 位的。如何来填补这个空隙呢? Intel 设计了一种在当时看来不失巧妙的方法,即分段的方法。

       Intel 在 8086CPU 中设置了四个“段寄存器”: CS 、 DS 、 SS 和 ES ,分别用于可执行代码即指令、数据、堆栈和其他。每个段寄存器都是 16 位,对应于地址总线中的高 16 位。每条“访内”指令中的“内部地址”都是 16 位的,但是在送上地址总线之前在 CPU 内部自动地与某个段寄存器中的内容相加,形成一个 20 位的实际地址。这样,就实现了从 16 位内部地址到 20 位实际地址的转换,或者“映射”。这里要注意段寄存器中的内容对应于 20 位地址总线中的高 16 位,所以在相加时实际上是拿内部地址中的高 12 位与段寄存器中的 16 位相加,而内部地址中的低 4 位保持不变。但这种方法是有缺陷的,主要是没有地址空间保护机制。对于每一个由段寄存器的内容确定的“基地址”,一个进程总是能够访问从此开始的 64K 字节的连续地址空间,而无法加以限制。同时,可以用来改变段寄存器内容的指令也不是什么“特权指令”,也就是说,通过改变段寄存器的内容,一个进程可以随心所欲地访问内存中的任何一个单元,而丝毫不受限制。不能对一个进程的内存访问加以限制,也就谈不上对其他进程以及系统本身的保护。与此相应,一个 CPU 如果缺乏对内存访问的限制,或者说保护,就谈不上什么内存管理,也就谈不上是现代意义上的中央处理器。由于 8086 的这种内存寻址方式缺乏对内存空间的保护,所以为了区别于后来出现的“保护模式”,就称为“实地址模式”。


保护模式

       针对 8086 的这种缺陷, Intel 从 80286 开始实现其“保护模式”。同时不久后 32 位的 80386CPU 也开发成功了。这样,从 8088/8086 到 80386 就完成了一次从比较原始的 16 位 CPU 到现代的 32 位 CPU 的飞跃,而 80286 则变成这次飞跃的一个中间步骤。

       80386 是个 32 位 CPU ,也就是说它的 ALU 数据总线是 32 位的,则最自然的地址总线宽度也应是与数据总线一致的。当地址总线的宽度达到 32 位时,其寻址能力达到了 4G ,对于内存来说似乎是足够了。所以,如果新设计一个 32 位 CPU 的话,其结构应该是可以做到很简洁,很自然的。但是, 80386 却无法做到这一点。作为一个产品系列中的一员, 80386 必须维持那些段寄存器,还必须支持实地址模式,在此同时又要支持保护模式。因此, Intel 决定在段寄存器的基础上构筑保护模式,并且保留段寄存器为 16 位 ( 这样才可以利用原有的四个段寄存器 ) ,但是却又增添了两个段寄存器 FS 和 GS 。为了实现保护模式,光是用段寄存器来确定一个基地址是不够的,至少还要有一个地址段的长度,并且还需要一些其他信息,如访问权限之类。所以,这里需要的是一个数据结构,而并非一个单纯的基地址。对此, Intel 设计人员的基本思路是:在保护模式下改变段寄存器的功能,使其从一个单纯的基地址变成指向这样一个数据结构的指针。因此,当一个访存指令发出一个内存地址时, CPU 按照下面过程实现从指令中的 32 位逻辑地址到 32 位物理地址的转换:

1.      首先根据指令的性质来确定该使用哪一个段寄存器,例如转移指令中的地址在代码段,而数据指令中的地址在数据段。这一点与实地址模式相同。

2.      根据段寄存器的内容,找到相应的 “ 段描述结构 ” 。

3.      从 “ 段描述结构 ” 中得到基地址。

4.      将指令中的地址作为位移,与段描述结构中规定的段长度相比,看是否越界;

5.      根据指令的性质和段描述符中的访问权限来确定是否越权;

6.      最后才将指令中的地址作为位移,与段基地址相加,得到物理地址。

       虽然段描述结构存储在内存中,在实际使用时却将其装载入 CPU 中的一组“影子”结构,而 CPU 在运行时则使用其在 CPU 中的“影子”。从保护的角度考虑,在由 ( 指令给出的 ) 内部地址 ( 或者说“逻辑地址” ) 转换成物理地址的过程中,必须要在某个环节上对访问权限时行比对,以访止不具有特权的用户程序通过玩弄某些诡计 ( 例如修改段寄存器的内容,修改段描述结构的内容等 ) ,得以非法访问其他进程的空间或系统空间,从而实现了保护。

明白了这个思路, 80386 的段式内存管理机制就比较容易理解了,下面就是此机制的实际实现。

首先,在 80386CPU 中增设了两个寄存器:一个是全局性段描述表寄存器 GDTR ,另外一个是局部性段描述表寄存器 LDTR ,分别可以用来指向存储在内存中的一个段描述结构数组,或者称为段描述表。由于这两个寄存器是新增设的,不存在与原有的指令是否兼容的问题,访问这两个寄存器的专用指令便设计成“特权指令”。    

    在此基础上,段寄存器的高 13 位用作访问段描述表中具体描述结构的下标 (index) ,如下图所示 :
段寄存器定义

RPL :请求特权级, 2 位二进制数字,求特权级是将要访问的段的特权级。

TI :表指示符。为 0 时,从 GDT 中选择描述符;为 1 时,从 LDT 中选择描述符。

Index :索引。指出要访问描述符在段描述符表中的顺序号。总共有 2的13次方=8192 个。


   GDTR 或 LDTR 中的段描述表指针段寄存器中给出的下标结合在一起,才决定了具体的段描述表项在内存中的什么地方,也可以理解成,将段寄存器内容的低 3 位屏蔽掉以后与 GDTR 或 LDTR 中的基地址相加得到描述表项的起始地址。因此就无法通过修改描述表项的内容来玩弄诡计,从而起到保护的作用。每个段描述表项的大小是 8 个字节,每个描述表项含有段的基地址和段的大小,再加上其他一些信息,其结构如下图所示:

也可以用一段“伪代码”来说明整个段描述结构:

段描述结构 :

typedef struct {

  unsigned int base_24_31:8;   // 基地址最高 8 位
  unsigned int g:1;         //granularity 表段长度单位 [0] 字节 [1]4KB
  unsigned int d_b:1;        //default operation size 存取方式 [0]16 位 [1]32 位
  unsigned int unused:1;      // 固定设置成 0
  unsigned int avl:1         //avaliable, 可供系统软件使用 
  unsigned int seg_limit_16_19:4;  // 段长度的最高 4 位 
  unsigned int p:1;         //segment present, [0] 该段的内容不在内存中 
  unsigned int dp1:2;        //Descriptor privilege level, 访问本段所需权限 
  unsigned int s:1;         // 描述项类型 [1] 系统 [0] 代码 / 数据 
  unsigned int type:4        // 段的类型 , 与 S 标志位一起使用 
  unsigned int base_0_23:24;       // 基地址的低 24 位 
  unsigned int seg_limit_0_15:16;     // 段长度的低 16 位

}descriptor;

以这里的位段 type 为例,“: 4 ”表示其宽度为 4 位。整个数据结构的大小为 64 位,即 8 个字节。

       在读写内存单元时, CPU 需要检查段描述符的内容是否和当前操作相一致, CPU 的运行效率极大地降低。为解决这个问题, CPU 在内部设置了段描述符高速缓存,可以看作是对段寄存器的扩充。扩充后的段寄存器分成两部分,一部分是可见的 ( 对程序而言 ) ,还与原来的段寄存器一样,另一部分是不可见的,就是用来放影子描述项的空间,这一部分是专供 CPU 内部使用的。在指令执行过程中,只有段寄存器的值发生改变时,才需要到 GDT 或 LDT 中装入段描述符。如果段寄存器的值不改变,高速缓存 ( 即对段寄存器扩充的那部分 ) 中的段描述符可以被直接引用,这样就避免了到主存中频繁读取段描述符。提高了 CPU 的效率。

       在 80386 的段式内存管理的基础上,如果把每个 段寄存器都指向同一个 描述项,而在该描述项中则将 基地址设成 0, 并将 段长度设成最大这样便形成一个从 0 开始覆盖整个 32 位地址空间的一个整段。由于基地址为 0, 此时的物理地址与逻辑地址相同, CPU 放到地址总线上去的地址就是在指令中给出的地址。这样的地址有别于由“段寄存器 / 位移量”构成的“层次式”地址,所以 Intel 称其为“平面 (Flat) ”地址。 Linux 内核的源代码 ( 更确切地应该是 gcc) 采用平面地址。这里要指出,平面地址的使用并不意味着绕过了段描述表、段寄存器这一整套段式内存管理的机制,而只是段式内存管理的一种使用特例。


以下图解:



内容摘自:《Linux内核源代码情景分析》《Linux内核完全注释》



  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值