逻辑地址--------------》线性地址------------》 物理地址
分段 分页
GDT是[gobal (segment) descriptor table]的缩写,它保存所有segment的信息
内存管理时,不让多进程的程序出现内存冲突的一解决方案是Segmentation。4GB的内存可以任何分割,每块的初始地址都是0。另外还有一种复杂的内存管理方案,既Paging,目前主流的操作系统都是采用这种方式。本文的OS为了实现简单,只采用Segmentation方案。
我们规定1个Segmentation的信息有:
l Size
l 初始地址
l 属性(读写权限等)。
每个Segementation信息占8Byte,既64bit。我们把所有的Segment编号,并把编号保存在CPU内的16位segment register里。Segment register可以处理0到8191个segment,本来16位的register应该可以处理0到65535个segment,但是因为register的低3为不能使用,所以最多只到8191。为了保存8192个segment(每个8bytes),需要64KB的容量,CPU是无法保存这么多数据的,因此需要内存的帮助。GDT是[gobal (segment) descriptor table]的缩写,它保存所有segment的信息。Segment的初始地址和有效设定个数保存在CPU的GDTR寄存器内。另外一个IDT是[interrupt descriptor table]的缩写,用来处理一些内部或外部的中断。例如键盘,鼠标,软驱,硬盘,CDROM,网卡等。IDT可以处理0到255编号的中断,例如当123号中断发生时,调用XXX函数。IDT的信息和GDT相似,也是每个8bytes。本系统的GDT分配在内存的0x27000到0x27ffff。IDT是0x26f800到0x26ffff,占2KB。
我们将Segment定义为:
struct SEGMENT_DESCRIPTOR {
short limit_low, base_low;
char base_mid, access_right;
char limit_high, base_high;
};
Segment信息的初始地址由32bit组成,成为Base地址,分为3个部分,low(2byte),mid(1byte),high(1byte),如此分割是为了兼容80286 CPU。
Segment信息的大小成为Limit,Limit最大可以为4GB,当然是在32位机。这样就需要4Byte,和Base地址一共需要8个byte,但这样就没有保存属性的位置。为了留给属性空间,只能给Limit 20bit,最大能表示1MB,当然这样是不能满足目前主流的32bit机的。Intel为了解决这个问题,规定了在属性里留有1位称为G bit(G表示granularity,粒度),limit的单位不是Byte,而是Page,PC的CPU的1个Page表示4KB。这样4KB*1MB=4GB。为了能表示20bit的Limit,我们使用2个Byte。其中多出的4bit用来表示属性。
Segment的属性由Limit多出的4bit加上剩下的8个bit表示,也称访问权限。访问权限的构成:
XXXX0000XXXXXXXX(X代表0或1)
上4bit是386以后使用的扩张访问属性,由[GD00]构成,G表示上面提到的粒度,D表示Segment mode,1表示32位机,0表示16位机。下8bit继承80286时代的Segement属性。8bit的属性内容非常多,在这里,我们常用的有如下几个:
00000000(0x00):未使用的Descriptor table
10010010(0x92):系统专用,可读写,不能执行
10011010(0x9a):系统专用,可执行,可读,不可写
11110010(0xf2):应用程序使用,可读写,不可执行。
11111010(0xfa):应用程序可用,可执行,可读,不可写。
属性规定了系统和应用程序使用的读写执行权限。系统模式称为Ring0,应用程序模式称为Ring3,中间的Ring1和Ring2模式是系统服务。Ring0管理Ring3,例如,Ring3的应用程序在请求Load LGDT时,操作系统将否决该请求,以保证系统的安全性。下面是新增加的源代码文件。
1. What is GDT
在Protected Mode下,一个重要的必不可少的数据结构就是GDT(Global Descriptor Table)。
为什么要有GDT?我们首先考虑一下在Real Mode下的编程模型:
在Real Mode下,我们对一个内存地址的访问是通过Segment:Offset的方式来进行的,其中Segment是一个段的Base Address,一个Segment的最大长度是64 KB,这是16-bit系统所能表示的最大长度。而Offset则是相对于此Segment Base Address的偏移量。Base Address+Offset就是一个内存绝对地址。由此,我们可以看出,一个段具备两个因素:Base Address和Limit(段的最大长度),而对一个内存地址的访问,则是需要指出:使用哪个段?以及相对于这个段Base Address的Offset,这个Offset应该小于此段的Limit。当然对于16-bit系统,Limit不要指定,默认为最大长度64KB,而 16-bit的Offset也永远不可能大于此Limit。我们在实际编程的时候,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定Segment,CPU将段积存器中的数值向左偏移4-bit,放到20-bit的地址线上就成为20-bit的Base Address。
到了Protected Mode,内存的管理模式分为两种,段模式和页模式,其中页模式也是基于段模式的。也就是说,Protected Mode的内存管理模式事实上是:纯段模式和段页式。进一步说,段模式是必不可少的,而页模式则是可选的——如果使用页模式,则是段页式;否则这是纯段模式。
既然是这样,我们就先不去考虑页模式。对于段模式来讲,访问一个内存地址仍然使用Segment:Offset的方式,这是很自然的。由于 Protected Mode运行在32-bit系统上,那么Segment的两个因素:Base Address和Limit也都是32位的。IA-32允许将一个段的Base Address设为32-bit所能表示的任何值(Limit则可以被设为32-bit所能表示的,以2^12为倍数的任何指),而不象Real Mode下,一个段的Base Address只能是16的倍数(因为其低4-bit是通过左移运算得来的,只能为0,从而达到使用16-bit段寄存器表示20-bit Base Address的目的),而一个段的Limit只能为固定值64 KB。另外,Protected Mode,顾名思义,又为段模式提供了保护机制,也就说一个段的描述符需要规定对自身的访问权限(Access)。所以,在Protected Mode下,对一个段的描述则包括3方面因素:[Base Address, Limit, Access],它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符。这种情况下,如果我们直接通过一个64-bit段描述符来引用一个段的时候,就必须使用一个64-bit长的段积存器装入这个段描述符。但Intel为了保持向后兼容,将段积存器仍然规定为16-bit(尽管每个段积存器事实上有一个64-bit长的不可见部分,但对于程序员来说,段积存器就是16-bit的),那么很明显,我们无法通过16-bit长度的段积存器来直接引用64-bit的段描述符。
怎么办?解决的方法就是把这些长度为64-bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用(事实上,是将段寄存器中的高13 -bit的内容作为索引)。这个全局的数组就是GDT。事实上,在GDT中存放的不仅仅是段描述符,还有其它描述符,它们都是64-bit长,我们随后再讨论。
GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此积存器,从此以后,CPU就根据此积存器中的内容作为GDT的入口来访问GDT了。
GDT是Protected Mode所必须的数据结构,也是唯一的——不应该,也不可能有多个。另外,正象它的名字(Global Descriptor Table)所揭示的,它是全局可见的,对任何一个任务而言都是这样。
除了GDT之外,IA-32还允许程序员构建与GDT类似的数据结构,它们被称作LDT(Local Descriptor Table),但与GDT不同的是,LDT在系统中可以存在多个,并且从LDT的名字可以得知,LDT不是全局可见的,它们只对引用它们的任务可见,每个任务最多可以拥有一个LDT。另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。
IA-32为LDT的入口地址也提供了一个寄存器LDTR,因为在任何时刻只能有一个任务在运行,所以LDT寄存器全局也只需要有一个。如果一个任务拥有自身的LDT,那么当它需要引用自身的LDT时,它需要通过LLDT将其LDT的段描述符装入此寄存器。LLDT指令与LGDT指令不同的时,LGDT指令的操作数是一个32-bit的内存地址,这个内存地址处存放的是一个32-bit GDT的入口地址,以及16-bit的GDT Limit。而LLDT指令的操作数是一个16-bit的选择子,这个选择子主要内容是:被装入的LDT的段描述符在GDT中的索引值——这一点和刚才所讨论的通过段积存器引用段的模式是一样的。
《自己动手写操作系统》是本很有意思的书。不过于渊前辈对某些内容没有给出更多的解释,让我们这些新米很难上手。 书中第3章介绍保护模式的时候,题到了GDT这个东东。在这里给出一个更浅显的介绍。 在介绍GDT这个概念前,现介绍一下segmentation这个概念。 大家还记得ORG这个汇编命令吧。书的开篇,给出的boot.asm代码的第一行就是: ORG 0x7c00 这个ORG就是告诉CPU,程序被载入内存的0x7c00这个地方了。也就是说CPU从这个内存地址开始读取指令就OK了。 但是,现在的CPU同时执行好几个程序是很正常的事情。那么不同的程序使用的内存就有可能重叠,这样载入程序的话就会发生冲突。解决方法之一就是把把内存分成若干个段(segmentation)。每段放一个程序,这样每个程序都可以声明ORG 0了。很方便。 (另一个解决方法是paging,这个以后有机会在谈。) 为了能使用segmentation, 每个段(segment)都需要清楚的表明以下信息: - 段(segment)的大小。 - 段在内存中的开始位置。 - 段的管理属性。比如只读,系统专用等等 CPU用8bytes(=64bits)来表示这些信息(叫做descriptor)。但是,CPU用来指定段的register只有16bits。就算是32bit的CPU,这个register也是16bits的。那怎么办呢? 方法就是每个segment给一个"编号"(正式叫做segment selector),然后在把这个编号放在register里就好了。剩下的工作就是让编号和segment一一对应起来。 大概的感觉是这样的: 1号segment的大小是4GB,从内存的0000开始。(也就是指整个内存了。) 2号segment的大小是xxKB,从内存的xxxx开始。。。。 segment的编号为0~8191。 刚才说了,segment register是16bits,按理说应该可以存放2**16 = 65536个编号。但是由于CPU设计式样的问题,最低3bits不能用,也就是只有13bits可用,刚好是2**13=8192个。 每个segment的描述需要8bytes,一共就需要8192*8=64KB。CPU里可没有这么大的空间。所以就放在内存里好了。这个64KB(可以小于这个值),记录了分段信息的数据结构就叫做GDT -- Global (segment) Descriptor Table. 我不知道这个东东的正式翻译是什么,直译过来就是一个存放了描述分段信息的表。 这次介绍了GDT的概念和这个东东是怎么来的,干什么用的。 下次介绍segment descriptor的数据结构,和如何使用