计算机指令就是指挥机器工作的指示和命令,程序就是一系列按一定顺序排列的指令,执行程序的过程就是计算机的工作过程。通常一条指令包括两方面的内容: 操作码和操作数,操作码决定要完成的操作,操作数指参加运算的数据及其所在的单元地址。MIPS所有指令都是32位的,操作码占用高6位(bit31-bit26)表示,低26位按格式划分为R型、I型和J型。但是按mips指令的功能划分,分别介绍运算指令、访存指令、分支和跳转指令、原子指令等。
同时在学习MIPS指令时,需要了解64位处理器中,CPU一次可以从主存加载的操作数有1字节、2字节、4字节和8字节。在c语言和汇编中的对应关系如下表:
c语言 |
MIPS名称 |
大小(字节) |
汇编助记符 |
char |
byte |
1 |
lb中的“b” |
short |
halfword |
2 |
lh中的“h” |
int |
word |
4 |
lw中的“w” |
long(long long) |
dword |
8 |
ld中的“d” |
表1 c语言数据类型大小和汇编助记符
理解了表1中,在接下来的汇编语言学习中,当加载数据时就能清楚的知道该使用那种指令。
1 运算指令
运算型指令完成寄存器值的算术、逻辑、移位、乘法和除法等操作。运算型指令包含了寄存器指令格式(R型)和立即数指令格式(I型),运算方式有三目运算和两目运算。 下面我们详细介绍:
1.1 加减运算
mips中和加法运算相关的指令如下:
指令 |
格式 |
功能概述 |
ADD |
ADD rd,rs,rt |
加法 |
ADDU |
ADDU rd, rs, rt |
无溢出检测加法 |
ADDI |
ADDI rd,rs,immediate |
带立即数的加法 |
ADDIU |
ADDIU rt, rs, immediate |
无溢出检测的带立即数的加法 |
DADD |
DADD rd, rs, rt |
64位加法 |
DADDU |
DADDU rd, rs, rt |
64位无溢出检测加法 |
DADDI |
DADDI rt, rs, immediate |
64位带立即数加法 |
DADDIU |
DADDIU rt, rs, immediate |
64位无溢出检测立即数加法 |
SUB |
SUB rd, rs, rt |
减法 |
SUBU |
SUBU rd, rs, rt |
无溢出检测的减法 |
DSUB |
DSUB rd, rs, rt |
64位减法 |
DSUBU |
DSUBU rd, rs, rt |
64位无溢出检测的减 |
表2 加法/减法指令
从上表可以看出,MIPS加法指令可以完成两个寄存器的加法运算、寄存器和立即数的运算。运算可以是32位的也可以是64位的(带D标识的)。其中rd、rs、rt是通用寄存器的任意一个。这里有“U”的代表“无溢出检测”,否则就是有溢出检测。比如下面例子:
add t0,a0,a1 # t0=a0+a1
addu t0,a0,a1 # t0=a0+a1
这两句都实现的是a0+a1,结果存入t0。但是add 指令对加结果有溢出检测(仅支持有符号运算),如果a0+a1结果有溢出的话,那么t0不会被改变,程序会报“整型溢出异常”。而addu指令表示“无溢出检测”,a0+a1的结果直接存入t0。
c和c++语言不支持“整型溢出异常”,所以总是使用带”U”的指令。
这里的“I”代表立即数。立即数就是嵌入在指令中的常数。加法运算中的立即数都占用16位,所以都需要有符号扩展到32位或者64位,然后再参与加法运算,比如下面的指令:
addiu t0,a0,0x3 # t0=a0+sign_extend(0x3)
daddiu t0,a0,0x3 #t0=a0+sign_extend(0x3)
同样是实现寄存器a0和立即数0x3的加法运算,但是addiu首先把立即数0x3进行有符号扩展(sign_extend)到32位,然后和寄存器a0进行加法操作,正确结果被存入t0。而daddiu实现的是64位加法运算,所以需要先把立即数0x3进行有符号扩展(sign_extend)到64位,然后和寄存器a0进行加法操作,正确结果被存入t0。
这里解释一下为何要对立即数进行有符号扩展。下图是addiu指令的具体格式:
图2 addiu指令格式
从上图可以看出,addiu是I型指令,立即数占用16位。而addiu实现的是32位加法操作,所以需要把立即数进行符号扩展到32位,然后和rs(a0)进行加法操作,结果存入rt。
再介绍一下符号扩展。符号扩展是指计算机对于小字节转换成大字节的规则。比如要16个字节的数转换成32字节,多出来的16个字节到底怎么填充?mips指令集中会经常需要将其中的立即数进行符号扩展。符号扩展分无符号扩展和有符号扩展,区别如下:
16位立即数 |
0x8000 |
0xe000 |
有符号扩展到32位 |
0xffff8000 |
0x0000e000 |
无符号扩展到32 |
0x00008000 |
0x0000e000 |
表3 符号扩展方式
从上表可以看出,有符号扩展就是高位填充的值取决于这个数扩展前的最高位。扩展前的数高位为1,那么扩展出来的高位就都是1,扩展前的数高位为0,那么扩展出来的高位就都是0。而无符号扩展就是无论扩展前的数是多少,扩展出来的高位都是0。
减法指令中没有带立即数的操作,只区别是32位和64位减法,减法是否有溢出。比如:
sub t0,a0,a1 # t0 = a0-a1
dsubu t0,a0,a1 # t0 = a0-a1
都是实现a0减a1,结果存入t0操作。dsubu支持64位的减法,而且没有整数溢出异常。
1.2 乘除运算
MIPS中乘法和除法指令如下表:
指令 |
格式 |
功能简述 |
MUL |
MUL rd, rs, rt |
乘法运算 |
MULT |
MULT rs, rt |
乘法运算,结果低32位存入LO,高32位存入HI |
MULTU |
MULTU rs, rt |
无符号乘法运算,结果低32位存入LO,高32位存入HI |
DMULT |
DMULT rs, rt |
64位乘法运算,结果低64位存入LO,高64位存入HI |
DMULTU |
DMULTU rs, rt |
64位无符号乘法运算,结果低64位存入LO,高64位存入HI |
MADD |
MADD rs, rt |
乘加运算,(HI,LO) ← (HI,LO) + (GPR[rs] × GPR[rt]) |
MADDU |
MADDU rs, rt |
无符号的乘加运算 |
MSUB |
MSUB rs, rt |
乘减运算,(HI,LO) ← (HI,LO) - (GPR[rs] × GPR[rt]) |
MSUBU |
MSUBU rs, rt |
无符号乘减运算 |
DIV |
DIV rs, rt |
除法运算,商存入LO,余数存入HI |
DDIV |
DDIV rs, rt |
64位除法运算,商存入LO,余数存入HI |
DIVU |
DIVU rs, rt |
无符号除法运算,商存入LO,余数存入HI |
DDIVU |
DDIVU rs, rt |
64位无符号除法运算,商存入LO,余数存入HI |
MFHI |
MFHI rd |
拷贝HI到rd |
MTHI |
MTHI rs |
拷贝rs到HI |
MFLO |
MFLO rd |
拷贝LO到rd |
MTLO |
MTLO rs |
拷贝rs到LO |
表4 乘法/除法指令
上表中完成了和乘法/除法相关的算术运算。运算过程中,操作数(rs,rt)默认按有符号计算,如需完成无符号数的乘法/除法操作,指令里面要带“U”,所以这张表里面的“U”意为”无符号”。”T”代表”结果临时存储“,也就是需要特殊寄存器HI/LO存储运算结果。下面举例说明:
mul t0,a0,a1 #t0 = a0*a1
mult a0,a1 # (HI,LO) ← (a0*a1)
multu a0,a1 # (HI,LO) ← (a0*a1) a0和a1均为无符号数
dmult a0,a1 # (HI,LO) ← (a0*a1) 64位运算
mul 实现了2个32位有符号数乘法运算,得出可以是64位的结果并存入t0。
mult 也实现两个32位有符号数乘法运算,得出可以是64位的结果,但是结果的低32位有符号扩展到64位后存入寄存器LO,高32位有符号扩展到64位后存入寄存器HI。multu 同mult指令,区别是a0和a1按32位无符号数运算。
dmult 实现的是两个64位有符号数乘法运算,可以得出一个128位的结果。结果的低64位存入LO。高64位存入HI。
接下来在看另一组运算:
madd a0,a1 # (HI,LO) ← (HI,LO) + (a0*a1)
maddu a0,a1 # (HI,LO) ← (HI,LO) + (a0*a1) a0和a1均为无符号数
msub a0,a1 # (HI,LO) ← (HI,LO) - (a0*a1)
msubu a0,a1 # (HI,LO) ← (HI,LO) - (a0*a1) a0和a1均为无符号数
madd实现了两个32位寄存器的有符号乘法运算后,结果的低32位累加到LO寄存器,高32位累加到HI寄存器。maddu功能同madd,区别是操作数a0和a1为无符号数。
msub实现了两个32位寄存器的有符号乘法运算后,结果的低32位累减到LO寄存器,高32位累减到HI寄存器。msubu功能同msub,区别是操作数a0和a1为无符号数。
下面在看除法运算:
div a0,a1 # (HI, LO) ← a0 / a1
divu a0,a1 # (HI, LO) ← a0 / a1 ,a0和a1均为无符号数
ddiv a0,a1 # (HI, LO) ← a0 / a1
ddivu a0,a1 # (HI, LO) ← a0 / a1 ,a0和a1均为无符号数
div实现两个有符号32位寄存器的除法运算,商有符号扩展到64位存放在LO寄存器,余数有符号扩展到64位后存放在HI寄存器。divu功能同div,区别是操作数a0和a1为无符号数。
ddiv实现两个有符号64位寄存器的除法运算,商存放在LO寄存器,余数存放在HI寄存器。ddivu功能同ddiv,区别是操作数a0和a1为无符号数。
上面介绍的和乘法/除法相关的运算时涉及到两个寄存器HI、LO。这两个寄存器不是通用寄存器。所以需要把里面的结果复制到通用寄存器才可以使用。下面介绍和HI、LO寄存器相关的指令:
mfhi t0 #t0 ← HI
mflo t0 #t0 ← LO
mthi t0 #HI ← t0
mtlo t0 #LO ← t0
上面4条指令完成了HI、LO和通用寄存器的互拷贝操作。指令中“f”意为from,“t”意为to 。mfhi实现把HI寄存器值拷贝到t0;mflo 实现把LO寄存器值拷贝到t0。mthi实现把t0拷贝到HI寄存器,mtlo实现把t0拷贝到LO寄存器。
1.3 逻辑运算
MIPS中逻辑运算指令包括与、或、非、条件设置等,具体如下表所示:
指令 |
格式 |
功能简述 |
SLT |
SLT rd, rs, rt |
小于设置 if GPR[rs] < GPR[rt] then GPR[rd]=1 else GPR[rd]=0 |
SLTI |
SLTI rt, rs, immediate |
小于设置 if GPR[rs] < immed then GPR[rd]=1 else GPR[rd]=0 |
SLTU |
SLTU rd, rs, rt |
无符号小于设置 |
SLTIU |
SLTIU rt, rs, immediate |
无符号小于立即数设置 |
AND |
AND rd, rs, rt |
与运算 rd = rs and rt |
ANDI |
ANDI rt, rs, immediate |
与立即数 rt = rs and immediate |
OR |
OR rd, rs, rt |
或运算 rd = rs or rt |
ORI |
ORI rt, rs, immediate |
或立即数 rt = rs or immediate |
XOR |
XOR rd, rs, rt |
异或 rd = rs ^ rt |
XORI |
XORI rt, rs, immediate |
异或立即数 rt = rs ^ immediate |
NOR |
NOR rd, rs, rt |
或非 rd = rs nor rt |
LI |
LI rt, immediate |
取立即数到寄存器 。 |
LUI |
LUI rt, immediate |
取立即数到高位GPR[rt] ← sign_extend(immediate || 0 16 ) |
CLO |
CLO rd, rs |
字前导1个数 |
DCLO |
DCLO rd, rs |
双字前导1个数 |
CLZ |
CLZ rd, rs |
字前导0个数 |
DCLZ |
DCLZ rd, rs |
双字前导0个数 |
WSBH |
WSBH rd, rt |
半字中字节交换 |
DSHD |
DSHD rd, rt |
字中半字交换 |
DSBH |
DSBH rd, rt |
双字中的半字中字节交换 |
SEB |
SEB rd, rt |
字节符号扩展 |
SEH |
SEH rd, rt |
半字符号扩展 |
INS |
INS rt, rs, pos, size |
位插入 , pos取值0-31 size取值1-32 。pos+size取值(1-32) |
DINS |
DINS rt, rs, pos, size |
双字位插入 pos取值0-31 size取值1-32 pos+size取值(1-32) |
DINSM |
DINSM rt, rs, pos, size |
双字位插入 pos取值0-31 size取值2-64,pos+size取值(33-64) |
DINSU |
DINSU rt, rs, pos, size |
双字位插入 pos取值32-63, size取值1-32,pos+sizee取值(33-64) |
EXT |
EXT rt, rs, pos, size |
位提取 pos取值0-31, size取值1-32,pos+size取值(1-32) |
DEXT |
DEXT rt, rs, pos, size |
双字位提取 pos取值0-31, size取值1-32,pos+size取值(1-63) |
DEXTM |
DEXTM rt, rs, pos, size |
双字位提取 pos取值0-31, size取值33-64,pos+size取值(33-64) |
DEXTU |
DEXTU rt, rs, pos, size |
双字位提取 pos取值32-63, size取值1-32,pos+size取值(33-64) |
表4 逻辑运算
这张表中的U代表无符号数。具体举例如下:
slt a3,t0,t1 #if(t0<t1) a3=1 else a3=0
slti a3,t0,0x3 #if(t0<0x3) a3=1 else a3=0
sltu a3,t0,t1 #if(t0<t1) a3=1 else a3=0
sltiu a3,t0,0x3 #if(t0<0x3) a3=1 else a3=0
slt指令比较两个操作数寄存器t0和t1,结果为真则目标寄存器a3置1,结果为假则目标寄存器a3置0。slti功能同slt,就是操作数之一是个立即数。sltu功能同slt,就是要求两个操作数必须是无符号数。sltiu功能也同slt,就是要求操作数之一是立即数,且两个操作数都为无符号数。
与、或、非等相关的指令比较简单,这里不展开解释。
LI、LUI都是加载立即数到寄存器。立即数大小是16位。如果超过16位将会被编译拆分成多条指令。指令LUI(Load Upper Immediate),这里的U代表Upper。就是加载一个16位的立即数放到目标寄存器rt的高16位上,rt低16为为0。例如我在内嵌汇编里面写了下面语句:
li v1,0x3ffff
gcc编译后会把它转化成:
lui v1,0x3 # v1 = 0x30000
ori v1,v1,0xffff # v1 = 0x30000 | 0xffff
指令CLO(Count Leading Ones)对地址为rs的通用寄存器的值,从其最高位开始向最低位方向检查,直到遇到值为“0”的位,将该位之前“1”的个数保存到地址为rd的通用寄存器中,如果地址为rs的通用寄存器的所有位都为1(即OxFFFFFFFF),那么将32保存到地址为rd的通用寄存器中。而指令CLZ(Count Leading Zeros)和CLO相反,对地址为rs的通用寄存器的值,从其最高位开始同最低位方向检查,直到遇到值为“1”的位,将该位之前“0”的个数保存到地址为rd的通用寄存器中,如果地址为rs的通用寄存器的所有位都为0(即0x00000000),那么将32保存到地址为rd的通用寄存器中。DCLO和DCLZ分别是CLO和CLZ的64位运算形式。
指令INS(Insert Bit Field)实现的是32位寄存器的域值插入,就是将操作数rs的低size位,插入到目标操作数的(pos到pos+size)位置。此处rs,rt均为32位数。举例说明:
ins a3,t0,0x5,0x3
上面INS指令的意思是将寄存器t0的低3位,放到目标寄存器a3的第5位到第7位(bit5-bit7)。DINS指令功能同INS,都实现的是目标操作数rt的低32位的域值设置。区别在于rs,rt可以是64位数。而DINSM和DINSU中的M代表Middle,U代表Upper,实现的是对目标操作数rt的高32位的域值设置。通常情况我们只要使用DINS指令就可以,汇编器会根据需要选择是DINSU还是DINSM。
指令EXT实现32位寄存器的域值提取,就是将操作数rs的第pos位到第(pos+size-1)位提取出来,结果保存到目标寄存器rt中。例如:
ext a3,t0,0x5,0x3
上面的ext指令的意思是将寄存器t0的第5位到第7位提取出来存放到寄存器a3。相关指令还有DEXT,实现的是64位操作数的域值部分提取。而DEXTM和DEXTU分别完成64位操作数rs的高32位(bit32-bit63)域值的部分提取。通常情况我们只要使用DEXT指令就可以,汇编器会根据需要选择是DEXTU还是DEXTM。
指令SEB和SEH实现的是符号扩展,B代表Byte(8位)、H代表HalfWord(16位)。命令比较简单,这里不做举例。
指令WSBH实现一个32位操作数rt内的两个半字内部进行字节交换,结果存入rd。形式类似于:
rd = rt( 23..16) || rt( 31..24 ) || rt( 7..0) || rt( 15..8 );
DSBH功能类似于:
rd = rt( 55.48) || rt( 63..56) || rt( 39..32) || rt( 47..40) || rt( 23..16) || rt( 31..24) || rt( 7..0 )|| rt( 15..8);
DSHD功能类似于:
rd = rt(15..0) || rt(31..16) || rt(47..32) || rt(63..48);
1.4 移位运算
移位操作指令是一组经常使用的指令,属于汇编语言逻辑指令中的一部分,它包括逻辑左移、逻辑右移动、算数右移、循环移位。其功能为将目的操作数的所有位按操作符规定的方式移动n位结果送入目的地址。mips中移位相关指令如下表:
指令 |
格式 |
功能简述 |
SLL |
SLL rd, rt, sa |
逻辑左移 GPR[rd] ← GPR[rt] << sa, sa为常量 取值0-31 |
SLLV |
SLLV rd, rt, rs |
可变的逻辑左移GPR[rd] ← GPR[rt] << GPR[rs] , rs取值0-31 |
DSLL |
DSLL rd, rt, sa |
双字逻辑左移,0<=sa<32 |
DSLLV |
DSLLV rd, rt, rs |
可变的双字逻辑左移 |
DSLL32 |
DSLL32 rd, rt, sa |
移位量加 32 的双字逻辑左移GPR[rd] ← GPR[rt] << (sa+32) |
SRL |
SRL rd, rt, sa |
逻辑右移 |
SRLV |
SRLV rd, rt, rs |
可变的逻辑右移 |
DSRL |
DSRL rd, rt, sa |
双字逻辑右移 |
DSRLV |
DSRLV rd, rt, rs |
可变的双字逻辑右移 |
DSRL32 |
DSRL32 rd, rt, sa |
移位量加 32 的双字逻辑右移GPR[rd] ← GPR[rt] << (sa+32) |
SRA |
SRA rd, rt, sa |
算术右移 |
SRAV |
SRAV rd, rt, rs |
可变的算数右移 |
DSRA |
DSRA rd, rt, sa |
双字算术右移 |
DSRAV |
DSRAV rd, rt, rs |
可变的双字算术右移 |
DSRA32 |
DSRA32 rd, rt, sa |
移位量加 32 的双字算术右移 GPR[rd] ← GPR[rt] >> (sa+32) |
ROTR |
ROTR rd, rt, sa |
循环右移 temp ← GPR[rt] sa-1..0 || GPR[rt] 31..sa rd ← sign_extend(temp) |
ROTRV |
ROTRV rd, rt, rs |
可变的循环右移 |
DROTR |
DROTR rd, rt, sa |
双字循环右移 |
DROTR32 |
DROTR32 rd, rt, sa |
移位量加 32 的双字循环右移 |
DROTRV |
DROTRV rd, rt, rs |
双字可变的循环右移 |
表6 移位运算
逻辑左移SLL(Shift Logical Left),就是rt向左(高位)移动sa位,sa是常量,取值范围在0-31之间(SLL指令是R型,sa占用5bit),空出的右边(低位)补0。和SLL相关的指令运算举例如下:
sll a3,t1,0x3 #a3 = t1 << 0x3
sllv a3,t1,t0 #a3 = t1 << t0 ,0<=t0<31
dsll a3,t1,0x3 #a3 = t1 << 0x3 t1为64位
dsll32 a3,t1,0x3 #a3 = t1 << (0x3 +32)
dsllv a3,t1,t0 #a3 = t1 << t0 ,0<=t0<31 t1为64位
这个例子中sll就是把一个32位操作数t1左移3位,低3位添0,结果符号扩展到64位后存入a3寄存器。sllv功能同sll,区别在于sllv中的v意为variable,就是要移位的位数不是常量,而是寄存器。dsll实现的是一个64位操作数t1左移3位,低3位添0,高位溢出后丢弃,结果存入a3寄存器。dsll32功能同dsll,区别在于sa需要加32。dsllv功能同sllv,区别在于t1表示64位数。
逻辑右移SRL(Shft Right Logical),就是rt向右(低位)移动sa位,sa取值范围在0-31,空出的左边(高位)补0。和SRL相关的指令运算举例如下:
srl a3,t1,0x3 #a3 = t1 >> 0x3
这个例子中srl就是把一个32位操作数t1右移3位,空出的高3位添0,结果符号扩展到64位存入寄存器a3。其他几组srlv、dsrl、dsrlv、dsrl32功能相信读者可以自行推倒出来。
算数右移SRA(Shift Right Arithmetic),就是实现rt向右移位,空出来的左边(高位)是补0还是1取决于rt的最高位(32位rt就取bit31,64位rt就取bit63),可移动的范围为0-31。
sra a3,t1,0x3 #a3 = t1 << 0x3
sra实现一个32位操作数t1的右移3位,空出来的高3位填充t1的bit31值,结果符号扩展后存入寄存器a3。和算数右移相关的还有指令srav、dsra、dsrav、dsra32。
循环右移就是操作数向右移动n位,然后把移出的n位顺序填充左边n位。比如:
rota a3,t1,0x3
rota实现一个32位操作数向右循环移位,循环量是3。就是把t1低3位放入t1的高3位,t1的高29位向右移动3位。ROTRV功能同ROTA,只是移位量不是常数而是寄存器,不过寄存器取值范围同SA(0-31)。
2 访存指令
访存指令就是实现对主存中的数据进行访问或存储。MIPS 体系结构采用 load/store 架构。所有运算都在寄存器上进行,只有访存指令可对主存中的数据进行访问。访存指令包含各种宽度数据的读写、无符号读、非对齐访存和原子访存等。MIPS中访存指令如下:
指令 |
格式 |
功能简述 |
LB |
LB rt, offset(base) |
8位加载, |
LBU |
LBU rt, offset(base) |
8位加载,结果0扩展 |
LH |
LH rt, offset(base) |
加载半字 |
LHU |
LHU rt, offset(base) |
加载半字,结果0扩展 |
LW |
LW rt, offset(base) |
加载字 |
LWU |
LWU rt, offset(base) |
加载字,结果0扩展 |
LWL |
LWL rt, offset(base) |
加载字的左部 |
LWR |
LWR rt, offset(base) |
加载字的右部 |
LD |
LD rt, offset(base) |
加载双字 |
LDL |
LDL rt, offset(base) |
加载双字左部 |
LDR |
LDR rt, offset(base) |
加载双字右部 |
LL |
LL rt, offset(base) |
加载标志处地址 |
LLD |
LLD rt, offset(base) |
加载标志处双字地址 |
SB |
SB rt, offset(base) |
存字节 |
SH |
SH rt, offset(base) |
存半字 |
SW |
SW rt, offset(base) |
存字 |
SWL |
SWL rt, offset(base) |
存字的左部 |
SWR |
SWR rt, offset(base) |
存字的右部 |
SD |
SD rt, offset(base) |
存双字 |
SDL |
SDL rt, offset(base) |
存双字左部 |
SDR |
SDR rt, offset(base) |
存双字右部 |
SC |
SC rt, offset(base) |
带条件的存储 |
SCD |
SCD rt, offset(base) |
带条件的存双字 |
表7 访存指令
上表中L开头的都是从主存加载数据操作,S开头的都是存储数据操作。操作数据的大小有B(Byte 8bit)、H(Halfword 16bit)、W(Word 32bit)、D(Double 64bit)。加载地址都是base+offset方式,offset取值范围在在-32767至32768。这一点可以从任意加载指令格式可以看出。
图4 LW指令格式
从图4 可以看出offset域占用16bit的大小。所以load相关指令的可寻址范围在base-32767到base+32768之间。下面举个例子说明加载指令的使用:
ld v0,-32688(gp) # v0 ← memory[gp-32688]
lwu v1,32(gp) #v1 ← memory[gp + 32]
这里ld指令意思是从地址值为gp-32688的内存位置,加载一个64位的数据赋给寄存器v0。如果地址无效,程序会收到异常信号。lwu指令意思是从地址为gp+32的内存位置加载一个32位数据,结果无符号扩展到64位后存入v1寄存器。
存储指令和加载指令功能上恰好相反,负责把寄存器数据存储到主存。举例说明:
sd ra,-352(ra) # memory[ra -352] ← ra
sw ra,0(s8) # memory[s8 +0] ← v0
这里sd指令意思是把寄存器ra里的64位数写出到内存地址为(ra-352)的位置。如果地址无效,程序会收到异常信号。sw指令意思是把寄存器ra里的32位数写出到内存地址为s8+0的位置。
上表中有两个指令ll和sc比较特殊,在本章最后一节会单独介绍。
3 跳转和分支指令
跳转和分支指令可改变程序的控制流。mips中指令如下表:
指令 |
格式 |
功能简述 |
J |
J target |
绝对跳转 |
JR |
JR rs |
寄存器绝对跳转,寻址空间大于256M |
JAL |
JAL target |
子程序调用 |
JALR |
JALR rs |
寄存器子程序调用,寻址空间大于256M |
B |
B lable |
相对PC的跳转,寻址范围小于256K |
BAL |
BAL lable |
相对PC的子程序调用,寻址空间小于256K |
BEQ |
BEQ rs, rt, lable |
相等则跳转 if (rs == rt) goto lable |
BNE |
BNE rs, rt, lable |
不等则跳转 if (rs = rt) goto lable |
BLEZ |
BLEZ rs, lable |
小于等于0跳转 if (rs <= 0) goto lable |
BGTZ |
BGTZ rs, lable |
大于0跳转 if (rs > 0) goto lable |
BLTZ |
BLTZ rs, lable |
小于0跳转 if (rs < 0) goto lable |
BGEZ |
BGEZ rs, lable |
大于等于0跳转 if (rs >= 0) goto lable |
BLTZAL |
BLTZAL rs, lable |
带条件的相对PC子程序调用 if(rs<0) lable() |
BGEZAL |
BGEZAL rs, lable |
带条件的相对PC子程序调用 if(rs >=0) lable() |
表8 跳转和分支指令
MIPS体系架构中,跳转、分支和子程序调用的命名规则如下:
- 和PC相关的相对寻址指令(地址计算依赖于PC值)称为“分支”(branch),助记词以b开头。绝对地址指令(地址计算不依赖PC值)称为“跳转”(Jump),助记词以j开头。
- 子程序调用为“跳转并链接”或“分支并链接”,助记词以al结尾。
上表中的第一个指令J 表示无条件跳转到一个绝对目标地址,地址可寻址范围在256MB(原因可以参阅第5章)。使用上可以是如下形式:
j test #跳转到test标识
如果要跳跃到大于256MB的地址,可以使用JR指令(寄存器指令跳转)。JR的操作数是寄存器,可以表达更大的地址空间。比如:
jr t9 #跳转到寄存器t9所存的地址
JAL、JALR实现了直接和间接的子程序调用。这两个指令和上面的J、JR指令的区别在于,JAL、JALR在跳转到指定地址的同时,还要保存返回地址(PC+8)到寄存器ra($31)。这样在函数返回可以通过JR ra完成。常见使用如下:
main:
jal test #跳转到test标识或者子程序
nop
ld v0 ,32696(gp)
...
test:
...
jr ra
nop
这里jal test指令执行时,首先会把pc+8的地址赋值给ra。那么ra就指向了“ld v0 ,32696(gp)”的位置。然后程序跳转到test程序运行。test子程序最后会通过“jr ra”指令返回到“ld v0 ,32696(gp)”的位置。
如果要跳转的子程序超过了258M,可以使用JALR指令完成。这里不做介绍。
相对寻址(PC-relotive)的子程序调用可以用BAL。BAL指令使用16位存储offset,所以可寻址范围特别小,仅为256K。所以平时很少使用到。
BEQ、BNE、BLEZ、BGTZ、BLTZ、BGEZ的功能都是带条件的分支跳转,即当操作数寄存器rs和寄存器满足一定条件时,才会跳转到label。以BEQ为例:
beq $8,$4,again #if($8==$4) goto again
nop
当寄存器$8和$4值相等时,跳转到标识符again位置。这里again的地址计算是依赖于程序计数器PC,计算过程由汇编器帮助我们完成。我们所要注意的就是again的范围必须在(PC-127K)至(PC+128K)之间 ,否则分支跳转就会失败。跳转失败不会有异常。
BLTZAL、BGEZAL指令 实现的是带条件的子程序调用。以BLTZAL指令为例:
bltzal a0,test #if(a0<0) test()
nop
bltzal指令意思是如果寄存器a0的值小于0,那么PC跳转到子程序test。这里要注意的是无论调用是否发生,返回地址一律保存到寄存器ra中。
4 协处理器0指令:CPU控制指令
MIPS的协处理器0指令就是CPU控制指令,包括数据在通用寄存器和CP0寄存器间的读写、管理内存、处理异常等。具体指令如下表所示:
指令 |
格式 |
功能简述 |
MFC0 |
MFC0 rt,cs |
从 CP0 寄存器取字 |
MTC0 |
MTC0 rt, cs |
往 CP0 寄存器写字 |
DMFC0 |
DMFC0 rt,cs |
从 CP0 寄存器取双字 |
DMTC0 |
DMTC0 rt, cs |
往 CP0 寄存器写双字 |
ERET |
ERET |
异常返回,返回EPC中保存的地址 |
CACHE |
CACHE k,addr |
Cache 操作 |
TLBR |
|
读索引的 TLB 项 |
TLBWI |
|
写索引的 TLB 项 |
TLBWR |
|
写随机的 TLB 项 |
TLBP |
|
在 TLB 中搜索匹配项 |
表9 cpu控制指令
MFC0、MTC0、DMFC0、DMTC0可以完成通用寄存器和CP0寄存器之间的数据传送。比如我们可以通过下面指令来获取epc寄存器的值。
mfc0 t1,$14 # 从CP0 取epc寄存器($14)数据,存到通用寄存器t0
mfc0完成从cp0寄存器获取一个32位数据(如果cp0寄存器是64位数据则只取其低32位。)到通用寄存器,如果要获取的是64位数据则需要使用dmfc0指令。
注意:和CP0相关的指令执行都需要特权模式,比如上面的mfc0指令的运行需要root权限或者sudo才可以运行。所以在一般用户程序中很少见到这些CP0相关的控制指令。
5 其他指令
mips64中,除了本章列举的运算指令、控制指令、跳转指令等,还有一些无法归类的指令,这里统称为其他指令,具体如下表:
指令 |
格式 |
功能简述 |
SYSCALL |
SYSCALL |
系统调用 |
BREAK |
BREAK |
断点 |
SYNC |
SYNC |
同步 |
RDHWR |
RDHWR rt,rd |
用户态读取硬件寄存器 |
WAIT |
WAIT |
等待指令 |
TEQ |
TEQ rs, rt |
条件自陷 if(rs == rt) exception(trap) |
TNE |
TNE rs, rt |
条件自陷 if(rs != rt) exception(trap) |
MOVE |
MOVE rd, rs |
移动 rd = rs |
MOVZ |
MOVZ rd, rs, rt |
条件移动 if (rt == 0) rd = rs |
MOVN |
MOVN rd, rs, rt |
条件移动 if (rt != 0) rd = rs |
表9 其他指令
SYSCALL指令可以产生一个引发系统调用的异常,相当于x86的 “int 0x80”指令。它的使用方式在本书后面第6章会有专门的讲解。
BREAK指令也称断点指令,它可以产生一个“断点”类型的异常。在我们调试代码时,break指令是很有用的调试手段了。比如我们在c语言中插入一个break指令:
asm(“break”);
那么程序执行到这条指令时,就会收到一个SIGTRAP信号,提示信息为“Trace/breakpoint trap”。
TEQ、TNE都是条件自陷指令。如果条件成立就会引发一个自陷(Trap)异常。自陷指令又叫做访管指令,出现在计算机操作系统中,用于实现在用户态下运行的进程调用操作系统内核程序,即当运行的用户进程或系统实用进程欲请求操作系统内核为其服务时,可以安排执行一条陷入指令引起一次特殊异常。通常自陷指令是给编译器和解释器用的,可以实现运行时数组边界检查之类的操作。比如我用c语言写了一个除法语句:
int c = a/b;
gcc编译完成后,对应的汇编指令就是下面的样子:
div zero,v1,v0 # lo = v1/v0 hi = v1%v0
teq v0,zero,0x7 # if(v0 == zer0) trap
mfhi v1 # v1 = hi
mflo v0 # v0 = lo
上面的div实现两个有符号数的除法运算,商存入lo,余数存入hi。下面第二句teq就是判断v0(代表c语言里的变量b)是否为0。如果为0,那么程序自陷,0x7会被保存到Cause寄存器。程序会收到信息“浮点数例外”,后面的mfhi、mflo将不再执行。
SYNC是用在多核处理器的内存操作(load/store寄存器)同步指令,用来保证 sync 之前对于内存的操作能够在 sync 之后的指令开始之前完成。比如下面指令:
sw v1,8208(v0) #写v1寄存器数据到内存地址v0+8208
sync # 同步
cache 21,8208(v0) #刷新缓存
RDHWR指令允许用户态读取硬件寄存器(CP0寄存器)。当然并不是所有硬件寄存器都可以读取的,而是受到CP0寄存器HWREna限制的。HWREna决定了哪些硬件寄存器可以被用户态程序访问。当前支持可读的寄存器信息如下表:
寄存器编号 (rd值) |
助记符 |
功能简介 |
0 |
CPUNum |
用于获取当前进程正在运行在哪个CPU上,实际是访问CP0寄存器Ebase的CPUNUM域 |
1 |
SYNCI_Step |
用于获取一级高速缓存行的有效宽度 |
2 |
CC |
读取CP0的Count寄存器 |
3 |
CCRes |
略 |
29 |
ULR |
用于获取线程本地存储(TLS)的位置 |
表10 RDHWR寄存器说明
在第一章我们已经举例了用RDHWR指令获取TLS地址方法。这里再举例说明如何获取当前进程正在运行在那个CPU核上,指令如下:
rdhwr $2,$0
这里就是从RDHWR的0号寄存器获取CPUNum信息到通用寄存器v0($2)。
MOVE、MOVZ、MOVN都实现了寄存器到寄存器的复制。MOVE是无条件复制,例如:
move a1,a2 # a1=a2
move就是把寄存器a2的值复制到寄存器a1。这条指令和下面的指令相等:
movz a1,a2,zero
6 原子指令LL/SC
mips用ll(load-linked)和 sc(store-conditional)两条指令实现多进程的互斥锁。也就是说用这两条指令可以实现原子操作。比如内核代码里的原子自加操作atomic_inc(&count) ,每次调用此函数,变量count 加1,汇编指令如下:
atomic_inc:
ll v0,0(a0) #v0指向了变量count位置(0(a0))
addu v0,1
sc v0,0(a0)
beq v0,zero,atomic_inc #如果sc指令失败(v0==0),跳到atomic_inc重试
nop
jr ra
nop
ll和sc指令具体介绍如下图:
图3 LL指令格式
图4 sc指令格式
从上图对ll和sc描述(Description)就可以看出ll/sc实现原子操作的原理:指令LL rt offset(base)从地址(base+offset)执行32位的加载,同时把加载的地址保存到LLAddr寄存器。当SC rt,offset(base)执行时,首先会检查是否可以保证从上次执行的LL开始以来的读写操作能否原子性的完成,如果能,rt值被成功存储同时rt被置1。否则rt被置0。所以上面实例中sc指令完成后,会有个对v0值的判断“beq v0,zero,atomic_inc”,如果v0值为0说明原子操作失败,需要返回atomic_inc函数入口重试。