作者:B站搜“九曲阑干”
视频链接:https://www.bilibili.com/video/BV1cD4y1D7uR?p=23
1、为什么要学处理器体系结构
对于软件开发人员,深入理解计算机系统有助于程序的开发和优化,理解处理器是如何工作的能够帮助理解整个计算机系统。虽然真正从事处理器设计工作的人数不多,但是许多人从事智能硬件的相关工作,对于嵌入式系统的设计者,需要了解处理器是如何工作的。
随着国家对集成电路的大力支持,芯片设计与制造的相关工作机会也在增加,因此,从事处理器的设计工作会是一个不错选择。此外,处理器的设计包括了很多好的工程实践原理,它需要完成复杂的任务,而结构设计又要尽可能的简单和规则。
综上所述,了解处理器的工作原理是有必要。
2、Y86-64指令集体系结构
指令系统是计算机软件和硬件交互的接口,程序员根据指令系统设计软件,处理器设计人员根据指令系统实现硬件。
由于x86-64指令系统过于复杂,为了方便学习和理解,CSAPP的原书中参照x86-64的指令系统,自定义了一个相对简单的指令系统Y86-64,该指令系统包括定义各种状态单元、指令集以及它们的编码,编程规范以及异常事件处理这几部分。
2.1 程序员可见的状态
首先,我们来看一下什么是程序员的可见状态,这里的程序员既可以是用汇编代码写程序的人,也可以是产生机器级代码的编译器,可见状态是指每条指令都会去读取或者修改处理器的某些部分,例如内存、寄存器、条件码、程序计数器以及程序状态等。

在Y86-64指令系统中,我们定义了15个64位的程序寄存器,相对于x86-64的指令系统,少了一个寄存器%r15,主要是为了降低指令编码的复杂度,稍后在指令的编码部分可以体会到。
在Y86-64指令系统中,寄存器%rsp也是被定义为栈指针,至于其他14个寄存器没有固定的含义。
除此之外,Y86-64的指令系统还简化了条件码寄存器,仅保留了3个条件码,分别为零标志(ZF)、符号标志(SF)和溢出标志(OF)。
至于程序计数器PC是用来存放当前正在执行指令的地址,注意是指令的地址,不是指令内容。
关于程序的执行状态,我们引入状态码来表示,稍后会有详细的讲述。
2.2 Y86-64指令
类比x86-64的指令集,Y86-64指令集做了一些相应的简化,我们将x86-64中的movq指令分成了四种不同的指令,具体如图所示。

重定义后的数据传送指令显示的指明了源操作数和目的操作数的格式。指令名字的第一个字母表明了源操作数的类型,源操作数可以是立即数(i)、寄存器(r)或内存(m)。指令名字的第二个字母指明了目的操作数的类型,目的操作数可以是寄存器和内存,这样设计的目的主要是为了降低处理器实现的复杂度。
2.3 指令编码
接下来,我们对上述数据传送指令进行编码。

每条指令的第一个字节表明指令的类型,这个字节分为两部分,每一部分占4个比特位,高4位表示指令代码,低4位表示指令功能。
对于我们定义的数据传送指令,不同的指令代码表示不同的指令,指令的功能部分都为0。

当指令中有寄存器类型的操作数时,会附加一个字节,这个字节被称为寄存器指示符字节,它用来指定一个或者两个寄存器,因此还需要对寄存器进行编码。

在Y86-64的指令系统中,我们定义了15个寄存器,虽然每个寄存器定义了不同的名字,但是还需要为每个寄存器指定一个编号,寄存器的编号可以用十六进制数0~0xE来表示,具体如图所示。

注意,如果指令中某个寄存器字段的值为0xF,表示此处没有寄存器操作数,例如irmovq指令中第一个操作数是立即数,所以此处用0xF来填充。

在Y86-64指令系统中,我们定义了四条整数操作指令,它们只能对寄存器数据进行操作,而x86-64还允许对内存数据进行操作,由于这四条指令属于同一类型,所以指令代码是一样的,不同的是功能部分,具体如图所示。

