MIPS秉承着指令数量少,指令功能简单的设计理念,那这样的设计理念是如何实现的呢?在这一节,我们就将来分析MIPS指令的特点。
相比于X86指令所提供的动辄上千页的指令说明,MIPS指令只用这两页纸就可以说清楚了。MIPS指令的基本格式就分为这三种,R型,I型和J型。R型指的是寄存器型,I型指的是立即数型,J型指的是转移型。我们用这张表对MIPS的指令进行不同纬度的分类,横轴是按照指令的格式分为R型、I型和J型,纵轴则是根据指令的功能类型分为运算指令、访存指令和分支指令。首先,我们来看指令格式为R型的运算指令。R型指令总共包含六个域,其中最高位的opcode域
是六个比特,最低位的funct域也是六个比特,中间的四个域,均为五个比特,我们分别来看各个域的用途。opcode域,用于指定指令的类型,对于所有的R型指令,这个域的值,均为零,但这并不是说明R型指令只有一种,它还需要用funct域来更为精确的指定指令的类型。所以说,对于R型指令,实际上一共有12个比特操作码,那大家可以思考一下,为什么不将opcode域和funct域合并成一个12比特的域呢?那样岂不是更直观明了吗?我们再来看这些5比特的域。RS域,这个域通常用来指定第一个源操作数所在的寄存器编号,rt域通常用来指定第二个源操作数所在的寄存器的编号,rd域通常用来指定目的操作数的寄存器编号,也就是保存运算结果的地方。5个比特的域可以表示0-31的数,正好对应MIPS的体系结构中的32个通用寄存器,还剩下最后一个域,它指示的是一位操作的位数。因为对于32比特的数,5比特的域正好可以表示0-31的移位位数。那这个域只是对于移位指令有用,对于非移位指令,这个域被设为0,我们来看一个例子,这是将9号寄存器和10号寄存器中的数相加,把运算结果保存在8号寄存器中,那我们通过这条汇编指令的描述,如何得到MIPS指令的二进制编码呢?
这其实很容易。首先,我们查询MIPS指令编码表,就可以得到加法指令的opcode域应该是0,funct域应该是32,因为它不是移位指令,所以移位的域被设为0,然后我们根据这条指令的操作数可以得到目的操作数,也就说rd这个域等于8,第一个源操作数应该是9,第二个源操作数应该是10,这样我们把各个域的数值转换成二进制数,填写到对应的位置,就可以得到这条指令的二进制编码了。
MIPS指令系统简洁明了的规则可以让我们非常容易的对指令进行这样的手工编码转换,同样也说明了CPU对这样的指令进行硬件的译码也会非常的方便。
如果指令中需要用到立即数,那么就要用到I型指令,因为R型指令当中只有一个5比特的域,也就说移位这个域 可以用来表示立即数,那能表示的数的范围为0-31,在程序中常用的立即数远大于这个范围,所以R型指令并不适用,我们需要新的指令格式。这就是I型指令,I型指令的大部分域与R型指令是相同的,I型指令的第一个域,也是opcode域,用于指定指令的类型,但它没有funct域,所以不同的I型指令,及opcode域是不一样的。第二个域rs,指定了第一个源操作数所在的寄存器编号,第三个数rt用于指定目的操作数,I型指令与R型指令不同,它只有两个寄存器数域,剩下的16位被整合成了一个完整的域,可以存放16位的立即数,可以表示2的十六次方个不同的数值。对一般的访存指令,我们需要用一个寄存器,加上一个立即数来指示一个内存单元,那么这个立即数就是访存地址的偏移量,16位的立即数,可以访问正负32K的空间,对于一般的访存指令来说,就可以满足了。而对于运算指令,虽然无法满足全部的需求,但是大多数情况下,16位也可以使用了。在这一点上,就可以体现出X86这样的CISC指令系统的优势,对于X86指令来说,如果它想使用更大宽度的立即数,它可以很容易的扩展,因为它的指令本来就没有限制长度,但是MIPS指令就不行。它的指令总长度就是32位的,再加上各个寄存器位域的使用,所以I型指令最多只能使用十六位的立即数。
我们来看一个例子,对于加法,如果我们想让其中的 源操作数是一个立即数的话,就可以用add i这个指令, 注意它和add指令是不一样的。add指令的操作数必须都是寄存器。
我们再来练习一下手工转换指令的编码。我们通过查指令编码表, 可以发现add
i指令的opcode域是8,从这一点我们也可以看出, add
i和add虽然只有一个字母的差别,但是他们指令格式是完全不一样的。
剩下的域我们通过分析这条指令的操作数就可以得到,
rs域,等于22,rt域=21,立即数域
等于-50,我们将这些数转换成二进制,就可以得到这条指令的编码了。
然后我们来看所有的分支指令,分支指令
是用于改变控制流的指令,其实就相当于X86当中的转移指令。
在MIPS中,分支指令也分为条件分支,和非条件分支
两种。对于条件分支有两条指令,beq和bne, 对于非条件分支,只有一条指令,j,
我们先来看条件分支指令,条件分支指令实际上是i型指令。
这就是两条条件分支指令,他们的opcode域分别是4和5,
我们以beq指令为例,它共有三个操作数,前两个是寄存器操作数,
第三个操作数是存储器地址,也就说一个立即数,CPU会判断第一个寄存器当中的数
和第二个寄存器当中的数是否相等。如果相等就跳转到 LE所指向的寄存器单元取出下一条指令,否则,
顺序执行deq之后的那条指令。我们需要注意,
这里和X86的条件转移指令有很大的不同。MIPS没有标志寄存器,它就在
一条指令当中即进行了比较,又完成了转移,
我们还记得MIPS的全称,就是为了减少指令流水线的互锁,也就说要尽量
避免不同指令之间相互的影响。而标志位这件事,很明显就是前一条指令运行的结果,可能
会对后面的某一条指令产生影响,这是MIPS指令设计时 要尽量避免的。所以BEQ指令也很好的体现了MIPS的这一设计理念。
我们来看一个例子。这段C语言代码是我们经常会写的。
如果把它转换为MIPS指令,是这样的,第一条BEQ指令,
如果S3寄存器和S4寄存器内容相同,则转移到
Q所对应的这行指令。那么S3和S4中保存了I和J这两个变量,
如果他们内容相同,会转移到这里,执行加法指令, 也就对应于F=G+H,如果他们不等,
则会顺序的执行下一条指令,也就一条减法指令对应于F=G-H
执行完之后,会跳过这条加法指令,然后进入后面的代码,
从条件分支指令的格式可以看出,目标地址只能使用十六位的位移量,
这是一个很大的局限,但是我们还得考虑如何充分发挥这十六位的作用。
如果以当前的PC寄存器 为基准,在MIPS中,指向下一条指令地址的寄存器称为PC,
类似于X86中的IP寄存器。这个寄存器,是指向32位寄存地址的。
如果以它为基准,十六位位移量可以表示出 当前指令前后2的15次方字节这么一个范围,
但是我们要注意一点,MIPS的指令长度固定位32个比特,因此每条指令的位置,
一定会在四个字节对齐的地方,这样地址,最低两位肯定为0,。
所以我们实际上可以用十六位的位移量去指示每四个字节为一个单位的地址。
这样就可以把目标地址的范围扩大四倍,可以达到前后128kB。
在这样的条件下,目标地址应该这么计算,
当分支条件不成立时,下一条指令的地址就等于当前的pc+4。
如果分支条件成立,那下一条指令的地址就等于已经加了4的pc, 再加上这个立即数乘以四。
然后我们来看非条件分支指令,相比于条件分支指令,有两个寄存器域用于
比较条件,那如果我们不需要判断条件,我们就可以想办法扩大目标地址的范围。
当然理想情况下是直接使用32位的地址, 但还是因为MIPS的指令长度固定为32位,而每条指令
至少需要有opcode域,指示它指令类型。
这就占用了六个bit。那我们把剩下的26个bit全都用于目标地址,
这就是J型指令。在考虑到MIPS指令是四字节对齐的这个情况,
对于这一行指令,下一条指令的地址的计算方法可以是将当前的pc加四之后,
取最高的四位,再加上J型指令编码中的26位,
然后在末尾填上两个零,虽然目标地址的范围还不能达到整个 4G的空间,但比之前的条件分支指令已经扩大了很多。
我们用一个例子来进行进一步的说明。
假设我们在高级语言中用的若干变量与寄存器的对应关系是这样的,
那我们就可以用这样一种方式来实现这段c语言的代码,
第一条指令是判断i和j是否相等,如果不相等, 则转移到else这个标号所对应的位置,
也就是执行一条减法指令对应于f=g-h,如果判断条件不成立,
也就是i=j的时候,顺序地执行下一条加法指令,
也就对应于f=g+h。然后用无条件分支指令
跳到else条件之后继续执行后面的程序。
我们现在已经知道这个J型指令的目标地址可以是当前指令
前后256MB的范围,那如果我们还想跳转到更远的地址, 应该怎么办呢?有一个很简单的方法
就是两次调用J指令,第一条J指令尽可能跳到最远的地方,
然后在那个目标地址再放一条J指令,像接力一样再跳一次。
这个方法很简单,但是用起来不算太方便,那么还可以用什么方法呢?
大家还记得我们曾说过间接转移指令吗?MIPS中也可以用同样的方法,
这就是jr指令。jr指令有一个寄存器操作数,
可以把要转移的目标地址放到寄存器当中,这样就可以使用32位的目标地址了,
但是这样的指令显然无法用J型指令来实现,
那么需要新增一种指令类行吗?其实也不需要,我们就用原来的r型指令就可以很好的实现。
只用占用其中的一个寄存器位域,然后新增一种function的编码就可以了。
这就是MIPS指令系统的核心内容,我们只用 熟悉这两页的内容就可以轻松的掌握MIPS的指令了。
我们已经介绍完了MIPS制定系统体系结构,
它不愧为精简指令系统的经典设计,指令简洁,而且精巧。
Reference:北京大学陆俊林老师计算机组成原理课程
Notice:如有侵权,请告知我,我会删除,谢谢!