首页 百科知识 的分段机制

的分段机制

时间:2022-10-09 百科知识 版权反馈
【摘要】:从逻辑地址到线性地址的转换由I386分段机制管理。段的类型是由一个4位的Type字段表示的。Linux对这个模型稍微进行了修改,以一种受限的方法来使用这种分段模型。这使Linux所用的段描述符数量受限,从而可将所有描述符都保存在GDT之中。因此,Linux内核的设计并没有全部采用Intel所提供的段方案,仅仅有限度地使用了分段机制。这不仅简化了Linux内核的设计,而且为把Linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制。

4.1.1 I386的分段机制

从逻辑地址线性地址的转换由I386分段机制管理。段寄存器CS、DS、ES、SS、FS或GS标识一个段。这些段寄存器作为段选择器,用来选择该段的描述符。在保护模式下,80386虚地址空间可达16K个段,每段大小可变,最大达4GB。这种分段单元背后的基本思想是将内存分段管理,每个段就是自己的地址空间。

分段地址还包括两个组件——段选择器(segment selector)和段内偏移量(offset into the segment)。段选择器指定了要使用的段(即基址和长度值),而段内偏移量组件则指定了实际内存位置相对于基址的偏移量。实际内存位置的物理地址就是这个基址值与偏移量之和。如果偏移量超过了段的长度,系统就会生成一个保护违例错误。

每个段都有一个16位的字段,称为段标识符(segment identifier)或段选择器(segment selector)。I386硬件包括几个寄存器,称为段寄存器(segment register),段选择器保存于其中。这些寄存器为CS(代码段)、DS(数据段)和SS(堆栈段)。每个段标识符都代表一个使用64位(8个字节)的段描述符(segment descriptor)表示的段。这些段描述符可以存储在一个GDT(全局描述符表,global descriptor table)中,也可以存储在一个LDT(局部描述符表,local descriptor table)中。

每次将段选择器加载到段寄存器中时,对应的段描述符都会从内存加载到相匹配的CPU寄存器中。每个段描述符表示内存中的一个段。这些都存储到LDT或GDT中。

段描述符条目中包含一个指针和一个20位的值(Limit字段),前者指向由Base字段表示的相关段中的第一个字节,后者表示内存中段的大小。其他某些字段还包含一些特殊属性,例如优先级和段的类型(CS或DS)。段的类型是由一个4位的Type字段表示的。

段选择器包含以下内容:

(1)一个13位的索引,用来标识GDT或LDT中包含的对应段描述符条目

(2)TI (Table Indicator),标志指定段描述符是在GDT中还是在LDT中,如果该值是0,段描述符就在GDT中;如果该值是1,段描述符就在LDT中。

(3)RPL (Request Privilege Level),定义了在将对应的段选择器加载到段寄存器中时CPU的当前特权级别。

描述符(Descriptor),就是描述段的属性的一个8字节存储单元。在实模式下,段的属性不外乎是代码段、堆栈段、数据段、段的起始地址、段的长度,等等,而在保护模式下则复杂一些。通用的段描述符的结构如图4-1所示。

img13

图4-1 段描述符的一般格式

第5个字节是存取权限字节,其中第7位P位(Present)是存在位,表示段描述符描述的这个段是否在内存中,如果在内存中,P=1;如果不在内存中,P=0。第6,第5位是DPL(Descriptor Privilege Level),就是描述符特权级,其值为0~3,用来确定这个段的特权级即保护等级。第4位是S位(System)表示这个段是系统段还是用户段,如果S=0,则为系统段,如果S=1,则为用户程序的代码段、数据段或堆栈段,系统段与用户段有很大的不同。第3,第2,第1位是类型位,第3位为E位,表示段是否可执行,当E=0时,为数据段描述符,这时的第2位ED表示扩展方向,当ED=0时,为向地址增大的方向扩展,这时存取数据段中的数据的偏移量必须小于等于段界限,当ED=1时,表示向地址减少的方向扩展,这时偏移量必须大于界限。当表示数据段时,第1位(W)是可写位,当W=0时,数据段不能写,W=1时,数据段可写入。在I386中,堆栈段也被看成数据段,因为它本质上就是特殊的数据段。当描述堆栈段时,ED=0,W=1,即堆栈段朝地址增大的方向扩展。当段为代码段时,第3位E=1,这时第2位为一致位(C)。当C=1时,如果当前特权级低于描述符特权级,并且当前特权级保持不变,那么代码段只能执行。所谓当前特权级(Current Privilege Level),就是当前正在执行的任务的特权级。第1位为可读位R,当R=0时,代码段不能读,当R=1时可读。第0位A位是访问位,用于请求分段不分页的系统中,每当该段被访问时,将A置1。对于分页系统,则A被忽略未用。

第6个字节的G位是粒度位,当G=0时,段长表示段格式的字节长度,即一个段最长可达1M字节。当G=1时,段长表示段以4K字节为一页的页数目,即一个段最长可达1M×4K=4G字节。D/B位表示缺省操作数的大小,如果为0,操作数为16位,如果为1,操作数为32位。AVL只能由系统软件使用,为了与将来的处理器兼容设置为0。