跳转指令一共有7条,跳转的条件与x86-64中的跳转指令是一样的,都是根据条件码的某种组合来判断是否进行跳转。

条件传送指令有6条,它与数据传送指令rrmovq有相同的指令格式,但是只有条件码满足条件时,才会更新目的寄存器的值。

停止指令(halt)可以使整个系统暂停运行;nop指令表示一个空操作,它们的指令编码只有一个字节,比较简单;call指令与ret指令分别实现函数调用和返回;push指令和pop指令分别实现入栈和出栈的操作。这四条指令与x86-64种的指令定义类似。

综上所述,Y86-64的指令集以及编码规则的定义就完成了。
通过上述定义的指令编码规则,我们可以将Y86-64的汇编代码翻译成二进制表示,例如图中这条指令。

根据rmmovq指令的编码定义,指令二进制表示的第一个字节为0x40。

接下来,我们再看指令的操作数部分。根据寄存器的编号规则,寄存器%rsp对应十六进制数0x4,基址寄存器rdx对应0x2,因此,我们可以得到第二个字节的编码为0x42。

指令编码中偏移量占8个字节,我们需要在该偏移量的前面通过填0来补齐8个字节,由于x86-64采用小端法存储,所以需要对偏移量进行字节反序操作,最终我们得到了长度为10个字节的二进制指令,具体如图所示。

2.4 Y86-64异常
最后,我们看一下程序的状态码,它描述了程序执行的总体状态。当代码值为1时,表示程序正常执行,而其他三个代码表示程序发生了某种类型的异常。

代码值2:表示处理器执行了一条停止指令(halt);
代码值3:表示程序正试图从非法地址读取数据或者向非法地址写入数据;
代码值4:表示程序遇到了非法指令。
对于Y86-64,当遇到异常情况时,我们就简单的让处理器停止执行指令。然而在成熟完整的设计中,处理器遇到异常会调用异常处理程序。
3、数字电路与处理器设计
在处理器内部,寄存器文件和算术逻辑单元(ALU)是串联的,寄存器文件的输出端口与ALU的输入端口相连。例如执行图中这条减法指令,ALU从寄存器文件中读取操作数,然后执行减法操作,最后将计算结果写入到寄存器文件中。

下图中展示了一个寄存器文件的功能表述。

它有一个读端口和一个写端口,端口的数据位宽是64位,我们规定读写操作共用地址线,由于我们定义了15个程序寄存器,因此地址线的宽度设计成4位即可满足寻址的要求,此外还有时钟信号、复位信号以及写使能信号。
根据上述功能定义,我们可以使用硬件描述语言(HDL)对寄存器文件进行行为级建模。常用的硬件描述语言有两种,最常用的是Verilog,另外一种是VHDL。
图中这段Verilog程序就是对寄存器文件的描述。

采用电子设计自动化(EDA)工具对这段程序进行逻辑综合,得到的电路就能可以实现预期的功能,逻辑综合的过程与编译有点类似。

看一下这个寄存器文件的内部结构,当执行读取操作时,使用地址线来传输寄存器的编号,多路选择器根据地址信号筛选出寄存器的值,最终数值会通过输出信号输出。

为了方便描述,下面开始暂时将时钟信号和复位信号省掉。
执行写操作需要确定三个参数:目的寄存器的ID、写入的数据以及是否能够写入。说白了就是能不能写,往哪儿写,写什么。输入数据信号线与每一个寄存器单元都相连(为了方便表述,这里我们忽略了数据的位宽);能不能执行写入操作由we信号线来确定,we是write enable的缩写。这个例子中地址信号线是读操作和写操作共用的,经过地址解析后的信号与we信号共同来确定对哪个寄存器执行写操作。

