CSAPP——Y86-64顺序实现(4.3)
介绍:
Y86为CSAPP书中为方便学习而简化的X86。Y86顺序结构的是无流水线的结构(SEQ):在一个足够长的时钟周期上,该结构会完成一条完整的汇编指令。每个汇编指令有6个执行阶段。
一、六个基本阶段
取指——译码——执行——访存——写回——更新PC
二、各阶段主要功能
1、取指:计算当前指令的长度,获取需要用到的寄存器和立即数
2、译码:从寄存器中读取数据
3、执行:算数逻辑单元(ALU)执行三类计算:算数逻辑运算、计算内存引用有效地址、针对push和pop等计算栈指针的增减
4、访存:从内存读取数据 or 将数据写入内存
5、写回:将数据写入寄存器
6、更新pc:设置下一条指令的地址
三、各阶段详解
1、取值阶段:
输入:PC 输出:icode、ifun、rA、rB、valC、valP
图1:取指阶段
首先从PC寄存器指向的位置取10字节(Y86中最长的指令的长度)作为当前指令,以保证在任何情况下指令都是完整的,再根据接下来的步骤确定指令的真正长度。10个字节中的第1个字节为操作码,包含icode和ifun。
图2:icode和ifun对应的部分指令
对于操作码,icode占第1个字节中的高4位,称为指令代码,表示该条指令的分类。ifun占低4位,称为指令功能,表示指令的具体功能。硬件根据icode可以判断指令是否合法。此外,还可以根据icode判断当前指令是否需要寄存器,需要几个寄存器,是否需要立即数,以确定当前指令的真正长度,分四种情况:
(1)寄存器+立即数:说明该指令长度就是10Bytes。其中第2个字节为寄存器标识符rA和rB(rA占高4位,rB占低4位)若需要两个寄存器,则rA和rB都按对应规则进行标识,若只需要一个,不需要的寄存器对应的标识符变为0xF,另一个正常标识。第3到10个字节为立即数(valC)。10个字节全部保留。
(2)仅立即数:该指令长度为9Bytes。第2到9个字节为立即数valC,第10个字节给下一条指令用。
(3)仅寄存器:指令长度为2Bytes。第2个字节为rA和rB,根据需要1个 or 2个寄存器,同(1)理处理rA和rB,第3到10字节给下一条指令。
(4)什么也不需要:该指令长度1Bytes。第2到10字节给下一条指令。
valP为下一条指令的位置。由上述步骤计算出当前指令的长度 + 当前PC可得出下一条指令的位置valP,可以用来更新PC。
图3:寄存器标识符rA和rB与寄存器的对应规则,图中number均占4个bit
图4:Y86的一些汇编指令,包含了前文所述的4种情况
2、译码:
输入:icode、rA、rB 中间变量:srcA、srcB 输出:valA、valB
硬件电路通过rA、rB和icode的值,找到最后要用到的寄存器srcA和srcB。rA和rB为寄存器标识符,与寄存器有对应规则,可以产生出srcA和srcB。之所以需要用到icode,是因为有些指令除了rA or rB外,还需要知道栈顶指针rsp的值,例如:pushq rA 该指令将rA对应的寄存器中的值压入栈中,因此需要rsp找到栈顶位置,rA对应的寄存器和rsp寄存器作为srcA和srcB。除了push外,还有pop、call、ret需要rsp。根据srcA和srcB,找到对应寄存器,并得到其中所存的数据,为valA和valB。
3、执行:
输入:icode、ifun、valA、valB、valC 中间变量:ALUA、ALUB、ALUfun、CC 输出:Cnd、valE
图5:执行阶段
算数逻辑单元(ALU)主要执行三类计算:算数运算和逻辑运算、计算内存引用有效地址、计算栈指针的增减。对于ALU,数据输出包括:(1)valE:计算结果。(2)Cnd:跳转指令标识,对于跳转指令,若Cnd==1则跳转,Cnd==0则不跳转。对于Cnd,需要由icode决定是否需要设置CC,因为只有跳转指令需要判断是否跳转,当前指令是否为跳转指令由icode判断;还需要ifun来决定最后是否跳转,因为ifun中指明了跳转条件(见图2)。
数据输入包括:
(1)ALUfun:由取指阶段产生的icode和ifun组成,用来规定ALU的计算方式。
(2)ALUB:数据源B,由译码阶段寄存器取出值valB和icode产生。之所以要用到icode,是因为需要告诉ALU当前指令是否是类似push的指令,既要进行数据传输,又要对栈指针进行增减。例如push rax,将rax中的值传送到rsp指向的位置后,还要将rsp中的值-8。
(3)ALUA:数据源A由icode、valA、valB产生。同时需要valA和valC的原因是:有些指令需要先将立即数和其中一个寄存器中的值进行计算,再进行其他计算。例如图4中:rmmovl rA,D(rB) 和 mrmovl D(rB),rA 指令,将D(也就是valC)与rB中的值相加后作为地址,之后该地址中的数据和rA对应寄存器再进行运算。
注:数据传送指令中,ALU有时会进行+0操作。例:irmovq %8,%rax valC=8,ALU将其+0得到valE。
4、访存:
输入:icode、valE、valA 输出:valM、Stat
图6:访存阶段
访存阶段不是必须的,当指令不涉及内存读写时不需要该阶段。
访存阶段,先由icode确定当前指令到底要读内存(Mem.read)还是写内存(Mem.write)。由icode、valE、valA、valP确定目标地址(Mem.addr)和待写入的数据(Mem.data)。当把寄存器中的值看作地址时,valA用于确定目标地址(Mem.addr);当把寄存器中的值看作数值时,valA用于确定写入的数据(Mem.data)。当目标地址由ALU计算得出时,valE用于确定目标地址。当执行call类指令时,需要将下一条指令的位置压入栈中,因此需要valP。最后,确定目标地址和写入数据都需要icode的来分辨valE、valA、valP的最终用法(读or写,写什么,读什么)。若是读数据,内存会输出一个读到的数据valM作为输出,同时该阶段还会产生一个状态码Stat。
5、写回:
输入:valM、valE 输出:造成寄存器文件中的数据的改变
图7:译码和写回阶段
译码阶段和写回阶段都是对寄存器的操作,可以画在一张图里,但是这两个阶段在时间上有先后之分。
写回阶段并不是必须的,当不需要修改(PC以外的)寄存器中的值时,不需要这一步。
输入值中:valE为ALU计算的结果,valM为访存阶段从内存中取出的值,二者为待写入的值。写入的位置是dstE和dstM,由icode,rA,rB计算得出。
6、更新PC:
输入:icode、Cnd、valC、valM、valP 输出:造成PC寄存器中的数据的改变
icode用于确定当前指令是普通指令or跳转指令;Cnd确定jxx类指令的跳转条件是否满足;valC是call指令或者jxx类指令的跳转目标;valM是从内存中取出的值,一般是ret类指令的跳转目标;valP为下一条指令的位置,当前指令非跳转指令时,使用valP来作为PC的新值。
四、总结
Y86的顺序结构直观明了,但存在两个缺点:
(1)要在一个时钟周期内执行完所有的阶段,这要求时钟周期足够长,因此时钟频率低计算速度慢。
(2)当前阶段处理完成后,当前阶段的硬件就处于空闲状态,利用效率不高。
使用流水线结构可以克服这两个缺点。