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 的具体指令来进一步学习这种体系结构。