目录
前言
构造了一个Y86-64指令集后,我们可以实现一个基于Y86-64指令的处理器,称为SEQ(sequential)处理器
这种处理器在一个时钟周期内只处理一条指令,因此效率很低,但理解处理器的顺序执行是之后实现高效的流水线化的处理器的基础
1. 指令处理的阶段
通常,对一条指令的处理包括很多操作,在这里,我们用一个统一的框架来描述。实现Y86-64指令所需要的计算可以被组织成六个阶段:
- 取指(fetch):取指阶段从内存中读取指令字节,内存的地址保存在程序计数器(PC)中,根据指令代码icode来判断指令是否含有寄存器指示符,是否含有常数,从而计算出当前的指令长度,并将valp置为PC+指令长度
- 译码(decode):译码阶段就是从寄存器文件中读取数据,寄存器文件有两个读端口,故最多读入两个操作数
- 执行(execute):执行阶段,算术逻辑单元(ALU)执行三种操作——根据指令功能ifun执行算数逻辑运算、计算内存引用的有效地址、增加或减少栈指针
- 访存(memory):访存阶段主要是针对内存的读写操作,既可以从内存中读出数据,也可以将数据写入内存
- 写回(write back):写回阶段与译码阶段类似,都是针对寄存器文件的操作,可以将最多两个结果写回寄存器文件
- 更新PC(PC update):将PC设置成下一条指令的地址
一般来说,处理器会循环往复地对指令执行这些阶段,只有在遇到halt指令或一些错误情况时,才会停下来——包括非法存储器地址(程序地址或数据地址),以及非法指令等
2. Y86-64指令处理详解
简单了解顺序处理的六个阶段后,针对不同的具体指令,这六个阶段的执行情况也有所不同
以下面的汇编代码和其对应的Y86-64指令为例来进行说明:
1 0x000: 30f20900000000000000 | irmovq $9, %rdx
2 0x00a: 30f31500000000000000 | irmovq $21, %rbx
3 0x014: 6123 | subq %rdx, %rbx #subtract
4 0x016: 30f48000000000000000 | irmovq $128, %rsp #problem
5 0x020: 40436400000000000000 | rmmovq %rsp, 100(%rbx) #store
6 0x02a: a02f | pushq %rdx #push
7 0x02c: b00f | popq %rax #problem
8 0x02e: 734000000000000000 | je done #not taken
9 0x037: 804103000000000000 | call proc #problem
10 0x040: | done:
11 0x040: 00 | halt
12 0x041: | proc:
13 0x041: 90 | ret #return
2.1 Opq、rrmovq、irmovq
阶段 | Opq rA, rB | rrmovq rA, rB | irmovq V, rB |
取指 | icod:ifun ← M1[PC] rA:rB ← M1[PC + 1] valP ← PC + 2 | icod:ifun ← M1[PC] rA:rB ← M1[PC + 1] valP ← PC + 2 | icod:ifun ← M1[PC] rA:rB ← M1[PC + 1] valC ← M8[PC + 2] valP ← PC + 10 |
译码 | valA ← R[rA] valB ← R[rB] | valA ← R[rA] | |
执行 | valE ← valB OP valA Set CC | valE ← 0 + valA | valE ← 0 + valC |
访存 | |||
写回 | R[rB] ← valE | R[rB] ← valE | R[rB] ← valE |
更新PC | PC ← valP | PC ← valP | PC ← valP |
以代码的第三行指令subq来进行说明:
注:←表示将右边的数据写入箭头指向的符号内
- 取指:从PC指向的内存中取出第一个字节M1[PC]为6 1,说明要执行的是个减法操作,icode:ifun ← M1[0X014] = 6:1;取第二个字节时找PC指向的地址后一个字节2 3为寄存器标识符,代表接下来用到的两个寄存器是%rdx和%rbx,rA:rB ← M1[0X015] = 2:3;由于取出的指令长度为两个字节,将valP的值置为PC+2,valP ← 0x014 + 2 = 0x016,用于在最后更新PC
- 译码:根据取指的结果从寄存器中读数据,将valA置为从寄存器rA(这里是%rdx)中读到的值,valA ← R[%rdx] = 9,将valB置为从寄存器rB(这里是%rbx)中读到的值,valB ← R[%rbx] = 21
- 执行:将译码阶段读到的valA和valB和取指的功能指示符(在这里是6 1,指明要执行减法操作)提供给ALU,ALU将valB - valA的值返回给valE,valE ← 21 - 9 = 12,同时设置条件码ZF ← 0,SF ← 0,OF ← 0
- 访存:这几条指令均不涉及内存的访问
- 写回:将ALU得到的valE写回到寄存器rB(%rbx)中,R[%rbx] ← valE = 12
- 更新PC:将取指阶段就计算好的valP写入PC中,代表下一条指令的地址,PC ← valP = 0x016
因此对于这样一条指令的计算的具体操作为:
阶段 | Opq rA, rB | sbuq %rdx %rbx |
取指 | icod:ifun ← M1[PC] rA:rB ← M1[PC + 1] valP ← PC + 2 | icode:ifun ← M1[0X014] = 6:1 rA:rB ← M1[0X015] = 2:3 valP ← 0x014 + 2 = 0x016 |
译码 | valA ← R[rA] valB ← R[rB] | valA ← R[%rdx] = 9 valB ← R[%rbx] = 21 |
执行 | valE ← valB OP valA Set CC | valE ← 21 - 9 = 12 ZF ← 0,SF ← 0,OF ← 0 |
访存 | ||
写回 | R[rB] ← valE | R[%rbx] ← valE = 12 |
更新PC | PC ← valP | PC ← valP = 0x016 |
同样,对于第四行的irmovq指令:
阶段 | Opq rA, rB | sbuq %rdx %rbx |
取指 | icod:ifun ← M1[PC] rA:rB ← M1[PC + 1] valP ← PC + 2 | icode ifun ← M1[0X016] = 3:0 rA:rB ← M1[0X017] = f:4 valP ← 0x016 + 10 = 0x020 |
译码 | ||
执行 | valE ← 0 + valC | valE ← 0 + 128 = 128 |
访存 | ||
写回 | R[rB] ← valE | R[%rsp] ← valE = 128 |
更新PC | PC ← valP | PC ← valP = 0x020 |
2.2 rmmovq和mrmovq
阶段 | rmmovq rA, D(rB) | mrmovq D(rB), rA |
取指 | icod:ifun ← M1[PC] rA:rB ← M1[PC + 1] valC ← M8[PC + 2] valP ← PC + 10 | icod:ifun ← M1[PC] rA:rB ← M1[PC + 1] valC ← M8[PC + 2] valP ← PC + 10 |
译码 | valA ← R[rA] valB ← R[rB] | valB ← R[rB] |
执行 | valE ← valB + valC | valE ← valB + valC |
访存 | M8[valE] ← valA | valM ← M8[valE] |
写回 | ||
R[rA] ← valM | ||
更新PC | PC ← valP | PC ← valP |
以代码的第五行指令rmmovq来进行说明
注:这条指令是将128这个立即数传送到地址为112的内存中,这个地址的计算为100(偏移量)+12(基址)
- 取指:从PC得到指令地址,访存得到指令,第一个字节是icode和ifun分别为4 0,代表了一个将寄存器的值传递到内存的操作,更新icode:ifun ← M1[0X020] = 4:0,同理rA:rB ← M1[0X021] = 4:3,接下来8字节是数据将要传到内存的地址相对于寄存器rB中地址的偏移量(这里内存操作的有效地址是偏移量与基址寄存器值之和),将这8个字节的数据放在valC中,valC ← M8[0x022] = 100,这条指令长度为10,故valP ← 0x020 + 10 = 0x02a
- 译码:将valA置为从寄存器rA(这里是%rsp)中读到的值,valA ← R[%rsp] = 128,将valB置为从寄存器rB(这里是%rbx)中读到的值,valB ← R[%rbx] = 12
- 执行:之前我们提到过,执行阶段的三个操作之一是计算有效地址,在这里将基址12与偏移量128相加得到有效地址,并保存在valE中,valE ← 12 + 100 = 112
- 访存:由于是要将128写入内存中,通过执行阶段我们已经得出了有效地址,因此访存阶段将valA(128)写到地址为112的8字节的内存中,M8[112] ← 128
- 写回:这里不涉及寄存器的写回操作
- 更新PC:将取指阶段就计算好的valP写入PC中,代表下一条指令的地址,PC ← valP = 0x02a
因此对于这样一条指令的计算的具体操作为:
阶段 | rmmovq rA, D(rB) | rmmovq %rsp, 100(%rbx) |
取指 | icod:ifun ← M1[PC] rA:rB ← M1[PC + 1] valC ← M8[PC + 2] valP ← PC + 10 | icode:ifun ← M1[0X020] = 4:0 rA:rB ← M1[0X021] = 4:3 alC ← M8[0x022] = 100 valP ← 0x020 + 10 = 0x02a |
译码 | valA ← R[rA] valB ← R[rB] | valA ← R[%rsp] = 128 valB ← R[%rbx] = 12 |
执行 | valE ← valB + valC | valE ← 12 + 100 = 112 |
访存 | M8[valE] ← valA | M8[112] ← 128 |
写回 | ||
更新PC | PC ← valP | PC ← valP = 0x02a |
2.3 push和pop
阶段 | pushq rA | popq rA |
取指 | icod:ifun ← M1[PC] rA:rB ← M1[PC + 1] valP ← PC + 2 | icod:ifun ← M1[PC] rA:rB ← M1[PC + 1] valP ← PC + 2 |
译码 | valA ← R[rA] valB ← R[%rsp] | valA ← R[%rsp] valB ← R[%rsp] |
执行 | valE ← valB + (-8) | valE ← valB + 8 |
访存 | M8[valE] ← valA | valM ← M8[valA] |
写回 | R[%rsp] ← valE | R[%rsp] ← valE R[rA] ← valM |
更新PC | PC ← valP | PC ← valP |
以代码的第六行指令pushq来进行说明
注:这条指令进行压栈,将栈指针减8,再把%rdx里的值入栈
- 取指:得到icode和ifun为a:0,icod:ifun ← M1[0x02a] = a:0,rA为2,rB没有寄存器为f,rA:rB ← M1[0x02b] = 2:f,指令长度为2字节,故valP ← 0x02a + 2 = 0x02c
- 译码:valA为要入栈的值,valA ← R[%rdx] = 9,valB为当前的栈指针%rsp内的地址,valB← R[%rsp] = 128
- 执行:执行阶段使用到了第三个操作——增加或减少栈指针,由于指令功能为压栈,故需要将%rsp的值减去8并返回给%rsp,valE ← 128 + (-8) = 120
- 访存:将要压栈的操作数valA写入栈空间,此时的地址就是valE,M8[120] ← 9
- 写回:将更新后的地址写回给%rsp实现栈空间增长的目的,R[%rsp] ← 120
- 更新PC:将取指阶段就计算好的valP写入PC中,PC ← valP = 0x02c
对于这样一条指令的计算的具体操作为:
阶段 | pushq rA | pushq %rdx |
取指 | icod:ifun ← M1[PC] rA:rB ← M1[PC + 1] valP ← PC + 2 | icod:ifun ← M1[0x02a] = a:0 rA:rB ← M1[0x02b] = 2:f valP ← 0x02a + 2 = 0x02c |
译码 | valA ← R[rA] valB ← R[%rsp] | valA ← R[%rdx] = 9 valB← R[%rsp] = 128 |
执行 | valE ← valB + (-8) | valE ← 128 + (-8) = 120 |
访存 | M8[valE] ← valA | M8[120] ← 9 |
写回 | R[%rsp] ← valE | R[%rsp] ← 120 |
更新PC | PC ← valP | PC ← valP = 0x02c |
popq指令和pushq的执行类似,除了在解码阶段要读两次栈指针以外
对于popq, 以代码的第七行指令popq为例:
阶段 | popq rA | popq %rax |
取指 | icod:ifun ← M1[PC] rA:rB ← M1[PC + 1] valP ← PC + 2 | icod:ifun ← M1[0x02c] = b:0 rA:rB ← M1[0x02d] = 0:f valP ← 0x02c + 2 = 0x02e |
译码 | valA ← R[%rsp] valB ← R[%rsp] | valA ← R[%rsp] = 120 valB ← R[%rsp] = 120 |
执行 | valE ← valB + 8 | valE ← 120 + 8 |
访存 | valM ← M8[valA] | valM ← M8[120] = 9 |
写回 | R[%rsp] ← valE R[rA] ← valM | R[%rsp] ← 128 R[%rax] ← 9 |
更新PC | PC ← valP | PC ← 0x02e |
译码阶段读两次栈指针的原因 :
第一次读将%rsp的值(也就是栈顶地址)读到valA里面,第二次读将%rsp的值读到valB里,其中valB参与栈指针的增减运算,这一过程发生在执行阶段,而在访存阶段用到了valA存的地址来找到出栈元素并放在valM中,写回阶段不仅要更新%rsp,还要将valM更新给rA。这样的设计增强了整体性,使得popq指令与其他指令更相似。
用没加过8的值作为内存读地址,保持了Y86-64(和x86-64)的惯例,popq应该首先读内存,然后再增加栈指针
2.4 jXX、call和ret
阶段 | jXX Dest | call Dest | ret |
取指 | icod:ifun ← M1[PC] valC ← M8[PC + 1] valP ← PC + 9 | icod:ifun ← M1[PC] valC ← M8[PC + 1] valP ← PC + 9 | icod:ifun ← M1[PC] valP ← PC + 1 |
译码 | valB ← R[%rsp] | valA ← R[%rsp] valB ← R[%rsp] | |
执行 | Cnd ← Cond(CC, ifun) | valE ← valB + (-8) | valE ← valB + 8 |
访存 | M8[valE] ← valP | valM ← M8[valA] | |
写回 | R[%rsp] ← valE | R[%rsp] ← valE | |
更新PC | PC ← Cnd ? valC : valP | PC ← valC | PC ← valM |
以代码的第八行指令je来进行说明
注:该指令表明当条件满足(这里是ZF = 1)就发生跳转
- 取指: 得到icode和ifun为7:3,icod:ifun ← M1[0x02e] = 7:3,由于是跳转指令,在取指阶段无法判断下一条指令是往下执行还是跳转执行,因为会先将二者的值都存起来,要跳转的地址放在PC+1的8字节内存中,因此valC ← M8[0x02f] = 0x040,valP存放顺序执行的下一条指令地址,valP ← 0x02e + 9 = 0x037
- 译码:不涉及寄存器的读操作
- 执行:在执行阶段,检查条件码和跳转条件(je表示相等时跳转)来确定是否要选择分支,产生一个信号Cnd,在这里之前的sub指令将所有的条件码都置0,而je不满足条件,因此这里产生的Cnd = 0
- 访存:不涉及内存的读写
- 写回:不涉及寄存器的写入
- 更新PC:检查Cnd,如果Cnd = 1,那么发生跳转到valC,Cnd = 0不发生跳转,下一条指令地为valP
对于这样一条指令的计算的具体操作为:
阶段 | jXX Dest | je 0x040 |
取指 | icod:ifun ← M1[PC] valC ← M8[PC + 1] valP ← PC + 9 | icod:ifun ← M1[0x02e] = 7:3 valC ← M8[0x02f] = 0x040 valP ← 0x02e + 9 = 0x037 |
译码 | ||
执行 | Cnd ← Cond(CC, ifun) | Cnd ← Cond(< 0, 0, 0 > , 3) = 0 |
访存 | ||
写回 | ||
更新PC | PC ← Cnd ? valC : valP | PC ← 0 ? 0x040 : 0x037 = 0x037 |
指令call和指令ret与指令pushq和popq类似,第九行的call指令:
阶段 | call Dest | call 0x041 |
取指 | icod:ifun ← M1[PC] valC ← M8[PC + 1] valP ← PC + 9 | icod:ifun ← M1[0x037] = 8:0 valC ← M8[0x038] = 0x041 valP ← 0x037 + 9 = 0x040 |
译码 | valB ← R[%rsp] | valB ← R[%rsp] = 128 |
执行 | valE ← valB + (-8) | valE ← 128 + (-8) = 120 |
访存 | M8[valE] ← valP | M8[120] ← 0x040 |
写回 | R[%rsp] ← valE | R[%rsp] ← 120 |
更新PC | PC ← valC | PC ← 0x041 |
值得注意的是,访存阶段将函数返回的地址存在了新开辟的栈空间中,也就是将返回地址压栈,而更新PC时,PC指向的是跳转的地址——函数的地址
3. SEQ硬件结构
实现所有Y86指令所需要的计算可以被组织成六个基本阶段:
取指、译码、执行、访存、写回、更新PC
下图是SEQ的硬件结构,一种顺序实现
3.1 取指阶段
- 取指阶段以程序计数器(PC)的值作为起始地址,每次从内存中取10个字节(由于无法提前预测指令的长度,取最长的10字节能保证取出一条完整的指令)
- 指令分成两部分,第一部分为1字节,第二部分为9字节,图中标号为Split的单元处理第一部分,它将第一字节分成两部分,每部分4个比特位,也就是 icode 和 ifun
- 根据icode可以判断指令的状态信息,首先是指令是否合法,其次可以判断指令是否包含寄存器指示符字节和常数字节,这样一来就能计算出指令的长度
- 标号位Align的硬件能够产生寄存器字段和常数字段,并装入相应的rA,rB和valC中
3.2 译码阶段
- 在Y86-64处理器中,寄存器文件有两个读端口,地址输入为srcA和srcB,并通过valA和valB输出
- srcA和srcB可以根据指令代码icode和寄存器指示值rA和rB产生寄存器ID
3.3 执行阶段
- 执行阶段的核心部件是算术逻辑单元,简称ALU,ALU根据指令功能(ifun)来判断对输入的操作数进行相应的运算
- 每次运行时,ALU都会产生三个与条件码相关的信号——零、符号、溢出
- 然而我们只需要在进行算数逻辑运算时才需要更新条件码,因此需要排除计算有效地址和栈指针的增减,这里Set_CC会根据icode来进行筛选
- 标号为cond会根据指令功能和条件码寄存器产生Cnd信号,Cnd = 1,执行跳转;Cnd = 0,不执行跳转
3.4 访存阶段
- 图中的Mem_read和Mem_write根据icode来判断是对内存进行读操作还是写操作
- 访存阶段的最后,会根据图中的信号来设置状态码Stat
3.5 写回阶段
- 写回阶段是将数据写回寄存器文件,两个写接口分别为M和E,对应的地址输入为dstE和detM
- 值得注意的是,当执行条件传送指令(cmovq)时,写入操作还要根据执行阶段计算出的Cnd信号。当不满足条件是,可以将目的寄存器的值设为0XF来禁止写入寄存器文件
3.6 更新PC
PC的值可能有三种情况:
- 如果当前正在执行的指令是调用函数指令call,新的PC等于call指令的常数字段
- 如果当前正在执行的指令是函数返回指令ret,指令ret在访存阶段会从内存(栈)中读出返回地址,这个地址就是新的PC值
- 如果当前正在执行的执行是跳转指令(jXX),当Cnd = 1时,新的PC等于指令的常数字段,当Cnd = 0时,新的PC等于当前指令的PC加上指令长度
4.小结
以上是一个Y86-64处理器的完整设计,但是这种顺序结构会导致指令执行的速度很慢 ——时钟必须很慢才能满足所有操作在一个时钟周期内,为了提高处理器性能,之后会带来更高效地流水线设计