206-MIPS指令简介
# 206-MIPS指令简介
MIPS秉承着指令数量少, 指令功能简单的设计理念,那这样的设计理念是如何实现的呢?在这一节,我们就将来分析MIPS指令的特点。
相比于X86指令所提供的动辄上千页的指令说明,MIPS指令 只用这两页纸就可以说清楚了。
# 基本格式
MIPS指令的基本格式就分为这三种,R型,I型和J型。R型指的是寄存器型, I型指的是立即数型,J型指的是转移型。
我们用这张表对MIPS的指令进行不同纬度的分类,横轴是按照指令的格式分为R型、I型和J型, 纵轴则是根据指令的功能类型分为运算指令、 访存指令和分支指令。
# R型指令
首先,我们来看指令格式为R型的运算指令。 R型指令总共包含六个域,其中最高位的opcode域 是六个比特,最低位的funct域也是六个比特, 中间的四个域,均为五个比特, 我们分别来看各个域的用途。
opcode域,用于指定指令的类型,对于所有的R型指令,这个域的值,均为零, 但这并不是说明R型指令只有一种,它还需要用funct域来更为精确的指定指令的类型。所以说, 对于R型指令,实际上一共有12个比特操作码, 那大家可以思考一下,为什么不将opcode域和funct域合并成一个12比特的域呢? 那样岂不是更直观明了吗?
我们再来看这些5比特的域。
rs域,这个域通常用来指定第一个源操作数所在的寄存器编号,
rt域通常用来指定第二个源操作数所在的寄存器的编号,
rd域通常用来指定目的操作数的寄存器编号, 也就是保存运算结果的地方。 5个比特的域可以表示0-31的数, 正好对应Mix的体系结构中的32个通用寄存器,
还剩下最后一个域,它指示的是移位操作的位数。因为对于32比特的数,5比特的域正好可以表示0-31的移位位数。那这个域只是对于移位指令有用,对于非移位指令,这个域被设为0,
我们来看一个例子,这是将9号寄存器和10号寄存器中的数相加,把运算结果保存在8号寄存器中, 那我们通过这条汇编指令的描述,如何得到mix指令的二进制编码呢? 这其实很容易。首先,我们查询MIPS指令编码表,就可以得到 加法指令的opcode域应该是0,funct域应该是32, 因为它不是移位指令,所以移位的域被设为0,然后我们根据这条指令的操作数 可以得到目的操作数,也就说rd这个域等于8,第一个源操作数应该是9,第二个源操作数应该是10, 这样我们把各个域的数值转换成二进制数,填写到对应的位置, 就可以得到这条指令的二进制编码了。
MIPS指令系统简洁明了的规则可以让我们非常容易的对指令进行这样的手工编码转换, 同样也说明了CPU对这样的指令进行硬件的译码也会非常的方便。
# I型指令
如果指令中需要用到立即数,那么就要用到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位的,再加上各个寄存器位域的使用, 。
我们来看一个例子,对于加法,如果我们想让其中的 源操作数是一个立即数的话,就可以用addi这个指令, 注意它和add指令是不一样的。add指令的操作数必须都是寄存器。 我们再来练习一下手工转换指令的编码。我们通过查指令编码表, 可以发现 addi 指令的opcode域是8,从这一点我们也可以看出, addi和add虽然只有一个字母的差别,但是他们指令格式是完全不一样的。 剩下的域我们通过分析这条指令的操作数就可以得到, rs域,等于22,rt域=21,立即数域 等于-50,我们将这些数转换成二进制,就可以得到这条指令的编码了。
# J型指令
然后我们来看所有的分支指令,分支指令是用于改变控制流的指令,其实就相当于X86当中的转移指令。 在MIPS中,分支指令也分为条件分支,和非条件分支两种。对于条件分支有两条指令,beq和bne, 对于非条件分支,只有一条指令,j
我们先来看条件分支指令,条件分支指令实际上是i型指令。 这就是两条条件分支指令,他们的opcode域分别是4和5, 我们以beq指令为例,它共有三个操作数,前两个是寄存器操作数, 第三个操作数是存储器地址,也就是一个立即数,CPU会判断第一个寄存器当中的数和第二个寄存器当中的数是否相等。如果相等就跳转到 L1所指向的寄存器单元取出下一条指令,否则, 顺序执行deq之后的那条指令。
我们需要注意, 这里和X86的条件转移指令有很大的不同。,它就在 一条指令当中即进行了比较,又完成了转移, 我们还记得MIPS的全称,就是为了减少指令流水线的互锁,也就说要尽量 避免不同指令之间相互的影响。而标志位这件事,很明显就是前一条指令运行的结果,可能会对后面的某一条指令产生影响,这是MIPS指令设计时要尽量避免的。所以BEQ指令也很好的体现了MIPS的这一设计理念。
我们来看一个例子。这段C语言代码是我们经常会写的。 如果把它转换为MIPS指令,是这样的,第一条BEQ指令, 如果S3寄存器和S4寄存器内容相同,则转移到 True 所对应的这行指令。那么S3和S4中保存了I和J这两个变量, 如果他们内容相同,会转移到这里,执行加法指令, 也就对应于F=G+H
如果他们不等, 则会顺序的执行下一条指令,也就一条减法指令对应于F=G-H 执行完之后,会跳过这条加法指令,然后进入后面的代码
从条件分支指令的格式可以看出,目标地址只能使用十六位的位移量, 这是一个很大的局限,但是我们还得考虑如何充分发挥这十六位的作用。 如果以当前的PC寄存器为基准,在MIPS中,指向下一条指令地址的寄存器称为PC, 类似于X86中的IP寄存器。这个寄存器,是指向32位寄存地址的。 如果以它为基准(peterjxl注,应该是指段寄存器),十六位位移量可以表示出当前指令前后2的15次方字节这么一个范围。
但是我们要注意一点,MIPS的指令长度固定位32个比特,因此每条指令的位置, 一定会在四个字节对齐的地方,这样的地址,最低两位肯定为0。 所以我们实际上可以用十六位的位移量去指示每四个字节为一个单位的地址。 这样就可以把目标地址的范围扩大四倍,可以达到前后128kB。(这里看的不是很懂)
在这样的条件下,目标地址应该这么计算, 当分支条件不成立时,下一条指令的地址就等于当前的pc+4。
如果分支条件成立,那下一条指令的地址就等于已经加了4的pc, 再加上这个立即数乘以四。
然后我们来看非条件分支指令,相比于条件分支指令,有两个寄存器域用于比较条件,那如果我们不需要判断条件,我们就可以想办法扩大目标地址的范围。 当然理想情况下是直接使用32位的地址, 但还是因为MIPS的指令长度固定为32位,而每条指令至少需要有opcode域,指示它的指令类型。 这就占用了六个bit。那我们把剩下的26个bit全都用于目标地址, 这就是J型指令。
在考虑到MIPS指令是四字节对齐的这个情况, 对于J型指令,下一条指令的地址的计算方法可以是将当前的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制定系统体系结构, 它不愧为精简指令系统的经典设计,指令简洁,而且精巧。
#
作者:花藤麻
链接:https://www.zhihu.com/question/26856453/answer/38277147
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。假设我的CPU是32位数据线,那么一次取数据必须是4字节,不可能只取一字节。如果我非要取一字节的话,也只能从内存里一次取4字节,然后把其中的一字节拿来使用。
回到开始的32位假设,我若是要取地址为0~3中的任意一个字节,很容易实现,CPU会把取到的4个字节中拿一个给你用。具体算法是当地址部件发出逻辑地址的时候,CPU会把地址信号中的高30位当成32数据线的地址偏移,低2位会当成32位数据线的线内偏移。
如果你取0~3字节的内容,高30位是0,数据线只需要传输第0个32位的数据过来给你选择就行了,如果取4~7字节的内容,高30位就是1,只需要传输第1个32的数据过来,然后4号地址对应的偏移是0,7号地址对应的偏移是3,以此类推。这是取单字节的情况。也就是说CPU传输数据的时候始终是4字节对齐的,从0开始,从4开始。假如一次取2个字节的话,假设数据线对准了高30位对准了0,那么你可以从0、1、2取两个字节,但是不能从3取,因为从3取的话还需要取第四字节的内容,这时候需要把地址线的高30位调整成0000000……0001来。如果一次取3字节的话,只能从0、1开始,对于一次取4字节的话只能从0开始了。
通俗来说就是“如果一次取一个数据块出来,必须使这个块在数据线对准的范围之内,否则只能移动数据线对准的地方,也就是多次才能取出来”。这就是地址对齐的原因了,而且大部分CPU不能一次取3字节。对于一次取一个字节,无需对齐,因为数据线始终能在第一次瞄准的时候就把这个字节取出来。其他数据块的读取就需要考虑CPU的数据宽度了
发布于 2015-01-28 13:02
如果你有两张银行卡都有1000块,你要取1000块当零花,你是直接到ATM从一张里取1000方便呢还是跑两个银行每张各取500方便?
处理器可以在1个时钟周期内从对齐的地址中取出相应字节数到寄存器中,而如果地址不对齐,必须在两个对齐的相邻地址中各取一部分,拼在一起,一般来说至少是2个时钟周期。
作者:Felix Qiu
链接:https://www.zhihu.com/question/26856453/answer/34298078
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
#
认识字节对齐
struct Data1
{
char a;
int b;
short c;
};
struct Data2
{
int a;
char b;
short c;
};
int main()
{
cout << sizeof(Data1) << endl;
cout << sizeof(Data2) << endl;
getchar();
return 1;
}
输出结果:
12
8
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
sizeof(Data1)和sizeof(Data2)分别表示Data1和Data2内存占用字节数,输出结果不一样是因为编译时对Data1和Data2做了不同的字节对齐。Data1的对齐为4Byte,Data2的对齐是2Byte。
假定存储起始地址为0x00,存储模型如下:
结构体或类中的每个成员都是内存对齐的。
编码时可以使用#pragma pack(x)来指定字节对齐大小,x必须为2的n次方,否则设定的字节对齐大小不生效。如上段代码开头加上#pragma pack(4),输出结果均为12。
为什么要字节对齐
首先明确:CPU从内存中读取数据的起始地址是对齐的。如下内存存储,cpu一次读取8个字节,对于int型数据则需要两次读取。如不对齐会降低执行效率。
内存对齐目的:为了让CPU一次性获得基本类型的数据,从而提升程序执行效率。
发布于 2019-10-10 17:26