虽然这张图对寄存器文件内部的描述已经比较详尽了,但多路选择器是怎么实现的?虚线框里面的这个寄存器又是如何存储数据的?为了进一步搞清楚这些模块的底层实现,接下来我们看一些数字电路的相关知识。
3.1 逻辑门
逻辑门是数字电路的基本计算单元,例如图中与门、或门、非门等。

它们的输出等于输入值按位进行相应的布尔运算。
逻辑门实际上是由晶体管级电路实现的,在现代计算机中,晶体管通常是指基于CMOS工艺的,CMOS有两种晶体管,一种叫N沟道MOS晶体管,简称N管;另外一种叫P沟道MOS晶体管,简称Р管。

N管和P管都有3个信号端口,分别为栅极、源极、漏极,我们可以将一个P管和一个N管串联起来实现非门,具体实现是将二者的漏极连在一起作为输出,栅极连在一起作为输入,然后将P管的源极接电源,N管的源极接地,具体如图所示。

当输入为高电平时,N管导通,P管不导通,输出为0。

当输入为低电平,P管导通,N管不导通,输出为1。
综上所述就是非门的基本组成以及工作原理。
3.2 组合电路和HCL布尔表达式
在CMOS工艺中,与门和或门实现起来不如与非门和或非门高效,所以在设计CMOS电路的时候最好使用与非门、或非门以及非门来实现,这些基本的门结构都可以P管和N管组合来实现。

接下来,我们看一下如何用基本的门电路实现一个多路选择器。

图中是一个双通道多路选择器的门级表示,当select等于0时,输入端a的数据可以通过该电路到达输出端

当select等于1时,输入端b的数据会到达输出端。

通常,多路选择器有多条输入通道和一条输出通道。
刚才我们看了寄存器文件的内部实现,多路选择器可以实现控制某个特定寄存器的内容传输到ALU的输入端,它可以使用基本的门电路来实现,只不过比双通道要复杂一点。
通常情况下,寄存器文件内的存储部件是由D触发器来实现的,其电路符号如图所示。

这里我们给出一种D触发器的门级实现,具体如图所示。

图中虚线框内的部分被称为D锁存器,关于D触发器的工作原理,这里就不展开描述了。
通过上述的两个示例,可以发现逻辑电路可以使用基本的门电路来搭建,很早以前,电路设计是将一个个门电路绘制在图纸上,但是随着半导体技术的发展,这种方式很难高效的实现大规模的复杂电路。目前,电路的逻辑设计通常采用硬件描述语言来实现,然后采用电子设计自动化(EDA)工具进行综合和后端设计。
硬件描述语言和综合工具的应用使得工程师们更多关注硬件功能的设计,而不是单个晶体管或者逻辑门的设计,接下来,我们看一下D触发器的Verilog实现。

其中dfilpflop表示模块的名称,D、C、G表示输入,Q表示输出。图中这条always语句表示当时钟C上升沿的时候,如果G为1,就把输入D的值赋给触发器的输出Q,否则Q保持不变。
通过这个例子可以发现用Verilog来描述电路比逻辑门要简单的多,Verilog语言的语法跟C语言有许多类似的地方,但表达的含义与C语言有着本质的区别,Verilog程序是并行执行的,而C程序是串行执行的,所以硬件设计人员需要从电路的角度来理解Verilog语言,而不是软件的思维。
学习Verilog语言,首先需要搞清楚组合逻辑电路与时序逻辑电路的区别,这两种电路的主要差异在于是否含有存储单元,其中组合逻辑电路的输出值仅由当前的输入状态来决定,而时序逻辑电路的输出值不仅与当前输入的状态有关,而且与原来的状态也有关。
当年教我们体系结构的老师曾说过,他用Verilog做设计只用三种语句:第一个是assign语句,用于描述组合逻辑;第二个是always语句,用于描述时序逻辑,其中posedge clock表示在时钟上升汾的时候变化;最后一个是模块调用语句。
学会这三种语句,所有的设计都够了。
4、Y86-64的顺序实现
由于x86-64指令系统较为复杂,为了方便学习和理解,Y86-64指令系统做了相应的简化处理,其中指令编码的长度从1个字节到10个字节不等,每条指令都含有一个长度为8个比特位的指令指示符,有的指令含有一个单字节的寄存器指示符,还有的含有一个8字节的常数。

