4.3.1 将处理组织成阶段
通常,处理一条指令包括很多操作。将它们组织成某个特殊的阶段序列,即使指令的动作差异很大,但所有的指令都遵循统一的序列。每一步的具体处理取决于正在执行的指令。创建这样一个框架,我们就能够设计一个充分利用硬件的处理器。下面是关于各个阶段以及各阶段内执行操作的简略描述:
取指(fetch):取指阶段从内存读取指令字节,地址为程序计数器(PC)的值。从指令中抽取出指令指示符字节的两个四位部分,称为icode(指令代码)和ifun(指令功能)。它可能取出一个寄存器指示符字节,指明一个或两个寄存器操作数指示符rA和rB。它还可能取出一个四字节常数字valc。它按顺序方式计算当前指令的下一条指令的地址 valP。也就是说,valp等于PC的值加上已取出指令的长度。
译码(decode):译码阶段从寄存器文件读入最多两个操作数,得到值 valA和/或 valB。通常,它读入指令rA和rB字段指明的寄存器,不过有些指令是读寄存器%rsp的。
执行(execute):在执行阶段,算术/逻辑单元(ALU)要么执行指令指明的操作(根据ifun的值),计算内存引用的有效地址,要么增加或减少栈指针。得到的值我们称为valE。在此,也可能设置条件码。对一条条件传送指令来说,这个阶段会检验条件码和传送条件(由ifun给出),如果条件成立,则更新目标寄存器。同样,对一条跳转指令来说,这个阶段会决定是不是应该选择分支。
访存(memory):访存阶段可以将数据写人内存,或者从内存读出数据。读出的值为 valM。
写回(write back):写回阶段最多可以写两个结果到寄存器文件。
更新 PC(PC update):将PC设置成下一条指令的地址。
处理器无限循环,执行这些阶段。在我们简化的实现中,发生任何异常时,处理器就会停止:它执行halt指令或非法指令,或它试图读或者写非法地址。在更完整的设计中,处理器会进入异常处理模式,开始执行由异常的类型决定的特殊代码。
从前面的讲述可以看出,执行一条指令是需要进行很多处理的。我们不仅必须执行指令所表明的操作,还必须计算地址、更新栈指针,以及确定下一条指令的地址。幸好每条指令的整个流程都比较相似。因为我们想使硬件数量尽可能少,并且最终将把它映射到一个二维的集成电路芯片的表面,在设计硬件时,一个非常简单而一致的结构是非常重要的。降低复杂度的一种方法是让不同的指令共享尽量多的硬件。例如,我们的每个处理器设计都只含有一个算术/逻辑单元,根据所执行的指令类型的不同,它的使用方式也不同。在硬件上复制逻辑块的成本比软件中有重复代码的成本大得多。而且在硬件系统中处理许多特殊情况和特性要比用软件来处理困难得多。
图 4-18给出了对 OPq(整数和逻辑运算)、rrmovq(寄存器-寄存器传送)和irmovq(立即数-寄存器传送)类型的指令所需的处理。让我们先来考虑一下整数操作。我们小心地选择了指令编码,这样四个整数操作(addq、subq、andq 和 xorq)都有相同的 icode值。我们可以以相同的步骤顺序来处理它们,除了ALU计算必须根据ifun 中编码的具体的指令操作来设定。
整数操作指令的处理遵循上面列出的通用模式。在取指阶段,我们不需要常数字,所以 valp就计算为PC+2。在译码阶段,我们要读两个操作数。在执行阶段,它们和功能指示符ifun一起再提供给ALU,这样一来 valE就成为了指令结果。这个计算是用表达式valB oP val来表达的,这里OP代表ifun指定的操作。要注意两个参数的顺序——这个顺序与Y86-64(和x86-64)的习惯是一致的。例如,指令subq %rax,%rdx计算的是R[%rdx]-R[%rax]的值。这些指令在访存阶段什么也不做,而在写回阶段,valE被写人寄存器 rB,然后 PC设为 valP,整个指令的执行就结束了。
执行rrmovq指令和执行算术运算类似。不过,不需要取第二个寄存器操作数。我们将 ALU的第二个输人设为0,先把它和第一个操作数相加,得到valE=valA,然后再把这个值写到寄存器文件。对irmovq的处理与此类似,除了ALU的第一个输入为常数值valc。另外,因为是长指令格式,对于irmovq,程序计数器必须加10。所有这些指令都不改变条件码。
图4-19给出了内存读写指令rmmovq和mrmovq所需要的处理。基本流程也和前面的一样,不过是用ALU来加 valc和valB,得到内存操作的有效地址(偏移量与基址寄存器值之和)。在访存阶段,会将寄存器值val写到内存,或者从内存中读出valM。
图4-20给出了处理pushq和popq指令所需的步骤。它们可以算是最难实现的Y86-64指令了,因为它们既涉及访问内存,又要增加或减少栈指针。虽然这两条指令的流程比较相似,但是它们还是有很重要的区别。
pushq指令开始时很像我们前面讲过的指令,但是在译码阶段,用%rsp作为第二个寄存器操作数的标识符,将栈指针赋值为valB。在执行阶段,用ALU将栈指针减8。减过8的值就是内存写的地址,在写回阶段还会存回到%rsp中。将valE作为写操作的地址,是遵循Y86-64(和x86-64)的惯例,也就是在写之前,pushq应该先将栈指针减去8,即使栈指针的更新实际上是在内存操作完成之后才进行的。
popq指令的执行与pushq的执行类似,除了在译码阶段要读两次栈指针以外。这样做看上去很多余,但是我们会看到让valA和valB都存放栈指针的值,会使后面的流程跟其他的指令更相似,增强设计的整体一致性。在执行阶段,用ALU给栈指针加8,但是用没加过8的原始值作为内存操作的地址。在写回阶段,要用加过8的指针更新栈指针寄存器,还要将寄存器rA更新为从内存中读出的值。用没加过8的值作为内存读地址,保持了Y86-64(和x86-64)的惯例,popq应该首先读内存,然后再增加栈指针。
图4-21表明了三类控制转移指令的处理:各种跳转、call和ret。可以看到,我们能用同前面指令一样的整体流程来实现这些指令。
同对整数操作一样,我们能够以一种统一的方式处理所有的跳转指令,因为它们的不同只在于判断是否要选择分支的时候。除了不需要一个寄存器指示符字节以外,跳转指令在取指和译码阶段都和前面讲的其他指令类似。在执行阶段,检查条件码和跳转条件来确定是否要选择分支,产生出一个一位信号Cnd。在更新PC阶段,检查这个标志,如果这个标志为1,就将 PC设为 valc(跳转目标),如果为0,就设为 valp(下一条指令的地址)。我们的表示法x?a:b类似于C语句中的条件表达式——当x非零时,它等于a,当x为零时,等于b。
4.3.2 SEQ 硬件结构
实现所有Y86-64指令所需要的计算可以被组织成6个基本阶段:取指、译码、执行、访存、写回和更新PC。图4-23更详细地给出了实现SEQ所需要的硬件(分析每个阶段时,我们会看到完整的细节)。
白色方框表示时钟寄存器。程序计数器PC是SEQ中唯一的时钟寄存器。
浅蓝色方框表示硬件单元。这包括内存、ALU等等。在我们所有的处理器实现中,都会使用这一组基本的单元。我们把这些单元当作“黑盒子”,不关心它们的细节设计。
控制逻辑块用灰色圆角矩形表示。这些块用来从一组信号源中进行选择,或者用来计算一些布尔函数。我们会非常详细地分析这些块,包括给出HCL描述。
线路的名字在白色圆圈中说明。它们只是线路的标识,而不是什么硬件单元。
宽度为字长的数据连接用中等粗度的线表示。每条这样的线实际上都代表一簇64根线,并列地连在一起,将一个字从硬件的一个部分传送到另一部分。宽度为字节或更窄的数据连接用细线表示。根据线上要携带的值的类型,每条这样的线实际上都代表一簇4根或8根线。
单个位的连接用虚线来表示。这代表芯片上单元与块之间传递的控制值。
4.3.3 SEQ 的时序
SEQ的实现包括组合逻辑和两种存储器设备:时钟寄存器(程序计数器和条件码寄存器),随机访问存储器(寄存器文件、指令内在和数据内存)。组合逻辑不需要任何时序或控制——只要输入变化了,值就通过逻辑门网络传播。正如提到过的那样,我们也将读随机访问存储器看成和组合逻辑一样的操作,根据地址输入产生输出字。对于较小的存储器来说(例如寄存器文件),这是一个合理的假设,而对于较大的电路来说,可以用特殊的时钟电路来模拟这个效果。由于指令内存只用来读指令,因此我们可以将这个单元看成是组合逻辑。
现在还剩四个硬件单元需要对它们的时序进行明确的控制——程序计数器、条件码寄存器、数据内存和寄存器文件。这些单元通过一个时钟信号来控制,它触发将新值装载到寄存器以及将值写到随机访问存储器。每个时钟周期,程序计数器都会装载新的指令地址。只有在执行整数运算指令时,才会装载条件码寄存器。只有在执行rmmovq、pushq或call指令时,才会写数据内存。寄存器文件的两个写端口允许每个时钟周期更新两个程序寄存器,不过我们可以用特殊的寄存器ID0xF作为端口地址,来表明在此端口不应该执行写操作。
要控制处理器中活动的时序,只需要寄存器和内存的时钟控制。我们遵循以下原则组织计算:
原则:从不回读
这条原则对实现的成功来说至关重要。为了说明问题,假设我们对pushq指令的实现是先将%rsp减8,再将更新后的%rsp值作为写操作的地址。这种方法同前面所说的那个原则相违背。为了执行内存操作,它需要先从寄存器文件中读更新过的栈指针。然而,我们的实现产生出减后的栈指针值,作为信号valE,然后再用这个信号既作为寄存器写的数据,也作为内存写的地址。因此,在时钟上升开始下一个周期时,处理器就可以同时执行寄存器写和内存写了。
4.3.4 SEQ 阶段的实现
现在我们已经浏览了Y86-64处理器的一个完整的设计。可以看到,通过将执行每条不同指令所需的步骤组织成一个统一的流程,就可以用很少量的各种硬件单元以及一个时钟来控制计算的顺序,从而实现整个处理器。不过这样一来,控制逻辑就必须要在这些单元之间路由信号,并根据指令类型和分支条件产生适当的控制信号。
SEQ唯一的问题就是它太慢了。时钟必须非常慢,以使信号能在一个周期内传播所有的阶段。让我们来看看处理一条ret指令的例子。在时钟周期起始时,从更新过的PC开始,要从指令内存中读出指令,从寄存器文件中读出栈指针,ALU将栈指针加8,为了得到程序计数器的下一个值,还要从内存中读出返回地址。所有这一切都必须在这个周期结束之前完成。
这种实现方法不能充分利用硬件单元,因为每个单元只在整个时钟周期的一部分时间内才被使用。我们会看到引入流水线能获得更好的性能。
标签:执行,4.3,Y86,指令,64,内存,寄存器,指针,阶段 From: https://blog.csdn.net/chenwinsagain/article/details/144802105