C/C++ 内存管理算法与实现--第一章:Intel奔腾CPU构架--保护模式 (翻译连载)

C/C++ 内存管理算法与实现--第一章:Intel奔腾CPU构架--保护模式 (翻译连载)

保护模式

保护模式提供了实模式中所不具备的丰富多彩的内容。Pentium处理器是为保护模式而特别设计的。它内部的执行管道流水线,执行32位指令的效率优于执行16位指令。

电脑在启动时候,Pentium被设计运行在实模式下,是为了便于电脑在启动时候操作系统的启动引导程序的执行。


运行在保护模式下的Intel处理器支持受保护的分段机制,同样也支持分页机制。这意味着地址解析会变得更加复杂。在实模式中,我们只需要在段地址上添加一个偏移地址便获得一个直接与物理内存对应的地址值。在保护模式中,处理器要求在相应的位置加载特定的数据结构。此外段地址和偏移地址对,可能不再直接对应物理地址,好吧,让我们继续下面对内容...

保护模式下的分段机制

对Intel平台上的分段机制理解的最好方法是图片示例,来解释它是如何实现的。一幅好的图例,胜过长篇大论,对于这本问题,尤其如此。所以请认真仔细的看下图1.9,并跟图1.8做下比较。你也可以将图1.9收藏起来,当你想看的候可以随时看到它。

 


图1.9


首先注意到保护模式中,使用了图1.2中所有的完整Pentium寄存器。是的,我们又回到32位寄存器了。同样,段地址寄存器不再存储16位的段地址值。而是保存段选择器。段选择器是一个16位的数据结构,它包含三个字段。它的组成形式如图1.10所示。真正重要的字段是索引(index)字段。索引字段存储了一个描述符表的索引。索引值由0开始。



图1.10

    注释 索引字段在段选择器中不是地址值。它跟你在C语言中访问数据元素时使用的索引类似。处理器取得索引值,并通过内部必要的计算将该索引值与该索引值对应的线性地址匹配。注意我这里说的线性地址(linear         address),不是指物理地址。此时(保护模式下的分段),线性地址和物理地址是一致的,当启用分页机        制以后,它们就不一致了,记住这一点。


描述符表是一个入口数组,数组中每个入口(称之为段描述符)描述对应内存段的相关属性。段描述符中包含它所描述内存段的基地值。32位的偏移地址加上段描述符的基地值用以指定内存字节的内存地址。有两种描述符表:全局描述符表(Global Descriptor Table -- GDT)和局部描述符表(Local Descriptor Table--LDT)。所有的操作系统必须具备一个GDT,但是具备一个可配置多个LDT。通常,如果一个LDT被使用,它将用于标识内段段所属的进程。GDT的基地址保存在GDTR的系统寄存器中。同样,LDT的基地值保存在LDTR寄存器中。当然,还有一些特定的系统指令用于装载这些值(例如,LGDT和LLDT指令)。


    注意:本书中讨论的大部分操作系统都集中于使用GDT而且很少使用LDT(即使有使用LDT的地方)


GDTR的大小为48位。GDTR有一个很明显的特点是,它存储两个不同的值。第一个大小为16位的值,用于保存GDT大小,单位是字节。另一个32位的值,用于存储GDT在物理内存中的线性基地址。如图1.11



图1.11
问题:1.处理器如何映射一个段选择器的索引到一个段描述符上呢

 

答案:处理器从段选择器取得其指定的索引值,将该值乘以8(因为段描述符是64位的缘故,所以该值要为8字节)然后将结果加上由GTDR或者LDTR提供的基地址值。

     注释:当你在看图1.2时,或许对另外两个内存管理寄存器产生了疑惑,其实我并没有忘记它们。它们对于我
   们的讨论话题来说并不如GDTR和LDTR这样重要。IDTR寄存器和IR寄存器是用于管理硬件中断和多任务的。本
   书集中讨论内存管理,所以不会对此类寄存器做更深入的讨论。如果你对它们有兴趣,我建议你去选一本我
   在本章末尾提供的相关Intel的手册资料

