202-x86体系结构
# 202-x86体系结构
X86是商业上最为成功,影响力最大的一种体系结构。 但从技术的角度看,它又存在着很多的问题, 那我们就来一起分析X86,这种体系结构的特点。 这张表列出了X86体系结构当中,具有代表性的一些微处理器的型号。 主要分成了16位、32位和64位三大类。
我们先来看最早推出来的8086, 8086是一款16位的CPU, 所谓16位CPU,主要是指CPU当中的运算部件可以支持16位数据的运算。 因为运算当中所需要的数据,一般会放在通用寄存器中。 所以通用寄存器的位宽通常和运算单元的位宽是相同的。 而运算单元产生的数据又经常会用做访问存储器的地址。 所以CPU访问存储器地址的宽度,也常常和运算单元的位宽相同。 那么对于8086来说,它是一个16位的CPU,它内部的通用寄存器也是16位的。 但是它连接存储器的地址线的宽度却是20位的。 那么它生成访问存储器的地址就需要采用一些特殊的方式。 下面我们先来看8086内部的通用寄存器的情况,再来介绍它生成地址的方式。
这就是8086体系结构所规定的寄存器。 都是16位宽的,主要可分为这几类:通用寄存器、 指令指针寄存器、标志寄存器,还有段寄存器。
我们首先来看通用寄存器,结合我们之前说过的模型机的例子, 通用寄存器就在这里,CPU从存储器当中取回一个数, 很可能就会放在某一个通用寄存器当中。 而CPU执行运算指令,其操作数的来源也往往会在寄存器中。
对于8086来说,这些用于存储数据的通用寄存器, 主要有这四个:AX、BX、CX和DX。 这四个寄存器都是16位寄存器。 但是这些16位寄存器还可以被分为两个8位的寄存器来使用。 大多数的算术运算和逻辑运算的指令,都可以使用这些数据寄存器。 那么这些寄存器除了可以一般性的存放数据之外, 还会有一些专门的用途,那我们在后面介绍到相关的指令时再做具体的讲解。 除了这四个寄存器还有SP、BP、SI、DI这四个通用寄存器。 它们在早期都有一些特殊的用途。 而随着X86体系结构的不断更新,它们也大多成为了可以用于保存普通数据的寄存器。
然后我们再来看标志 寄存器,之前分析模型机时我们提到, 当执行运算指令时,ALU会将X和Y两个 寄存器当中的内容相加,并将运算结果放在Z这个寄存器中。 同时将运算结果的一些特性保持在标志寄存器中。例如这个 加法运算如果产生了进位,那就可以将这个进位保持在标志寄存器中, 以免丢失,后续的运算也可以知道之前的运算产生了这样一个进位。 那在8086中,也有这样一个标志寄存器。 称为FLAGS,这个寄存器当中,包含了若干个标志位。 主要可以分为量大类,一类称为状态标志,它反映的是CPU的工作状态。 另一大类称为控制标志。 这是对CPU的运行起到特定的控制的作用。
那8086的这个标志寄存器也是16位的,但实际只有其中一部分有具体的含义。 在图中标为红色的都是状态标志,标为紫色的这三个是控制标志。 例如我们刚才提到的加法的进位标志。 当CPU执行完一条加法指令, 而这次加法运算的结果产生了一个进位, 那CPU内部除了将加法的运算结果保存到对应的寄存器之外, 还同时会将这个CF标志置为1, 这个动作是由硬件自动完成的,不需要由编程人员来设置。
我们再来看下一个寄存器。 在模型机上,CPU要去取下一条指令之前, 都会先从PC寄存器当中,取出下一条指令的地址, 将这个地址发到存储器中,才能取回下一条指令的编码。 那在8086当中,这个寄存器称为IP寄存器。 IP是指令指针的缩写。 编程人员是不能直接修改IP寄存器的, 除了顺序取出指令,IP寄存器会自动增加以外, 如果遇到了转移指令,这些会改变程序流向的指令, 那CPU会自动修改IP寄存器的内容。
这里我们还需要注意一个问题,因为IP寄存器是16位宽的, 所以它能够指向的内存单元的数量是2的16次方。 也就是64K个字节单元,那么即使在那个时代,64K的内存也是太小了, 无法满足当时大多数程序的需求。 因此,实际上8086在外部连接的是一个1兆字节的内存。 这样就需要8086对外有20位的地址线。 那多出来的这4位地址线,从哪里来呢? 那8086采用的是一个很巧妙,也很繁琐的解决方案。 这就是用段寄存器的方式。
段寄存器是用来和其它寄存器一起联合生成存储器地址的, 8086当中有4个段寄存器。 CS是代码段寄存器,DS是数据段寄存器。 ES是附加段寄存器,SS是堆栈段寄存器。 我们以代码段寄存器为例,来看一看地址生成的方式。 假设8086CPU要从这个1M的内存中取出一条指令, 那就需要现在段寄存器当中保存这个地址的一部分。 然后地址的另一部分根据这个程序的本身来产生。这样的组合,就称为逻辑地址。
我们假设已经在这个代码段寄存器当中存放了一个16位数, 那用16进制来表示,就是2000H, 而根据程序运行的状况,当前IP寄存器当中的值是3000H, 那下一条指令的地址是怎么产生的呢? 那在CPU内部就会有一个硬件单元,负责移位, 先将段寄存器当中的16位数向左移4位。 那新产生的这个数用16进制来表示就是20000H, 然后再将这个移位后的数与IP寄存器当中的内容相加, 这又需要用到一个加法器。 相加之后就得到一个20位的地址, 在这里就是23000H,这时CPU才 可以将这个地址发送到存储器去,从而取回下一条指令的编码。 而这个地址则被称为物理地址,从逻辑地址到物理地址,就是用段寄存器当中的内容乘以16再加上程序中产生的偏移地址。
那再用我们已经很熟悉的模型机来看另一个例子:DS段寄存器。 假设这时CPU已经把下一条指令的编码取回了, 放在IR寄存器当中,那这条指令是要将3000H 所指向的内存地址当中的数取出来,放在AX寄存器当中。 那如果是在我们之前讲过的模型机上运行, CPU就会将3000H这个数, 放到MAR寄存器当中去,然后再传送到地址总线上。
但是对于8086来说,它要发出的是一个20位的地址, 必须先要用段加偏移的方式进行计算。 那我们假设之前已经在DS寄存器当中保存了2000H这个数, 那CPU的硬件就会将DS当中的数取出来。 送到一个移位的部件,向左移4位。 然后再和3000H相加。 这样就得到了一个20位的地址,23000H, 然后才能把 这个生成的地址放在MAR寄存器当中,再传送到地址总线上。
然后存储器则会返回23000H这个地址所对应的内容, 并放到数据总线上,进一步保存到了MDR寄存器中,最后CPU的硬件 会将MDR当中的内容,再传送到AX计算器当中,从而完成了这条指令所执行的操作。
那么结合上一页我们所介绍的内容,我们会发现,对于8086来说, 它在取指令的时候,就要执行一次段加偏移的这样的计算。 那么在执行指令的时候,还要执行这样一次计算,那它执行一条指令的过程 就比我们之前在模型机上学习的例子要复杂得多了。 当然,虽然很繁琐, 但在那个时期,确实在一定程度上解决了16位地址空间太小的问题。
# 80386
但是想要提供更高的性能,以满足当时蓬勃发展的个人计算机的需要, 还是要从体系结构上做大的改进。 而1985年推出的80386就是这样一款跨时代的作品。
80386是x86系列当中第一款32位的微处理器, 也就是说它的运算部件可以支持32位数据的运算, 同时也提供32位的通用寄存器, 那么自然它也可以产生32位的地址, 从而可以指向2的32次方,也就是4G字节的内存空间。 这样大容量的内存空间在之后相当长的 时间里,都让编程几乎不受内存空间的限制。 而英特尔也凭借80386确立了它在个人计算机CPU领域的优势地位。 此外,80386还对运行模式进行了改进, 以便更好和更稳定地支持操作系统,以及越来越丰富的软件。
32位x86的体系结构也被称为IA-32, 它所提供的32位寄存器是在8086 16位寄存器的基础上扩展而来的。 例如8086中的AX寄存器,在为它增加了16位之后就变成了32位的EAX寄存器。 在指令中如果使用EAX, 就是指这个32位的寄存器,但与此同时,指令中 还可以继续使用AX来指定其中的低16位。 同样,也可以继续使用AH和AL这两个8位的寄存器编号, 那这样IA-32中就有了8个32位的通用寄存器, 还有一个32位的标志寄存器。 指令指针寄存器也扩展到了32位, 用这个寄存器就可以指向2的32次方,也就是4G字节的内存空间。
从这里看来,386只要使用这个EIP寄存器就足够了, 但实际上386不但保留了原先的4个段寄存器, 还增加了2个段寄存器。而运行在保护模式下, 这些段寄存器的使用方法是不一样的。 你如果有兴趣可以查阅保护模式相关的资料进行学习, 在这里就不再详细描述了。
那到了上世纪90年代后期,即使在个人计算机领域, 32位CPU也逐渐出现了难以满足性能需求的情况, 尤其是4G内存的空间限制了大规模程序的应用, 那在这时,一贯主导x86体系结构改进的英特尔,它提出了名为IA-64的体系结构。 这个64位的体系结构和之前的x86体系结构并不兼容。由于种种原因,这个新的结构并未获得成功。
那趁着这个机会,AMD后来居上, 提出了与原先兼容的64位的x86的方案, 从而在64位的时代占据了先机, 当然后来英特尔也转回来支持这个兼容的方案。 那这个方案有很多不同的名字, 比如说AMD64,Intel64, 通常我们更多地把它称为x86-64。 那x86-64的寄存器模型则是在IA-32的32位寄存器模型的基础上进行了扩展。 那与之前类似,在原先32位的EAX寄存器的基础上再增加32位,形成了64位的RAX寄存器。 而指令指针寄存器也被扩展到了64位,因此理论上我们就可以访问2的64次方个字节这么大的内存空间。 此外,因为把常用的操作数放在寄存器当中比放在存储器当中性能要好得多。 因此有更多的寄存器,编程就会更加地方便。 那么在x86-64当中,另外还新增了8个64位的通用寄存器, 这8个新增的寄存器的名称依次为R8,R9,一直到R15。 因为之前我们就已经有了8个通用寄存器, 如果要给它们编号的话,就正好是从R0到R7, 所以新增的寄存器就从R8开始编号, 这就是x86体系结构从16位直到64位的大致情况。
现在我们已经了解了x86体系结构的基本特点, 之后我们将通过分析x-86的具体指令来进一步学习 这种体系结构。