cpu中本来是没有实模式这一称呼的,是因为有了保护模式后,为了将老的模式区别开来,所以称老的模式为实模式。这情况就像所有同学坐在同一个教室里,本来没有老同学这一概念,但某天老师领着一个陌生人进入教室并和大家宣布:“这是新转到我们班的韩梅梅,大家欢迎新同学”。得,无形之中,大伙儿就成了老同学。
实模式的“实”体现在:程序中用到的地址都是真实的物理地址,“段基址:段内偏移”产生的逻辑地址就是物理地址,也就是程序员看到的完全是真实的内存。
不过要说实模式,咱们还得从cpu的发展说起,任何事物发展到今天,都是有一段“合理”的过程,了解这一过程是怎么来的,有助于理解它今天的形态。
不知道各位同学当初学习汇编语言时有没有这样疑问:“老师都是拿8086型号的cpu举例,为什么不拿最新型号的呢?用那么古老的cpu讲解知识,是否已经落伍太久了,我们学习的知识到社会上能用吗?”我记得当初学习汇编时,那时的cpu都是奔腾2.8了。我带着这样的疑问请教了老师,老师回答我说:“8086是intel历史上第一个x86的cpu,也就是自那以后的cpu称为286、386、486、586…即使是现在的奔腾也是属于x86体系,道理是不变的,而且,用最简单的8086 cpu学习,这才更容易理解和看透cpu运行机制。”一番话彻底打消了我的疑虑,自那以后我才理解x86中的x原来是个变量^_^,它是指代intel所有86系列的产品。
在8086之前的cpu是什么样呢?为什么8086就可以称为cpu界的里程碑呢?原因是这样的,在它之前的cpu前辈们,对内存的访问比8086还要“实诚”,它们没有段的概念,程序中要访问内存,需要把地址写死,也就是所谓的“硬编码”,这其实很麻烦的,首先程序无法重定位,必须加载到内存中固定的位置,如果在此位置有其它程序在用,得,您先睡会,等它运行完成后我叫您。您看,得等人家运行完了腾出内存后才轮得到自己,可见程序对地址的依赖性之强。当可用内存很多但却因为某一个字节的内存被占用而让后来的程序等很久,这是很平常的事。有些开发人员等不及了,干脆把程序中的地址改成别的吧,重新编译后发现还是有某个地址被占用,还是没法上cpu运行,怎么办?再改地址…所以我估计那时的开发人员脾气都会很差,这人脾气差就容易伤肝,肝火一旺就会两鬓斑白,所以IT工程师还是很值得体恤的。
看着越来越多的程序员两鬓斑白,intel早期的工程师难以承受内心的自责,不顾自己的满头白发,熬了无数通宵之后,终于发明了“段”,即cpu访问内存用“段+偏移”的形式。这就是前面曾经讲解过访问内存用“段基址:段内偏移地址”的策略,它就是首次在8086上出现的。自那之后的cpu都是用这类思想访问内存,只是在形式上有所小改动,难怪8086如此极富盛名了。为了支持段机制,cpu中新增了段寄存器,如cs、ds、es等。
8086的地址总线是20位宽,也就是其寻址范围是2的20次方=1M。但其内部寄存器都是16位的,若用单一寄存器来寻址的话,只能访问到2的16次方等于64K的空间。
由于地址线位宽和寄存器位宽是没有必然联系的,所以大家不要觉得为什么寄存器不是20位的?这样通过寄存器寻址就能访问到1M空间了,显得多“配套”。
如8086的多种寻址方式中,有一种是基址寻址,这是用基址寄存器bx或bp来提供偏移地址。如mov [bx], 0x5;这条指令便是将立即数0x5存入ds:bx指向的内存。
大家看,bx寄存器是16位的,它最大只能表示0xFFFF的地址,也就是单一的一个寄存器无法表示20位的地址空间:1M。也许有人会说,段基址和段内偏移地址都搞到最大,都为0xFFFF。对不起,方案不成立,首先说会溢出,结果是0xFFFE,地址不增反小了个1。即使不溢出的话,其结果也只是由16位变成了17位,即两个n位的数字无论多大,其相加的结果也超不过n+1位,道理很简单,即使两个数都是n位能表示的最大数,两个相同的数相加,相当于乘以2,也就是数值上等于左移一位而已。依然无法访问20位的地址空间。也许有同学又有好建议了:cpu的寻址方式又不是仅仅这一种,上面的限制是因为寄存器是16位,只要不全部通过寄存器寻址不就行了吗。段寄存器也是寄存器,同样也是16位,既然它必须得用,那就在偏移地址上下功夫,不要把偏移地址写在寄存器里了,把它直接写成20位立即数不就行啦。如mov ax, [0x12345],这样最终的地址是ds+0x12345,肯定是20位,解决啦。不错,这种是直接寻址方式,至少道理上讲得通,这是通过编程技巧来突破这一瓶颈,能想到这一点我觉得非常nice。但是做为一个严谨的cpu,既然宣称支持通过寄存器来寻址,那就要能够自圆其说才行,不能靠程序员的软实力来克服cpu自身的缺陷。于是,一个大胆的想法出现了。
为了让16位的寄存器寻址能够访问20位的地址空间(注意啦亲,我这里一直说的是通过寄存器寻址,因为只有通过16位的寄存器去寻址才会受到16位的限制),cpu工程师定位到根本瓶颈是在段寄存器,它要是能提供20位的段地址,哪怕偏移地址是1也照样可以访问到内存的各个角落。于是,通过先把16位的段基址左移4位后变成20位,再加段内偏移地址,这样便形成了20位地址,只要保证了段基址是20位的,偏移地址是多少位都不关心了,从而突破了16位寄存器做为偏移地址而无法访问1M空间的限制。
有了20位地址便能访问到20位的空间,虽然解决了一个大问题,但是引入了一个小问题。还拿0xFFFF来说,现在能访问的最大的地址是0xFFFF:0xFFFF,经过左移段基址4位后得到的最大地址是:0xFFFF*16+0xFFFF=0xFFFF0+0xFFFF=0xFFFFF+0xFFF0=1M+16*4k-16-1=0x10FFEF。这公式有点晕是吗?其实最后结果是0x10FFEF是最重要的,前面的推算就是想告诉大家,按照新方法获取地址,可以得到的最大地址是1M+64K-16字节,因为这是空间范围,所以要减去1得到地址范围。
大家看到了,当初费了好大周折才搞定了能够访问20位地址空间,现在反而有点过了,过头的原因是段基址为0xFFFF0,偏移地址应该小于等于F就对啦,而这个偏移地址却是0xFFFF,超出了0xFFF0的空间,也就是多出来的64K-16字节,这部分内存就是传说中的高端内存区HMA(High Memory Area)。可是这部分内存不存在,怎么处理呢。
答案说出来吓你一跳:不用处理,哈哈。您想,8086一共就20条地址,地址线是从0开始的,即A0~A19,所以其地址空间才是1M的啊。内存地址0xFFFFF+是要用到A20地址线,可是8086它没有啊,只能接收20位长的地址。所以由于超过了20位而产生的进位,就给丢掉了。其作用相当于把地址对1M取模了。举例,如0xFFFFF+2,理论上是变成了0x100001。但由于只能容纳20位长的数据,所以最终结果是0x00001。这是地址回卷的效果:即超过最大范围后,从0重新开始计数。回卷英文称为wrap-around,示意如图
这就引出了从实模式到保护模式要打开A20地址线的问题,不过这部分在讲保护模式时咱们再说。兄弟们下次再来玩儿哟。