之前我提到过的 段描述符存储着它所描述的内存的线性基地址。然而,段描述符同样保存着其他完整的元数据。图1.12更好的展示了在一个段描述符中陈列的内容。在该图中,我将64位的描述符切分为两个32位大小。高阶部分位于低阶部分的顶部。
图1.12
 
在这个段描述符64位值中打包了大量信息。正如你所看到的,有些字段被分开存储在描述符的不同位置。在图1.11中或许有两个字段没有明显标出。首先是SS标志,它标识描述符对应的内存段是系统内存段还是普通的代码/数据段。系统段,或许看到这个名词,你在抓头皮了,系统段是用于提供中断处理和多任务服务的。我不会介入这些话题。
 
    注意:在接下来的某些部分你会看到大量的1-bit的标志。这里预先说明一下,以供后面参考。当bit标志为1的时候,它表示被置位,当bit标志为0的时候,它表示被清除。底层的操作系统代码涵盖大量基于bit的操作。没有其他方法。使用高级语言的工程师们总是轻视这类底层代码的工程师。他们称这些人为 bit-bashers或者 bit-twiddlers。程序员真是很命苦呀。
 
假定SS标志被置位,描述符中4位类型的字段描述内存段的特定属性:
 

表 1.1

Bit

Type类型

Description 描述

11

10

9

8

0

0

0

0

data

read-only

0

0

0

1

data

read-only, accessed

0

0

1

0

data

read-write

0

0

1

1

data

read-write, accessed

0

1

0

0

data

read-only, expand down

0

1

0

1

data

read-only, expand down, accessed

0

1

1

0

data

read-write, expand down

0

1

1

1

data

read-write, expand down, accessed

1

0

0

0

code

execute-only

1

0

0

1

code

execute-only, accessed

1

0

1

0

code

execute-read

1

0

1

1

code

execute-read, accessed

1

1

0

0

code

execute-only, conforming

1

1

0

1

code

execute-only, conforming, accessed

1

1

1

0

code

execute-read, conforming

1

1

1

1

code

execute-read, conforming, accessed

 
已访问内存段是指最近被访问过的内存段,所以这些内存段描述符位于8bit上的标志被置位。‘Expand down’ 段用于创建堆栈,因为这些段支持内存堆栈创建,创建由高端内存向低端内存延伸(故名Expand down段)。‘Conforming code’段允许较低权限的代码段跳转进来,并以允许这些代码以较低的权限在该段内运行。 
具备安全意识到系统工程师会适时的发出警告,谨慎对待当他们处于允许操作系统内存段设置为Conforming code’段时的环境的时。
 
问题:1.好了,我们明白了这些内存段的分类和段描述符中存储的元数据的种类。那么这些内存段是如何被保护的呢
答案: 原来是,段选择器和段描述符包含大部分用于实现保护方案所需的大量信息。处理器充分的利用了这些元数据信息跟踪内存访问的违规行为。例如,段描述符里的大小限定字段用于保存内存段内存引用的起始位置到结束位置。同样段描述符内的类型字段确保该内端段指定为只读不能写入。处理器利用段选择器和段描述符内的权限字段防止非法的可执行代码或者非法数据获得较高权限。 
 
     注释:这点很容易混淆--0x00是最高权限,虽然它是最小的数  
 