由于一个段描述符的大小是8个字节,因此它在GDT或LDT中的相对地址可以这样计算:段选择器的高13位乘以8。例如,如果GDT存储在地址0x00020000处,而段选择器的Index域是2,那么对应的段描述符的地址就等于(2*8) + 0x00020000。GDT中可以存储的段描述符的总数等于(2^13 - 1),即8191。

Linux对这个模型稍微进行了修改,以一种受限的方法来使用这种分段模型(主要是出于兼容性方面的考虑)。

在Linux中,所有的段寄存器都指向相同的段地址范围——换言之,每个段寄存器都使用相同的线性地址,段寄存器指向相同的地址集。这使Linux所用的段描述符数量受限,从而可将所有描述符都保存在GDT之中。这种模型有两个优点:

当所有的进程都使用相同的段寄存器值时(当它们共享相同的线性地址空间时),内存管理更为简单。

在大部分架构上都可以实现可移植性。某些RISC处理器也可通过这种受限的方式支持分段。

Linux使用以下段描述符:内核代码段,内核数据段,用户代码段,用户数据段,TSS段,默认LDT段。

Intel微处理器的段机制是从8086开始提出的,那时引入的段机制解决了从CPU内部16位地址到20位实地址的转换。为了保持这种兼容性,I386仍然使用段机制,但比以前复杂得多。因此,Linux内核的设计并没有全部采用Intel所提供的段方案,仅仅有限度地使用了分段机制。这不仅简化了Linux内核的设计,而且为把Linux移植到其他平台创造了条件,因为很多RISC处理器并不支持段机制。

从2.2版开始,Linux让所有的进程(或叫任务)都使用相同的逻辑地址空间,因此就没有必要使用局部描述符表LDT。但内核中也用到LDT,那只是在VM86模式中运行Wine,也就是说在Linux上模拟运行Winodws软件或DOS软件的程序时才使用。在I386上任意给出的地址都是一个虚拟地址,即任意一个地址都是通过“选择符:偏移量”的方式给出的,这是段机制存访问模式的基本特点。所以在I386上设计操作系统时无法回避使用段机制。一个虚拟地址最终会通过“段基地址+偏移量”的方式转化为一个线性地址。但是,由于绝大多数硬件平台都不支持段机制,只支持分页机制,所以为了让Linux具有更好的可移植性,我们需要去掉段机制而只使用分页机制。

但I386规定段机制是不可禁止的,因此不可能绕过它直接给出线性地址空间的地址。为此,Linux的设计人员干脆让段的基地址为0,而段的界限为4GB,这时任意给出一个偏移量,则等式为“0+偏移量=线性地址”,也就是说“偏移量=线性地址”。另外由于段机制规定“偏移量〈4GB”,所以偏移量的范围为0H~FFFFFFFFH,这恰好是线性地址空间范围,也就是说虚拟地址直接映射到了线性地址,我们以后所提到的虚拟地址和线性地址指的也就是同一地址。Linux在没有回避段机制的情况下巧妙地把段机制给绕过去了。

另外,由于I386段机制还规定,必须为代码段和数据段创建不同的段,所以Linux必须为代码段和数据段分别创建一个基地址为0,段界限为4GB的段描述符。不仅如此,由于Linux内核运行在特权级0,而用户程序运行在特权级3,根据I386的段保护机制规定特权级3的程序是无法访问特权级为0的段的,所以Linux必须为内核和用户程序分别创建其代码段和数据段。这就意味着Linux必须创建4个段描述符——特权级0的代码段和数据段,特权级3的代码段和数据段。

Linux在启动的过程中设置了段寄存器的值和全局描述符表GDT的内容,段的定义在include/asm-i386/segment.h中:

#define __KERNEL_CS 0x10 /*内核代码段,index=2,TI=0,RPL=0*/

#define __KERNEL_DS 0x18 /*内核数据段,index=3,TI=0,RPL=0*/

#define __USER_CS 0x23 /*用户代码段,index=4,TI=0,RPL=3*/

#define __USER_DS 0x2B /*用户数据段,index=5,TI=0,RPL=3*/

从定义看出,没有定义堆栈段,实际上,Linux内核不区分数据段和堆栈段,这也体现了Linux内核尽量减少段的使用。因为没有使用LDT,因此,TI=0,并把这4个段都放在GDT中,index就是某个段在GDT表中的下标。内核代码段和数据段具有最高特权,因此其RPL为0,而用户代码段和数据段具有最低特权,因此其RPL为3。可以看出,Linux内核再次简化了特权级的使用,使用了两个特权级而不是4个。

所有进程共享默认LDT段。默认情况下,其中包含一个空的段描述符。这个默认LDT段描述符存储在GDT中。Linux所生成的LDT的大小是24个字节,默认有3个条目:

LDT[0]=空

LDT[1]=用户代码段

LDT[2]=用户数据/堆栈段描述符

GDT中可存储的条目总数可通过以下公式确定:

GDT可以保存的条目数8192。在这8192个段描述符中,Linux要使用6个段描述符,另外还有4个描述符将用于APM特性(高级电源管理特性),在GDT中还有4个条目保留未用。因此,GDT中的条目数等于8192-14,也就是8180。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