一、 段寄存器的产生
段寄存器的产生源于Intel 8086 CPU体系结构中数据总线与地址总线的宽度不一致。
段寄存器
数据总线的宽度,也即是ALU(算数逻辑单元)的宽度,平常说一个CPU是“16位”或者“32位”指的就是这个。8086CPU的数据总线是16位。
地址总线的宽度不一定要与ALU的宽度相同。因为ALU的宽度是固定的,它受限于当时的工艺水平,当时只能制造出16位的ALU;但地址总线不一样,它可以设计得更宽。地址总线的宽度如果与ALU相同当然是不错的办法,这样CPU的结构比较均衡,寻址可以在单个指令周期内完成,效率最高;而且从软件的解决来看,一个变量地址的长度可以用整型或者长整型来表示会比较方便。
但是,地址总线的宽度还要受制于需求,因为地址总线的宽度决定了系统可寻址的范围,即可以支持多少内存。如果地址总线太窄的话,可寻址范围会很小。如果地址总线设计为16位的话,可寻址空间是2^16=64KB,这在当时被认为是不够的;Intel最终决定要让8086的地址空间为1M,也就是20位地址总线。
地址总线宽度大于数据总线会带来一些麻烦,ALU无法在单个指令周期里完成对地址数据的运算。有一些容易想到的可行的办法,比如定义一个新的寄存器专门用于存放地址的高4位,但这样增加了计算的复杂性,程序员要增加成倍的汇编代码来操作地址数据而且无法保持兼容性。
Intel想到了一个折中的办法:把内存分段,并设计了4个段寄存器,CS,DS,ES和SS,分别用于指令、数据、其它和堆栈。把内存分为很多段,每一段有一个段基址,当然段基址也是一个20位的内存地址。不过段寄存器仍然是16位的,它的内容代表了段基址的高16位,这个16位的地址后面再加上4个0就构成20位的段基址。而原来的16位地址只是段内的偏移量。这样,一个完整的物理内存地址就由两部分组成,高16位的段基址和低16位的段内偏移量,当然它们有12位是重叠的,它们两部分相加在一起,才构成完整的物理地址。
这种寻址模式也就是“实地址模式”。在8086中,段寄存器还只是一个单纯的16位寄存器,而且操作寄存器的指令也不是特权指令。通过设置段寄存器和段内偏移,程序就可以访问整个物理内存,无安全性可言。
总之一句话,段寄存器的设计是一个权宜之计,现在看来可以说是一个临时性的解决方案,设计它的目的是为了把地址空间从64KB扩展为1MB,仅此而已。但是它的加入却为日后Intel系列芯片的发展带来诸多不便,也为理解i386体系带来困扰。
二、 实现保护模式
到了80386问世的时候,工艺已经有了很大的进步,386的ALU有已经从16位跃升为32位,也就是说,38086是32位的CPU,而且结构也已经比较成熟,接下来的80486一直到Pentium系列虽然速度提高了几个数量级,但并没有质的变化,所以被统称为i386结构。
对于32位的CPU来说,只要地址总线宽度与数据总线宽度相同,就可以寻址2^32=4GB的内存空间,这已经足够用,已经不再需要段寄存器来帮助扩展。但这时Intel已经无法把段寄存器从产品中去掉,因为新的CPU也是产品系列中的一员,根据兼容性的需要,段寄存器必须保留下来。
这时,技术的发展需求Intel在其CPU中实现“保护模式”,用户程序的可访问内存范围必须受到限制,不能再任意地访问内存所有地址。Intel决定利用段寄存器来实现他们的保护模式,把保护模式建立在段寄存器的基础之上。
对于段的描述不再只是一个20位的起始地址,而是全新地定义了“段描述项”。段描述项的结构如下:
每一行是两个字节,总共8个字节,64位。
DES1和DES2分别是一些描述信息,用于描述本段是数据段还是代码段,以及读写权限等等。B0~B31是段的基地址,L0~L19是段的长度。
注意,规定段的长度是非常必要的,如果不限定段长度,“保护”就无从谈起,用户程序的访问至少不能超过段的范围。另外,段长度只有20位,所代表的最大可能长度为2^20=1M,而整个地址空间是2^32=4GB,这样来看,段的长度是不是太短了?其实,在DES1中,有一位用于表示段长度的单位,当它被置1时(一般情况下都是如此),表示长度单位为4KB,这样,一个段的最大可能尺寸就成了1M*4K=4G,与地址空间相稳合。4KB也正是一个内存页的大小,说明段的大小也是向页对齐的。
另外,注意到一个有趣的现象吗?段描述项的结构被设计得不连续,不论是段基地址还是段长度,都被分成了两节表示。这样的设计与80286的过渡有关。上面的段描述项结构去掉第一行后剩下的三行正是286的段描述项。286被设计为24位地址总线,所以段基址是24位,相应地段长是16位。在386的地址总线扩展为32位之后,还必须兼容286产品的设计,所以只好在段描述项上“打补丁”。
在386中,段寄存器还是16位,那么16位的段寄存器如何存放得下64位的段描述项? 段描述项不再由段寄存器直接持有。段描述项存放在内存里,系统中可以有很多个段描述项,这些项连续存放,共同构成一张表,16位的段寄存器里只是含有这张表里的一个索引,但也并不仅是一个简单的序号,而是存储了一种数据结构,这种结构的定义如下:
其中index是段描述表的索引,它指向其中的某一个段描述项。RPL表示权限,00最高,11最低。
还有一个关键的问题,内存中的段描述表的起始地址在哪里?显然光有索引是有不够的。为此,Intel又设计了两个新的寄存器:GDTR(global descriptor table register)和LDTR(local descriptor table register),分别用来存储段描述表的地址。段寄存器中的TI位正是用于指示使用GDTR还是LDTR。
当用户程序要求访问内存时,CPU根据指令的性质确定使用哪个段寄存器,转移指令中的地址在代码段,取数指令中的地址在数据段;根据段寄存器中的索引值,找到段描述项,取得段基址;指令中的地址是段内偏移,与段长比较,确保没有越界;检查权限;把段基址和偏移相加,构成物理地址,取得数据。
新的设计中处处有权限与范围的限制,用户程序只能访问被授权的内存空间,从而实现了保护机制。就这样,在段寄存器的基础上,Intel实现了自己的“保护模式”。
三、 与页式存储管理并存
现代操作系统的发展要求CPU支持页式存储管理。
页式存管本身是与段式存管分立的,两者没有什么关系。但对于Intel来说,同样是由于“段寄存器”这个历史的原因,它必须把页式存管建立在段式存管的基础之上,尽管这从设计的角度来说这是没有道理,也根本没有必要的。
在段式存管中,由程序发出的变量地址经映射(段基址+段内偏移)之后,得到的32位地址就是一个物理地址,是可以直接放到地址总线是去取数的。
在页式存管中,过程也是相似的,由程序发出的变量地址并不是实际的物理地址,而是一个三层的索引结构,这个地址经过一系统的映射之后才可以得到物理地址。
现在对于Intel CPU来说,以上两个映射过程就要先后各做一次。由程序发出的变量地址称为“逻辑地址”,先经过段式映射成为“线性地址”,线性地址再做为页式映射的输入,最后得到“物理地址”。
Linux内核实现了页式存储管理,而且并没有因为两层存管的映射而变得更复杂。Linux更关注页式内存管理,对于段式映射,采用了特殊的方式把它简化。让每个段寄存器都指向同一个段描述项,即只设了一个段,而这个段的基地址为0,段长度设为最大值4G,这个段就与整个物理内存相重合,逻辑地址经映射之后就与线性地址相同,从而把段式存管变成“透明”的。
这,就是Intel处理器中“段寄存器”的故事。