根据图中定义的指令,通过一个例子看一下C程序翻译成的Y86-64汇编代码。

图中这段C代码用来计算一个数组的元素之和,指针start指向数组的起始位置,count用来表示数组的长度。
仅从指令的格式来看,除了数据传送指令,其他的指令与x86-64指令差异不大,使用Y86-64汇编器可以将图中的汇编代码翻译成二进制指令,具体如图所示。

Y86-64汇编器的翻译过程是基于Y86-64指令系统的,图中的二进制指令可以运行在Y86-64的处理器上,由于篇幅限制,我们这里只展示了其中的一部分。
第四章的主要内容是设计一个Y86-64的处理器,用它来执行这些二进制指令。
4.1 将处理组织成阶段
通常,处理器执行一条指令包含很多操作,实现所有Y86-64指令所需要的计算可以被组织成6个基本阶段,分别为取指、译码、执行、访存、写回以及更新PC。

1、首先是取指阶段,这个阶段对于所有的指令都是需要的,在我们定义的Y86-64指令系统中,指令的长度并不是固定的。

取指阶段还会根据指令代码判断指令是否含有寄存器指示符,是否含有常数,从而计算出当前指令的指令长度。

2、译码阶段比较简单,就是从寄存器文件中读取数据,寄存器文件有两个读端口,可以支持同时进行两个读操作。

3、执行阶段,算术逻辑单元(ALU)主要执行三类操作:第一类操作是执行算术逻辑运算;第二类操作是计算内存引用的有效地址;第三类操作是针对push指令和pop指令。

4、访存阶段主要是针对内存的读写操作,既可以从内存读出数据,也可以将数据写入内存。

5、写回阶段与译码阶段类似,都是针对寄存器文件的操作,不同的是译码阶段是读寄存器文件,写回阶段是写寄存器文件。

6、更新PC是将PC设置成下一条指令的地址。
以上我们大致介绍了指令执行的不同阶段,但是并不是所有的指令执行都要经历这6个阶段,接下来,通过几个例子来看一下不同的指令在各个阶段执行的操作。
4.2 subq 的各个阶段
例如图中这条减法指令:

取指阶段会根据指令代码来判断该指令是否包含寄存器指示符,是否包含常数,根据判断结果可以得出该指令的长度。

由于减法指令的源操作数和目的操作数都是寄存器类型的,在译码阶段会根据寄存器指示符来读取寄存器的值。

执行阶段,ALU根据译码阶段读取到的操作数以及指令功能来执行具体的运算,除了输出运算结果,还会根据结果来设置条件码寄存器。

由于减法指令不需要读写内存,因此访存阶段不需要任何操作。
写回阶段将ALU的运算结果写回到寄存器rbx。

最后对程序计数器PC进行更新。

以上就是减法指令相关的操作。
4.3 irmoveq 的各个阶段
接下来,看一下数据传送指令irmoveq。

这条指令执行的操作是将一个立即数传送给寄存器。
取指阶段根据指令代码可以判断该指令既含有寄存器指示符字节,也含有常数字段。

由于这条指令不需要从寄存器文件中读取数据,所以译码阶段不需要执行任何操作。
从表面上看,数据传送指令只是数据搬运,并不需要ALU部件,在实际的硬件中,ALU的输出端口与寄存器文件的写入端口相连,该指令在执行阶段使用ALU对立即数执行加0的操作。

在写回阶段将运算结果写入寄存器文件就完成了数据传送的操作。
由于该指令不涉及内存的读写,所以访存阶段不执行任何操作,执行阶段使用ALU对常数进行加0的操作需要注意一下。

