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个通用寄存器。
还剩下最后一个域 shamt,它指示的是移位操作的位数。因为对于32比特的数,5比特的域正好可以表示0-31的移位位数。那这个域只是对于移位指令有用,对于非移位指令,这个域被设为0。
我们来看一个例子,这是将9号寄存器和10号寄存器中的数相加,把运算结果保存在8号寄存器中。那我们通过这条汇编指令的描述,如何得到MIPS指令的二进制编码呢?这其实很容易。首先,我们查询MIPS指令编码表,就可以得到加法指令的opcode域应该是0(十进制),funct域应该是32。因为它不是移位指令,所以移位的域shamt被设为0。然后我们根据这条指令的操作数,可以得到目的操作数也就说rd这个域等于8,第一个源操作数应该是9,第二个源操作数应该是10。这样我们把各个域的数值转换成二进制数,填写到对应的位置,就可以得到这条指令的二进制编码了。
MIPS指令系统简洁明了的规则可以让我们非常容易的对指令进行这样的手工编码转换,同样也说明了CPU对这样的指令进行硬件的译码也会非常的方便。
如果指令中需要用到立即数,那么就要用到I型指令。
因为R型指令当中只有一个5比特的域,也就是shamt移位这个域可以用来表示立即数,那能表示的数的范围为0-31。在程序中常用的立即数远大于这个范围,所以R型指令并不适用,我们需要新的指令格式。这就是I型指令,I型指令的大部分域与R型指令是相同的。
I型指令的第一个域,也是opcode域,用于指定指令的类型。但它没有funct域,所以不同的I型指令,其opcode域是不一样的。
第二个域rs,指定了第一个源操作数所在的寄存器编号;
第三个域rt,用于指定目的操作数。
I型指令与R型指令不同,它只有两个寄存器数域。
剩下的16位被整合成了一个完整的域,可以存放16位的立即数,可以表示2的十六次方个不同的数值。对一般的访存指令,我们需要用一个寄存器加上一个立即数来指示一个内存单元。那么这个立即数就是访存地址的偏移量,16位的立即数,可以访问正负32K(216−1=32K216−1=32K字节)的范围,那如果我们还想跳转到更远的地址,应该怎么办呢?有一个很简单的方法就是两次调用J指令。第一条J指令尽可能跳到最远的地方,然后在那个目标地址再放一条J指令,像接力一样再跳一次。这个方法很简单,但是用起来不算太方便。那么还可以用什么方法呢?大家还记得我们曾说过间接转移指令吗?MIPS中也可以用同样的方法,这就是jr指令。jr指令有一个寄存器操作数,可以把要转移的目标地址放到寄存器当中,这样就可以使用32位的目标地址了,但是这样的指令显然无法用J型指令来实现。那么需要新增一种指令类型吗?其实也不需要,我们就用原来的R型指令就可以很好的实现。只用占用其中的一个寄存器位域,然后新增一种funct的编码就可以了。