0.7 内存访问为什么要分段
按理说咱们应该先看看段是什么,不过了解段是什么之前,先看看内存是什么样子,如图0-2所示。
内存按访问方式来看,其结构就如同上面的长方形带子,地址依次升高。为了解释问题更明白,我们假设还在实模式下,如果读者不清楚什么是实模式也不要紧,这并不影响理解段是什么,故暂且先忽略。
内存是随机读写设备,即访问其内部任何一处,不需要从头开始找,只要直接给出其地址便可。如访问内存0xC00,只要将此地址写入地址总线便可。问题来了,分段是内存访问机制,是给CPU用的访问内存的方式,只有CPU才关注段,那为什么CPU要用段呢,也就是为什么CPU非得将内存分成一段一段的才能访问呢?
说来话长,现实行业中有很多问题都是历史遗留问题,计算机行业也不能例外。分段是从CPU 8086开始的,限于技术和经济,那时候电脑还是非常昂贵的东西,所以CPU和寄存器等宽度都是16位的,并不是像今天这样寄存器已经扩展到64位,当然编译器用的最多的还是32位。16位寄存器意味着其可存储的数字范围是2的16次方,即65536字节,64KB。那时的计算机没有虚拟地址之说,只有物理地址,访问任何存储单元都直接给出物理地址。
编译器在编译程序时,肯定要根据CPU访问内存的规则将代码编译成机器指令,这样编译出来的程序才能在该CPU上运行无误,所以说,在直接以绝对物理地址访问内存的CPU上运行程序,该程序中指令的地址也必须得是绝对物理地址。总之,要想在该硬件上运行,就要遵从该硬件的规则,操作系统和编译器也无一例外。
若加载程序运行,不管其是内核程序,还是用户程序,程序中的地址若都是绝对物理地址,那该程序必须放在内存中固定的地方,于是,两个编译出来地址相同的用户程序还真没法同时运行,只能运行一个。于是伟大的计算机前辈们用分段的方式解决了这一问题,让CPU采用“段基址+段内偏移地址”的方式来访问任意内存。这样的好处是程序可以重定位了,尽管程序指令中给的是绝对物理地址,但终究可以同时运行多个程序了。
什么是重定位呢,简单来说就是将程序中指令的地址改写成另外一个地址,但该地址处的内容还是原地址处的内容。
CPU采用“段基址+段内偏移地址”的形式访问内存,就需要专门提供段基址寄存器,这些是cs、ds、es等。程序中需要用到哪块内存,只要先加载合适的段到段基址寄存器中,再给出相对于该段基址的偏移地址便可,CPU中的地址单元会将这两个地址相加后的结果用于内存访问,送上地址总线。
注意,很多读者都觉得段基址一定得是65536的倍数(16位段基址寄存器的容量),这个真的不用,段基址可以是任意的。这就是段可以重叠的原因。
举个例子,看图0-2,假设段基址为0xC00,要想访问物理内存0xC01,就要将用0xC00:0x01的方式来访问才行。若将段基址改为0xc01,还是访问0xC01,就要用0xC01:0x00的方式来访问。同样,若想访问物理内存0xC04,段基址和段内偏移的组合可以是:0xC01:0x03、0xC02:0x02、0xC00:0xC04等,总之要想访问某个物理地址,只要凑出合适的段基地址和段内偏移地址,其和为该物理地址就行了。这时估计有人会问这样行不行,0xC05:-1,能这样提问的同学都是求知欲极强的,可以自己试一下。
说了这么多,我想告诉你的是只要程序分了段,把整个段平移到任何位置后,段内的地址相对于段基址是不变的,无论段基址是多少,只要给出段内偏移地址,CPU就能访问到正确的指令。于是加载用户程序时,只要将整个段的内容复制到新的位置,再将段基址寄存器中的地址改成该地址,程序便可准确无误地运行,因为程序中用的是段内偏移地址,相对于新的段基址,该偏移地址处的内存内容还是一样的,如图0-3所示。
所以说,程序分段首先是为了重定位,我说的是首先,下面还有其他理由呢。
偏移地址也要存入寄存器,而那时的寄存器是16位的,也就是一个段最多可以访问到64KB。而那时的内存再小也有1MB,改变段基址,由一个段变为另一个段,就像一个段在内存中飘移,采用这种在内存中来回挪位置的方式可以访问到任意内存位置。
所以说,程序分段又是为了将大内存分成可以访问的小段,通过这样变通的方法便能够访问到所有内存了。
但想一想,1M是2的20次方,1MB内存需要20位的地址才能访问到,如何做到用16位寄存器访问20位地址空间呢?
在8086的寻址方式中,有基址寻址,这是用基址寄存器bx或bp来提供偏移地址的,如“mov [bx],0x5;”指令便是将立即数0x5存入ds:bx指向的内存。
大家看,bx寄存器是16位的,它最大只能表示0~0xFFFF的地址空间,即64KB,也就是单一的一个寄存器无法表示20位的地址空间——1MB。也许有人会说,段基址和段内偏移地址都搞到最大,都为0xFFFF,对不起,即使不溢出的话,其结果也只是由16位变成了17位,即两个n位的数字无论多大,其相加的结果也超不过n+1位,因为即使是两个相同的数相加,其结果相当于乘以2,也就是左移一位而已,依然无法访问20位的地址空间。也许读者又有好建议了:CPU的寻址方式又不是仅仅这一种,上面的限制是因为寄存器是16位,只要不全部通过寄存器不就行了吗。既然段寄存器必须得用,那就在偏移地址上下功夫,不要把偏移地址写在寄存器里了,把它直接写成20位立即数不就行啦。例如mov ax,[0x12345],这样最终的地址是ds+0x12345,肯定是20位,解决啦。不错,这种是直接寻址方式,至少道理上讲得通,这是通过编程技巧来突破这一瓶颈的,能想到这一点我觉得非常nice。但是作为一个严谨的CPU,既然宣称支持了通过寄存器来寻址,那就要能够自圆其说才行,不能靠程序员的软实力来克服CPU自身的缺陷。于是,一个大胆的想法出现了。
16位的寄存器最多访问到64KB大小的内存。虽然1MB内存中可容纳1MB/64KB=16个最大段,但这只是可以容纳而已,并不是说可以访问到。16位的寄存器超过0xffff后将会回卷到0,又从0重新开始。20位宽度的内存地址空间必然只能由20位宽度的地址来访问。问题又来了,在当时只有16位寄存器的情况下是如何做到访问20位地址空间的呢?
这是因为CPU设计者在地址处理单元中动了手脚,该地址部件接到“段基址+段内偏移地址”的地址后,自动将段基址乘以16,即左移了4位,然后再和16位的段内偏移地址相加,这下地址变成了20位了吧,行啦,有了20位的地址便可以访问20位的空间,可以在1MB空间内自由翱翔了。