cpu访问内存时,为什么要把内存分成各种段?

cpu访问内存时,为什么要把内存分成各种段?

内存按访问方式来看,其结构就如同上面的长方形带子,地址依次升高。为了解释问题更明白,我们假设还在实模式下,如果读者不清楚什么是实模式也不要紧,这并不影响理解段是什么,故暂且先忽略。

内存是随机读写设备,即访问其内部任何一处,不需要从头开始找,只要直接给出其地址便可。如访问内存0xC00,只要将此地址写入地址总线便可。问题来了,分段是内存访问机制,是给cpu用的访问内存的方式。只有cpu才关注段。那为什么cpu要用段呢,也就是为什么cpu非得将内存分成一段一段的才能访问呢。

说来话长,现实行业中有很多问题都是历史遗留问题,计算机行业也不能例外。这分段是从cpu 8086开始的,限于技术和经济,那时候电脑还是非常昂贵的东西,所以cpu和寄存器等宽度都是16位的,并不是像今天寄存器已经扩展到64位,当然编译器用的最多的还是32位。16位寄存器意味着其可存储的数字范围是2的16次方,即65536字节,64k。那时的计算机没有虚拟地址之说,只有物理地址,访问任何存储单元都直接给出物理地址。

(迷你兔数据恢复minitool具有“删除恢复”、“格式化恢复”、“硬盘恢复”、“深度恢复”、“移动存储设备恢复”五大功能模块,恢复效率高,安全性有保障。)

编译器在编译程序时,肯定要根据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:0x04等等,总之要想访问某个物理地址,只要凑出合适的段基地址和段内偏移地址,其和为该物理地址就行啦。这时估计有人会问这样行不行,0xc05:-1,能这样提问的同学都是求知欲极强的,你可以自己试一下啦。

说了这么多,我想告诉你的是,只要程序分了段,把整个段平移到任何位置后,段内的地址相对于段基址是不变的,无论段基址是多少,只要给出段内偏移地址,cpu就能访问到正确的指令。于是加载用户程序时,只要将整个段的内容复制到新的位置,再将段基址寄存器中的地址改成该地址,程序便可准确无误的运行,因为程序中用的是段内偏移地址,相对于新的段基址,该偏移地址处的内存内容还是一样的。如图所示:

cpu访问内存时,为什么要把内存分成各种段?

所以说,程序分段是首先是为了重定位,我说的是首先,下面还有理由呢。

偏移地址也要存入寄存器,而那时的寄存器是16位的,也就是一个段最多可以访问到64K。而那时的内存再小也有1M呢,这样通过改变段基址的方式,由一个段变为另一个段,就像一个段在内存中飘移,采用这种在内存中来回挪位置的方式来访问到任意内存位置。

所以说,程序分段又是为了将大内存分成可以访问的小段,通过这样变通的方法便能够访问到所有内存了。

但想一想,1M内存是2的20次方,即需要20位的地址才能访问到,如何做到16位寄存器访问20位地址空间呢?

在8086的寻址方式中,有基址寻址,这是用基址寄存器bx或bp来提供偏移地址。如mov [bx], 0x5;这条指令便是将立即数0x5存入ds:bx指向的内存。

大家看,bx寄存器是16位的,它最大只能表示0~0xFFFF的地址空间,即64K,也就是单一的一个寄存器无法表示20位的地址空间——1M。也许有人会说,段基址和段内偏移地址都搞到最大,都为0xFFFF,对不起,即使不溢出的话,其结果也只是由16位变成了17位,即两个n位的数字无论多大,其相加的结果也超不过n+1位,因为即使是两个相同的数相加,其结果相当于乘以2,也就是左移一位而已,依然无法访问20位的地址空间。也许有同学又有好建议了:cpu的寻址方式又不是仅仅这一种,上面的限制是因为寄存器是16位,只要不全部通过寄存器不就行了吗。既然段寄存器必须得用,那就在偏移地址上下功夫,不要把偏移地址写在寄存器里了,把它直接写成20位立即数不就行啦。如mov ax, [0x12345],这样最终的地址是ds+0x12345,肯定是20位,解决啦。不错,这种是直接寻址方式,至少道理上讲得通,这是通过编程技巧来突破这一瓶颈,能想到这一点我觉得非常nice。但是做为一个严谨的cpu,既然宣称支持了通过寄存器来寻址,那就要能够自圆其说才行,不能靠程序员的软实力来克服cpu自身的缺陷。于是,一个大胆的想法出现了。

(迷你兔数据恢复软件minitool具有“删除恢复”、“格式化恢复”、“硬盘恢复”、“深度恢复”、“移动存储设备恢复”五大功能模块,恢复效率高,安全性有保障。)

16位的寄存器最多访问到64k大小的内存。虽然1M内存中可容纳1M/64k=16个最大段,但这只是可以容纳而已,并不是说可以访问到。16位的寄存器超过0xffff后将会回卷到0,又从0重新开始。20位宽度的内存地址空间必然只能由20位宽度的地址来访问。问题又来了,在当时只有16位寄存器的情况下是如何做到访问20位地址空间的呢。

这是cpu设计者在地址处理单元中动了手脚,该地址部件接到“段基址+段内偏移地址”的地址后,自动将段基址乘以16,即左移了4位,然后再和16位的段内偏移地址相加,这下地址变成了20位了吧,行啦,有了20位的地址便可以访问20位的空间,可以在1M空间内自由翱翔了。