(摘自http://hengch.blog.163.com/blog/static/107800672009028105929795/)推荐对DOS感兴趣的看一下他的博客
在80x86的CPU里,描述符的概念实在是太重要了。
在实模式下,大家都知道物理地址是由段地址和偏移地址两部分组成,其公式如下:
物理地址 = 段地址 × 16 + 偏移地址
或者:物理地址 = 段地址 << 4 + 偏移地址
其结果都是一样的,由于段地址和偏移地址的长度都是16位,所以这种方式能够表达的最大地址为:ffff:ffffH,也就是10ffefH,大致是1088KBytes,由于8086CPU的地址线只有20位,所以在8086上实际的寻址能力仅为1024KBytes,在80286和80386CPU上,通过A20的使用,可以实际寻址到1088KBytes,这个问题在《关于A20 gate》的文章中有过介绍。
从80386开始,CPU的地址总线已经到了32位,但只有在保护模式下才能真正地享受32位的高性能,实际上在保护模式下,CPU也是使用段:偏移地址的方式来寻址的,只是这里面有两点区别,1-偏移地址可以是16位也可以是32位;2-段寄存器中的内容和在实模式下的含义完全不一样,而且已经不是组成物理地址的一部分。显然,偏移地址的这种变化是很容易理解的,无需更多地解释,所以,保护模式和实模式相比,重要的区别就是段寄存器中内容的区别了。
那么,这个段寄存器中到底存的是什么东东呢?它存的是某个描述符在描述附表中的偏移值,这是什么意思呢?一个描述附表中有很多描述符,每个描述符的长度都相同,假如第一个描述符我们编号是0的话,那么第n个描述符的编号就是n-1,这个n-1就是所谓的描述符索引,所谓“偏移”就是从描述符表起始位置起,到我们要用的这个描述符有多少个字节,也就是描述符索引 × 描述符长度,很显然如果知道描述符表的起始地址,再加上段寄存器的值,我们就能找到我们要的这个描述符了。
我们先来把到此为止的问题罗列一下:
1、这个描述符是什么东东?
2、毕竟我们是要通过段:偏移地址的形式得到物理地址,既然这个段代表一个描述符,那么这个描述符和物理地址有什么关系?
3、上面说到的描述符表的起始地址和描述符长度是是什么?因为没有这两个东西还是找不到描述符的。
我们试着来说明这几个问题。
首先,描述符实际上就是一个8字节长的数据结构,它的定义如下:
每一个描述符代表一个分段(有点像实模式下的64K分段),可以看到段基址由Byte2、Byte3、Byte4和Byte7组成,一共32bits,段的最大长度不像在实模式中固定为64K,而是有一个20位的段边界(由Byte0、Byte1和Byte6的低4位组成)来设定,由于偏移地址为32位,所以实际上段的最大长度可以达到4GBytes,段边界的单位可以是字节也可以是4KBytes,这取决于G=0还是G=1(Byte6的bit 7),当G=1时,段边界的单位是4KBytes,相当于低12bits全部为1(4K刚好12bits),加上20位的段边界,刚好为32位,最大可以达到4GBytes;当G=0时,段边界的单位为字节,20位最大为1MBytes。
在描述符的定义中还有一个访问权字节以及AVL、D/B等,介绍起来篇幅很长,以后有机会介绍。
说到这儿,可能大家心里有点眉目了,段寄存器里存放一个描述符在描述符表中的偏移,通过这个偏移可以找到相应的描述符,通过这个描述符中的段基址再加上偏移地址,就组成了物理地址,对了,基本上就是这样,当然不会这么简单,其中还有很多细节,但大致原理就是这样。
前面提到的三个问题,我们已经回答了两个半,就是描述符是什么,怎样计算物理地址,还有描述符的长度是多少。最后一个问题是:描述符表的起始地址在那里?其实这个问题最好回答,在那篇《80386寄存器组成》的文章中提到过一个GDTR的系统表寄存器,这个描述符表的起始地址就存在这个寄存器中(这么说不是很完整,但不影响理解),如果你还要问,描述符表存在哪里?如何把描述符表的起始地址放到GDTR寄存器中?这两个问题也不难回答,描述符表可以存储在内存的任何位置;通过LGDT指令可以把描述符表的起始地址存入GDTR寄存器中。
大家有没有感到奇怪,为什么32位的段基址和20位的段界限在描述符中都不连续存放,这是和Intel 80x86 CPU的发展历史分不开的,最先有保护模式的CPU是80286(现在已经不多见了),这个CPU只有24位的地址线,所以可以看到现在386的描述符中基地址的前24bits是连续存放的,后来在扩充时需要把基地址变成32位,为了和80286兼容,只好分开存放了,段界限也是同样原因造成的,所以在80286上,一个描述符的长度虽然也是8字节长,但它只用到了前6个字节,到了386就全都用上了。
到这里,应该对保护模式下存储器的寻址有了一个大致的了解,CPU首先通过段寄存器中的偏移值,配合GDTR寄存器中的描述符表的起始地址,找到段寄存器表示的描述符,再从描述符中获得段基址,然后再加上偏移地址就得到了物理地址(其中许多细节省略不说)。
大家有没有感到,CPU的寻址过程很累,那么这么累的寻址会不会导致速度变慢呢?实际上,CPU的寻址过程并不像上面描述的这么累,要解释CPU的实际寻址过程,必须要提到描述符高速缓存(Descriptor Cache Register)。
实际上,不管是在实模式还是在保护模式下,CPU都会把一个分段的基地址放在一组隐藏的寄存器中,这组隐藏的寄存器,对程序员是不可见的,程序也是无法直接存取的,但却是实际存在的,这组隐藏的寄存器叫做描述符高速缓存寄存器(Descriptor Cache Registers),当段寄存器的值发生变化时,段的基地址、段的边界以及存取属性(存取权限)都会被重新加载到这个段寄存器对应的高速缓存中,为增强性能,CPU对随后的寻址均会直接从这个高速缓存中计算,而不会去描述符表中提取描述符,实际上,各种CPU中这个高速缓存的内部结构是不同的,以下是几种CPU中高速缓存的结构图:
不管其内部结构是怎样的,但我们可以看到,CPU在把描述符加载进高速缓存时并不是简单的拷贝,而是做了一些适当的处理,比如把原来没有连续存放的段基址和段界限变成了连续的,把原来20bits的段界限根据G值转换成了32bits,所以我们在高速缓存里看不到原来描述符里的G标志就是这个道理,显然,这些处理十分有利于快速地得出实际地址来。
前面我们提到过,CPU内部寄存器GDTR中存储着描述符表的起始地址,细心的读者可能会发现,《80386寄存器组成》一文中提到的GDTR寄存器有48bits,而且分成两部分,一部分是32bits,另一部分是16bits,这是怎么回事呢?实际上GDTR中不仅存着描述符表的起始地址,还存放着描述符表的长度,其中16bits的部分就是描述符表的长度,32bits的部分就是描述符表的起始地址,按照规范,描述符表中最多可以有8192(8K)个描述符,每个长度8个字节,所以最大长度为64K,16bits已经足够了,同理,段寄存器仍然保持16bits长度也是足够的。
不管是在实模式还是在保护模式下,CPU在实际寻址时都会使用这个高速缓存。所不同的是,在实模式下,在段寄存器的值发生变化时,仅仅把段寄存器的值×16(左移4位)放到高速缓存的的基地址位置,段界限和存取权限总是一个固定不变的值(按照Intel的说法,PC机加电后工作在实模式,高速缓存中将被置入缺省值,在实模式下,其中的段界限和存取权限将一直保持不变);而在保护模式下,当段寄存器发生变化时,CPU要从描述符表中加载数据到高速缓存中,实际上,保护模式下CPU在从描述符表中向高速缓存中加载数据时要做大量的保护性检查,大致如下:
- 段寄存器的值不能是0。根据规范,描述符表中的第一个描述符必须是空描述符,所以段寄存器值为0是不合法的。如果为0,产生异常中断13.
- 段寄存器中的值是否大于或等于描述符表的长度(存在GDTR中),如果大于或等于描述符表的长度,产生异常中断13.
- 如果段寄存器是CS,检查描述符表中的段类型是否为代码段,如果不是,产生异常中断13.
- 如果段寄存器CS要求装入的段是代码段,检查描述符表该段是否存在,如果不存在产生异常中断11
- 如果段寄存器CS通过了3、4的检查,还要检查IP是否超越了该段的边界,如果越界,产生异常中断13
- 如果段寄存器CS通过了3、4、5的检查,则把相应的描述符装入高速缓存
- 如果段寄存器不是CS,检查描述符表中的段类型为数据段,如果不是产生异常中断13
- 如果段寄存器不是CS,该段为数据段,检查其是否存在,如果不存在产生异常中断13
- 如果段寄存器不是CS,且通过了7、8检查,则把相应的描述符装入高速缓存
以上过程,不一定很完整,大概就是这样,从中大家应该可以看出所谓保护模式的保护方式,至少有一个感性认识。
说到这里,基本上可以结束了,但是我们所说的描述符表实际上是非常肤浅的,实际上本文说到的描述符表叫做全局描述符表(Global Descriptor Table 简称GDT),还有局部描述符表,中断描述符表等,但其原理大同小异,理解了GDT,其它的也就比较容易了。