4.4 rmmovq 的各个阶段
接下来,再来看另外一条数据传送指令rmmovq。

取指阶段和译码阶段跟前面讲的类似。

重点看一下执行阶段,ALU根据偏移量和基址寄存器来计算访存地址,访存阶段将寄存器rsp的数值写入内存中,内存地址由执行阶段得出,下需要写寄存器,所以写回阶段不进行任何操作。

通过上面的描述,ALU还可以用来计算内存引用地址。
4.5 pushq 的各个阶段
接下来看一下指令pushq的操作流程。

取指阶段根据指令代码来判断该指令含有寄存器指示符,不含常数,因此,指令长度为2个字节。

需要特别注意的是∶译码阶段不仅需要读寄存器rdx的值,还需要读寄存器rsp的值,这是因为指令pushq要将寄存器rdx的值保存到栈(内存)上。

执行阶段会计算内存地址,具体方法是:将寄存器rsp指向的内存地址进行减8的操作,阶段还需要读寄存器rsp的值。

访存阶段会将寄存器rdx的值写到栈上,具体如图所示。

由于寄存器rsp指向的内存地址发生了变化,没需要更新寄存器rsp的值,最后更新PC的值。

以上就是push指令各个介段所进行的操作。
4.6 je 的各个阶段
最后看一下跳转类指令。

取指阶段根据指令代码来判断该指令含有常数字段,不含寄存器指示符字节,因此,指令的长度为9个字节。

由于不需要读取寄存器文件,所以译码阶段不进行任何操作。
执行阶段,标号为Cond的硬件单元根据条件码和指令功能来判断是否执行跳转,这个模块产生一个信号Cnd,如果Cnd等于1,执行跳转;Cnd=0,不执行跳转。

注意在更新PC的阶段,如果Cnd等于1,就将PC的值设为0x040;如果Cnd等于0,PC的值就等于当前值加上9。

以上就是跳转指令的执行流程。
通过这个统一的框架,能够处理不同类型的Y86-64指令。

虽然指令的行为大不相同,但是我们可以将指令的处理组织成6个阶段。
5、Y86-64处理器硬件结构
5.1 取值阶段的硬件设计
取指阶段以程序计数器(PC)的值作为起始地址,取指操作每次从指令内存中读取10个字节,这是由于在取指操作之前,无法判断当前指令的长度,Y86-64指令系统中最长的指令占10个字节,一次性从内存中取出10个字节可以保证一次取指操作至少可以获取一条完整的Y86-64指令。

接下来,将这10个字节分成两部分,部分占1个字节,另外一部分占9个字节。

下图中标号为split的硬件单元处理第一部分,它将第一个字节分成两部分,每一部分占4个比特位。根据Y86-64指令系统的定义,这两个字段分别为指令代码和指令功能,这里用icode和ifun表示。

根据icode可以确定当前指令的状态信息。首先可以判断这条指令是否是一条合法的指令,如果icode在0x0到xB之间,那么这条指令就是一条合法指令;如果不是,则表示当前指令属于非法指令。此外,根据icode还可以判断当前指令是否包含寄存器指示符字节,以及是否包含常数字节。

根据上述的判断结果,就可以算出当前指令的长度。
例如︰既含有寄存器指示符字节,又含有常数字节,那么当前指令长度就是10个字节;如果既不含寄存器指示符字节,也不含常数字节,那么当前指令长度就是1个字节。
既然通过icode可以获得当前指令的长度,那么指令内存中下一条指令的地址,就可以通过当前PC值加上当前指令的长度计算出来。

我们继续看一下剩余9个字节是如何处理的,标号为Align的硬件单元可以产生寄存器字段和常数字段。当need_regids等于1时,表示该指令包含寄存器指示符字节,那么第一个字节将被分成两部分,每一部分占4个比特位,然后分别装人寄存器指示符rA和rB中;当need regids等于0时,表示这条指令没有寄存器指示符字节,此时rA、rB这两个字段会被置为0xF;当指令中只含有一个寄存器操作数时,同样另外一个字段也会被置为0xF.