权限级别是操作系统用于防止用户应用程序操纵内核文件并危及系统安全。在实模式的讨论中,你见过使实模式系统被破坏和崩溃是如何的简单。我只是在中断向量表上跳了一小段华尔兹并擦除了里面的数据。在保护模式中,这样的威胁,已经被化解了。重要的数据结构和操作系统代码可以在硬件层得到很好的保护。
Intel平台支持四种不同的权限级别(0-3)。另一种说法是Intel支持四种保护环。这些保护环如图1.13所示。这是到目前以来涉及内存保护到结构中极其简洁的一种了。很多年前当Control Data在编写NOSVE操作系统的时候,系统构架师想要15个内存保护环!同时代古怪的事情类似Linux和Windows,它们只实现了两个内存保护换(一个给系统内核,另一个给系统内核以外的所有部分)。他们都没有充分利用Pentium所提供的内存保护功能。
 

图1.13
当一个内存被使用,处理器会执行一系列的检查。这些检查是在内存地址解析到它的实际物理位置中同时进行。因为这些检查和地址解析并发执行在相同时钟周期中,因此不会产生性能问题。将内存管理的任务交由硬件来完成真是再好不过了。如果处理器的某项检查侦测到内存保护的非法操作,处理器会产生一个异常。异常是由处理器发出的一个信号。处理器的各种中断处理功能会捕获并且处理这些异常。详细的过程已经超出了我们的话题。笼统的来说,处理器利用特定的数据结构——类似中断描述符表(IDT),将异常转交给操作系统,由操作系统来决定采取何种处理方式。当操作系统启动时候,它有负责为处理器进行类似IDT的初始化和设置工作。这使得操作系统可以自由的在IDT注册特定的处理程序,当发生非法内存操作的时候,相关的处理程序能够被调用。 当Windows中发生一个内存异常时,一个典型对话框会出现,提示非法内存访问,并且Windows会终止你的程序,如果想知道我所要表达的意思,那就在Windows编译并且运行下面的代码:
 
* --overflow.c-- */
#include<stdio.h>

void main()
{
int array[4];
int i;
for(i=0;i<100;i++)
{
array[i]=i;
printf("set array[%d]=%d\n",i);
}
return;
}
 

上面的代码中有一个明目张胆的数组越界。当你运行这个程序时,程序会崩溃,Windows会显示一个如图1.14中所示的对话框。如果你以前还没有见过这样的对话框,好好看看吧。如果你在Windows上进行某种大量指针相关的程序开发,你迟早会看到它。

图1.14

 

我没有对控制寄存器说太多。跟目前这部分内容最相关的寄存器是是CR0寄存器。我们会在下一部分看到另一对寄存器。CR0寄存器的第一位(最低顺序位)被称为PE标志位(作为保护模式开启的标志)。通过将PE标志位置1,我们将处理器切换至保护模式开启所有我们前面讨论的段保护机制。下面是一个完成这个关键工作的汇编代码的片段

 MOV EAX,CR0
OR AL,1
MOV CR0,EAX
另一个达到同样目的的方法是使用特定目的的SMSWLMSW system 指令: 
 SMSW AX
OR AL,1
LMSW AX
在这里,你已经反复听到很多名词:选择器(selector),描述符(descriptor)等等。如果
这是你第一次听到关于保护模式的内容。你或许还在混淆它们到底是什么。下面是一个是一个简
的概要,有助于你记住这一些列的名词和相互关系:

Cast Member

Purpose

 段选择器
Segment selector

在描述符表中选择一个描述符
Selects a descriptor in a descriptor table

段描述符 
Segment descriptor 

描述一个内存段(元数据信息)
Describes a memory segment (metadata)

描述符表
Descriptor table 

保护一个段描述符数组
Holds an array of segment descriptors

描述符表寄存器 
Descriptor table register

存储描述符表的基地址 
Stores the base address of the descriptor table

 

如果分页还没有被启用,由图1.9中的结构所生成的最终地址同样也是物理地址,该地址值与处理器放置到它的32位地址线的值是相同的。如果启动了分页,情况不会再是这样,引导我们进入下一个部分:保护模式下的分页机制

你可能感兴趣的:(C/C++ 内存管理算法与实现--第一章:Intel奔腾CPU构架--保护模式 (翻译连载))