计算机组成
3 指令系统体系结构
3.2 x86体系结构
X86是商业上最为成功,影响力最大的一种体系结构。但从技术的角度看,它又存在着很多的问题,那我们就来一起分析X86这种体系结构的特点。
要探讨x86体系结构,我们就得从8086开始说起。8086是英特尔在1978年推出的一款16位的微处理器。
它内部的通用寄存器为16位,对外有16根数据线和20根地址线,因此可以访问的存储单元数量为2的20次方,也就是1MByte。而cpu发到存储器的地址,我们称为物理地址,8086的物理地址采用了段加偏移的方式,关于这一点我们在后面进行解释。8086作为微处理器,在一个芯片上集成了原先由多个部件组成的CPU的功能,因此可以以低廉的价格,很小的体积和功耗,提供相对不错的性能,这也为个人计算机的发展奠定了基础。
1981年IBM就推出了自己的个人计算机pc5150。它采用了8086的简化版本8088,这是一款八位宽的微处理器,此后个人计算机的市场迅速增长,反过来也推动了x86微处理器的发展。
1982年英特尔推出了80286,由于应用程序对内存空间的需求越来越大,286将地址总线扩展到了24位,可以寻址16兆的内存空间,而且更多的任务也要求更高效的管理模式,286引入了保护模式,虽然这个机制还不完善,但是一个非常重要的进步。为了兼容8086,286仍然保留了8086的工作模式,这就被称实模式,而兼容为之前处理器开发的软件成为x86能够成功的关键因素之一。
实模式又称为实地址模式。运行在实模式下的x86处理器就像是一个更快的8086,而且现在所有x86处理器在加电或复位后首先进入实模式。在实模式中会运行系统初始化程序,为后面进入保护模式做好准备。现在的个人计算机在启动之后,cpu首先会通过主板上的芯片组,从BIOS芯片中取出指令进行执行,这段程序就是运行在实模式下的。
那即使286将地址总线扩大到了24位,很快也无法满足应用程序的需求,所以英特尔在1985年将x86体系结构升级到32位,并将之命名为IA32,IA是Intel Architecture的缩写。
386是x86系列中的第一款32位微处理器,它的内部真正支持了32位的算术和逻辑运算,并提供了32位的通用寄存器,地址总线也扩展到了32位,这样就可以寻址到\(2^{32}\),也就是 4GByte 的内存空间。386还在286的基础上改进了保护模式,此外还增加了虚拟8086模式,以运行兼容8086的程序。
在386之后,x86的微处理器的主要工作模式就是保护模式。保护模式为多任务的执行提供了很好的支持和保护,同时可以访问4G字节的物理存储空间,这相比之前是一个很大的进步,可以在相当长的时间内满足软件的需求。而且从386开始还引入了虚存的概念,可以对存储空间进行更好的管理。简单说来保护模式,可以让操作系统加强对应用软件的控制,使得系统运行更加安全高效。
现在在系统启动后,会首先进入实模式,在初始化完成后,通过设置cpu中的控制寄存器,进入保护模式,操作系统和应用程序都运行在保护模式下。如果需要运行兼容8086的程序,可以从保护模式下切换到虚拟8086模式。在这种情况下,如果发生中断或异常,是会回到保护模式处理,处理完成后,再返回虚拟8086模式。不管在哪种模式下,经过复位以后都会重新从实模式开始启动。
32位的x86,也就是IA32体系结构,维持了很长时间,并逐渐占据了市场的主导地位,但是随着应用对内存空间的需求越来越大,32位的体系结构已经难以满足要求。在x86向64位升级的过程中,x86-64的体系结构是最先由AMD提出来的。而英特尔当时正专注于另一种64位的体系结构,称为IA64,但它与IA32是不兼容的。
这就是第一款64位的x86微处理器,AMD的皓龙。它可以访问高于4G字节的存储器,从而解决内存空间受限的问题,其实x86扩展到64位实在说不上是一个亮点。因为在当时已经有很多体系结构都已经升级到了64位,但是由于x86一直坚持着向前兼容的设计原则,聚集了大量的应用程序,形成了很好的生态环境,因此AMD的x86-64的更重要的一点是,它能够兼容32位的x86程序,而且运行这些程序不会降低性能。与此对应的是英特尔提出的IA64体系结构,这是基于这个结构的安腾处理器,它与IA32体系结构并不兼容,只是提供了一种模式,可以运行32位的x86程序,但是会大大降低性能,这就成为了安腾不受欢迎的重要原因之一。中间曲线图是英特尔对安腾销售的预期,这条蓝色的线是97年的预测,第二条线是98年的预测,第三条线是99年的预测,实际的销售情况是这条橙色的线所表示的,所以最后英特尔也回过头来才用AMD的扩展方案。
这就是x86-64,x86-64支持原有的32位的x86的运行模式,并将之命名为传统模式。新增的模式称为长模式。长模式又分为两个子模式,64位模式和兼容模式。在兼容模式下,原有的x86程序不需要重新编译,就可以高效的运行,这也是x86-64得以成功的关键。
x86体系结构从16位演变到64位,其中有很多的变化,我们来看两个重要的方面。第一是寄存器模型的变化。
这是8086所使用的寄存器。我们分别来看它各自的功能。
在通用计算机中,前四个又称为数据寄存器,它们都是16位寄存器,但也可以当做两个八位寄存器使用。大多数算术运算和逻辑运算都会使用到这些数据寄存器,用于临时保存数据,而且它们除了作为通用的用途之外,还有一些专门的用途。
接下来的四个寄存器,又被称为指针寄存器和变址寄存器。SP和BP这两个寄存器用于堆栈操作,SI和DI这两个寄存器用于串操作。这是它们名称的含义,之后在介绍相关的指令时,会讲解它们的具体用途。当然这些寄存器也可以当做普通的数据寄存器使用。
我们再来看标志寄存器,标志寄存器当中包含的若干标志位,标志位分为两大类。
状态标志用于反映cpu的工作状态。比如说执行完加法运算后,如果产生进位,就会将标志寄存器当中体现进位的状态位置为有效。
而控制标志则是对cpu的运行起特定的控制作用。例如,如果让cpu采用单步的方式,而不是连续执行指令的方式运行,则需将标志寄存器当中的某个控制标志置为有效。
这是8086的标志位,实际只使用了其中九个比特。红色这些是状态标志,例如比特0就是进位标志。另外三个紫色是控制标志。这些标志位我们也会结合着指令进行讲解。
从8086到IA32,寄存器模型也由16位扩展到了32位。通用寄存器都在原有的基础上增加了高16位,扩展成了32位。例如,原来的AX扩展成32位以后,新的名称为EAX。程序中使用EAX寄存器就是一个32位寄存器,但仍然可以使用AX访问16位,也可以使用AH或者AL访问其中的特定的八位。再来看指令指针寄存器也从16位扩展到32位,标志寄存器也是同样。但是段寄存器的长度没有扩展,而是增加了两个16位的段寄存器。
我们再来看x86-64对寄存器模型的扩展。x86-64将大多数寄存器扩展成了64位,例如EAX扩展了高32位以后成为了RAX,和之前一样,不但可以使用64位的RAX寄存器,同时也可以使用EAX、AX等等。需要注意的是,段寄存器仍然没有扩展。此外,x86-64还增加了八个64位的通用寄存器,命名为R8到R15。
然后我们来看x86体系结构演变过程中的另一个要点,就是存储器的寻址。
8086依靠指令指针寄存器,也就是IP寄存器进行寻址。IP寄存器中保存了一个内存地址,指向当前需要取出的指令。当cpu从内存中取出一个指令后,IP寄存器会自动增加指向下一个指令的地址。程序员不能直接操作IP寄存器,但是可以通过编写转移等指令来改变IP寄存器的内容。我们知道8086是16位的,我们使用IP寄存器可以访问到的内存空间就是2的16次方,也就是64k个字节单元。但其实8086对外有20位的地址线,寻址范围是1兆的字节单元。那如何解决这个矛盾呢?
8086中提供了段寄存器来解决这个问题。8086一共提供了四个段寄存器:CS是代码段寄存器,DS是数据段寄存器,ES是附加段,SS是堆栈段。
现在我们就来看8086的物理地址是如何生成的?我们在编写程序时直接给出的地址称为偏移地址,又叫偏移量,这是16位的。而段寄存器中存放了段基址,这一对地址就被称为逻辑地址。其中段基址在计算时首先被左移四位,然后和偏移量相加,这样就得到了一个20位的物理地址,这也就是从逻辑地址生成物理地址的过程。二进制的左移四位,实际相当于十进制的乘以16,所以物理地址相当于段基址乘以16,再加上偏移量。这就是8086段加偏移的物理地址生成方式。
用这样的方式,8086对存储器进行分段式的管理,每个段最大为2的16次方,也就是64k个字节,他们在编程时使用逻辑地址,这样的好处在于,如果我们将程序在存储器当中变换位置,那程序中原来编写的偏移地址都不需要改动,只需要修改段寄存器的内容就可以了。而且还要说明,不同的逻辑段实际上是可以有重叠的。这也为高效的利用存储器的空间提供了便利。
我们来看一个实例。如果我们写一条指令 MOV AX, [3000H],但实际上在段加偏移的模式下,操作数是默认存放在DS也就是数据段寄存器所指向的段中,让我们假设事先已经在DS段寄存器当中存入了2000H。那么实际的物理地址应该采用段寄存器乘以16,加上偏移量的方式得到最终我们所用的物理地址是23000H。
我们来看左边这个存储器的片段,假设代码段在这里,其中有一条指令就是我们现在所用的MOV指令。第一个字节是操作码,后两个字节就是指令中指定的偏移地址。那cpu在取回这条指令后,发现要从存储器中取得一个数据,因此cpu会取出DS寄存器的内容,并将其左移四位后,与指令编码中的偏移量相加,从而得到了23000这个地址,然后用这个地址去访问存储器。因为我们的目标寄存器AX是16位的,所以从存储器中取出两个字节,并依照着高地址放在高字节,低地址放在低字节的原则,存放到AX当中。这就完成了这条指令的执行。这样段加偏移的方式实际上是非常复杂的,这是在内部的运算器和通用寄存器只有16位的情况下,又想访问超过16位的地址空间所想出的一个折中的办法,算是在简陋的条件下用了一个巧妙的方法,从而提供了较好的性能。但是如果不仅限于微处理器,计算机中是早就采用了32位的地址空间的。
比如60年代的大型计算机 IBM S/360,在其比较早的型号中就使用了32位的通用寄存器,cpu对外发出32位的地址。
我们再来看IA32体系结构的存储器寻址。我们以指令的寻址为例,原先16位的IP寄存器已经扩展为了32位的EIP寄存器。当然在实模式下面还是使用 CS:IP 这样的逻辑地址。而在保护模式下,因为ERP寄存器的寻址能力已经达到了 \(2^{32}\),也就是4G个字节单元,这和CPU对外的地址总线的宽度是一致的,也就是说EIP寄存器完全有能力访问到外部存储器的每一个字节单元,但在保护模式下,IA32仍然采用了与之前形式上一致的寻址模式 CS:EIP 这样的逻辑地址的组合,但是在这种模式下,段寄存器 CS 的含义已经发生了变化。
实际在保护模式下,段基值是放在内存中的。这是一个存储器的片段,这些数据以八个字节为一个单位,我们称为一个描述符。我们来看其中一个描述符,字节2、3、4、7,一共四个字节,保存了一个基地址。而cpu中,CS段寄存器不再保存段基址,而是指向这个描述符的地址,但是我刚才说过CS只有16位,它无法在4G空间中寻址,所以CPU当中还增加了一个寄存器,叫 GDTR。而 GDTR 中则保存了这个全局描述符表的起始地址,CS 段寄存器中则保存了相对于这个起始地址的偏移量,所以我们可以这么理解,cpu在取指令时,先将 GDTR 中保存的地址取出,并于CS寄存器当中的内容相加,得到一个内存地址,用这个地址去访问存储器,获得了这个描述符对应的八个字节,再将其中的基地址部分提取出来,与EIP寄存器中保存的偏移地址相加,就得到了真正要访问的地址。当然在这个描述中还描述了这个段有多长,也就是段界限;以及对这个段的内容是否能读写,也就是权限;还有一些其它的信息。CPU通过这些信息可以由硬件来判断,当前要执行的这条指令是否符合这个段的要求,从而起到了保护的作用。我们可以看到分段的方式变得越来越复杂,虽然它提供了很好的保护机制,但是代价也是不小的。而且在分段的基础上,还可以采用分页等机制,进行更细粒度的管理,所以在有些系统上就对分段进行了简化的处理,让所有的程序都运行在一个段上,也就是不改变段寄存器的内容,而在x86-64上得到了进一步的简化。
图中一个x86-64当中对应的描述符,我们可以看到,很多的字节都被固定设置为零。也就是说,这时候已经没有了段基址和段界限,所有的代码段都是从地址0开始的。在描述符中只设置了这个段对应的权限和若干的控制位。
这些就是x86体系结构演变过程中的一些要点。现在我们已经了解了x86体系结构的基本特点,之后,我们将通过分析x86的具体指令,来进一步学习这种体系结构。