如果该指令含有常数, Align单元还产生常数字段valC,同样需要根据信号need_regids的值来判断。当need_regids等于1时,第2字节到第9个自己表示常数字段valC;当need_regids等于0时,第1个字节到第8个字节表示常数字段valC。

5.2 译码阶段的硬件设计
译码阶段是从寄存器文件中读取数据,在Y86-64处理器中寄存器文件有两个读端口,它支持同时进行两个读操作,两个读端口的地址输人为srcA和srcB,从寄存器文件中读出的数值通过valA和valB输出。

图中标号为srcA和srcB的圆角矩形块可以产生寄存器的ID值,产生寄存器的ID值需要指令代码icode以及寄存器指示值rA和rB。

读取寄存器的数据,需要rA和rB比较容易理解,那么为什么还需要指令代码icode呢?
例如push指令,该指令的寄存器指示符中只含有目的寄存器的ID值,当执行压栈操作时,还需要获得栈顶指针rsp的值。

不仅仅是push指令,实际上对于图中这四条指令,在译码阶段都是需要读寄存器rsp的内容,所以译码阶段不仅需要rA和rB信号,还需要icode信号。

5.3 执行阶段的硬件设计
执行阶段的核心部件是算术逻辑单元,简称ALU。ALU根据指令功能(ifun)来判断对输人的操作数进行何种运算,每次运行时,ALU都会产生三个与条件码相关的信号——零、符号、溢出。

不过,我们只希望ALU在执行算术逻辑指令时才会设置条件码,当ALU计算内存引用地址以及对栈进行操作时,并不会设置条件码。因此,图中Set_CC会根据指令代码icode来挫制是否要更新条件码寄存器。

标号为Cond的硬件单元会根据指令功能和条件码寄存器产生一个Cnd信号,对于跳转指令,如果cnd等于1,执行跳转;如果cnd等于0,则不执行跳转。

对于ALU,不仅可以执行算术逻辑指令,还要涉及内存地址的计算以及栈指针的增加或减少的操作。
5.4 访存阶段的硬件设计
访存阶段的任务就是从内存中读数据或者将数据写入内存中。

图中的读控制块表明应该进行读操作,写挫制块表明应该进行写操作。
此外还有产生内存地址和输人数据的挫制块,具体如图所示。

需要注意的是访存阶段的最后操作,会根据图中的信号来计算状态码Stat。
5.5 写回阶段的硬件设计
写回阶段是将数据写入到寄存器文件。

两个写端口分别为M和E,对应的地址输人为dstE和dstM。这里需要注意的是,当执行条件传送指令(cmov)时,写入操作还要根据执行阶段计算出的cnd信号,当条件不满足条件时,以将目的寄存器设置为0xF来禁止写入寄存器文件。

5.6 更新PC阶段的硬件设计

PC的值可能有三种情况:
第一种情况,如果当前正在执行的指令是函数调用指令Call,那么新的PC就等于call指令的常数字段;
第二种情况,如果当前正在执行的指令是函数返回指令ret,指令ret在访存阶段会从内存(栈)中读出返回地址,这个返回地址就是新的PC值;
第三种情况,如果当前正在执行的指令是跳转指令(jxx),当cnd信号等于1时,也就是满足跳转条件时,此时新的PC等于跳转指令的常数字段,当不满足跳转条件时,跳转指令与其他指令一样,新的PC等于当前PC的值加上当前指令的长度。
以上就是一个Y86-64处理器的完整设计,不过这种顺序结构存在一个问题︰就是指令的执行速度太慢了,时钟必须非常慢,这样才能使得所有的操作在一个时钟周期内完成。
本文介绍Y86-64指令集体系结构及其处理器设计,包括指令编码、异常处理等内容,并详细解释了处理器各阶段的硬件实现。
1286

被折叠的 条评论
为什么被折叠?



