x86架构的内存管理机制以复杂著称,这里面有很多历史原因。下面对x86的分段机制和分页机制进行简单介绍。
1、分段机制
分段是一种朴素的内存管理机制,它将内存划分成以起始地址base和长度limit描述的块,这些内存块就称为段。段可以与程序最基本的元素联系起来。例如程序可以简单地分为代码段、数据段和栈,段机制中就有对应的代码段、数据段和栈段。
分段机制由4个基本部分构成:逻辑地址、段选择寄存器、段描述符和段描述符表。其核心思想是:使用段描述符描述段的基地址、长度以及各种属性。当程序使用逻辑地址访问内存的某个部分时,CPU通过逻辑地址中的段选择符索引段描述符表以得到该内存对应的段描述符。并检验程序的访问是否合法,如合法,根据段描述符中的基地址将逻辑地址转换为线性地址。
段选择符:segment selector
段选择符是逻辑地址的一个组成部分,共16位,用于索引段描述表以获得该段对应的段描述符,段描述符由index、TI、TPL组成。各字段的含义如下。
Index:段描述符的索引。
TI:用于指明索引哪个段描述符表。0为GDT,全局描述符表;1为LDT,本地描述符表。
RPL:
段选择符作为逻辑地址的一部分,对程序是可见的。但是通常段描述符的修改和分配由连接器和加载器完成,而不是应用程序本身。为了使CPU快速获得段选择符,x86架构提供了6个段寄存器用于存放当前程序的各个段的段选择符。6个段寄存器分别如下。
CS代码段:存放代码段的段选择符。
DS数据段:存放数据段的段选择符。
SS栈段:存放栈的段选择符。
ES、FS、GS:提供程序自由使用,可以存放额外3个数据段的段选择符。
段描述符:描述某个段的基地址、长度以及各种属性。在这里我们只关心其中的Base段和limit字段。前者描述了该段的基地址,后者描述了该段的长度。
当CPU通过一个逻辑地址的段选择符获得该段对应的段描述符后,会使用描述符中各种属性字段对访问进行检查,一旦访问被确认合法,CPU将段描述符中的32位基地址和逻辑地址中的32位偏移量相加以获得该逻辑地址的线性地址。
为了加速段描述符的访问,x86在段寄存器后增加了一个程序不可见的段描述符寄存器。当寄存器被加载入一个新的段选择符后,CPU自动将该段选择符索引的段描述符加载入这个不可见的符寄存器中,故CPU自需要在更新段寄存器时才索引段描述符表。
段描述符表:
x86架构提供两种段描述符表,它们是:全局段描述符表GDT和本地段描述符表LDT。系统中至少有一个GDT可以被所有进程访问。相对的,系统中可以有一个或多个LDT,可以被某个进程私有,也可以被多个进程共享。GDT仅仅是内存中的一个数据结构,可以把它看做成一个数组,由基地址Base和长度Limit描述。与之相反,LDT是一个段,它需要一个段描述符来描述它。LDT的段描述符存放在GDT中,当系统中有多个LDT时,GDT中必须有对应数量的段描述符。
为了加速对GDT和LDT的访问,x86提供了GDTR寄存器和LDTR寄存器。它们的描述如下。
GDTR:包括一个32位的基地址BASE和一个16位长度LIMIT。
LDTR:结构同段寄存器。
可以使用LGDT/SGDT指令对GDTR进行读取/存储,类似地可以使用LLDT/SLDT对LDTR进行同样的操作。通常在进程切换时,LDTR中的值会被换成新进程对应的LDT的段描述符。
GDTR/LDTR为GDT/LDT提供了基地址,段选择符的TI为确定索引GDT还是LDT。
逻辑地址转换总结:假设程序中某条语句访问数据段,
int a = 100;
int func()
{
int b;
b = a;
}
程序从加载到变量a的逻辑地址转换为线性地址的过程:
1、程序加载
通过该进程LDT的段选择符索引GDT,获得LDT的段描述符,被加载到LDTR寄存器中。该进程的CS、DS、SS被加载入相应的段选择符。同时,CPU根据段选择符的TI字段,索引GDT/LDT获得相应的段描述符并加载入CS、DS、SS对应的不可见的段描述符寄存器。
程序执行到 b = a; 需要从a所在的内存中取值,必需先把a的逻辑地址转换为线性地址。
1,进行必要的属性、访问权限检查。
从DS对应的段描述符寄存器获得该段的基地址。
将变量a的32位偏移量和描述符中的基地址相加,获得变量a的线性地址。