根据 Programmed Introduction to MIPS Assembly Language 和《计算机组成与设计——软件/硬件接口》(原书第 5 版)整理。

寄存器

通用寄存器

MIPS 提供 32 个 32 位的通用寄存器。

寄存器助记符用途
$0zero常数零
$1$at汇编器保留
$2$3$v0$v1子程序返回值
$4-$7$a0-$a3子程序参数
$8-$15$t0-$t7临时寄存器(调用者保存)
$16-$23$s0-$s7保存寄存器(被调用者保存)
$24$25$t8$t9临时寄存器(调用者保存)
$26$27$k0$k1操作系统保留
$28$gp全局指针
$29$sp栈指针
$30$fp帧指针
$31$ra返回地址

特殊寄存器

MIPS 提供一对 32 位寄存器 hilo 来帮助完成乘法、除法运算。

浮点寄存器

MIPS 有 32 个 32 位的浮点寄存器 $f0$f31

指令格式

R 型(用于寄存器)

字段名oprsrtrdshamtfunct
含义操作码第一个源操作数寄存器第二个源操作数寄存器目的寄存器位移量功能码
长度655556

由于 shamt 用 5 位表示,所以 0 <= shamt < 2^5。

I 型(用于立即数)

字段名oprsrtimmediate
含义操作码源操作数寄存器目的寄存器立即数
长度65516

由于 immediate 用 16 位表示,所以 -2^15 <= immediate < 2^15 。

注意,此处 rt 的含义与上面不同,且后面的指令如不注明,immediate 默认被符号扩展为 32 位!

J 型(用于分支和跳转)

字段名opaddress
含义操作码地址
长度626

特别的,对于条件分支指令,地址字段可能又被划分为了多个部分。

基本汇编指令

算术运算指令

加法指令

指令格式含义
addu rd,rs,rtrd = rs + rt(溢出时不产生异常)
add rd,rs,rtrd = rs + rt(溢出时产生异常)
addiu rt,rs,immediatert = rs + immediate(溢出时不产生异常)
addi rt,rs,immediatert = rs + immediate(溢出时产生异常)

注意,MIPS 只有 32 位算术运算,在高级语言中需要进行 16 位或 8 位算术运算时,编译器可能产生更多的指令来模拟这些运算,这意味这 32 位算术运算的速度往往会快于其他情况,而不是数据长度越短运算越快。

减法指令

指令格式含义
subu rd,rs,rtd = rs-rt(溢出时不产生异常)
sub rd,rs,td = rs-rt(溢出时产生异常)
subiu rd,rs,immediated = rs-immediate (溢出时不产生异常)
subi rd,rs,immediated = rs-immediate(溢出时产生异常)

乘法指令

指令格式含义
mult rd,rthilo = rd * rt(有符号数)
multu rd,rthilo = rd * rt(无符号数)
  • hi:结果的高 32 位
  • lo:结果的低 32 位

注意,此处指令中带的 u 和加减法指令含义不同。对于加减法,有符号和无符号数的运算相同,区别仅在u 的版本溢出时不产生异常;而对于乘除法,有符号和无符号数的运算不同,且无论是否溢出,都不产生异常,区别仅在u 的版本特指无符号数运算

除法指令

指令格式含义
div rd,rtlo = rd / rt; hi = rd % rt(有符号数)
divu rd,rtlo = rd / rt; hi = rd % rt(无符号数)
  • lo:商
  • hi:余数

逻辑运算指令

位运算指令

指令格式含义
ori rt,rs,immediate`rt = rs
andi rt,rs,immediatert = rs & immediate
xori d,s,immediatert = rs ^ immediate
or rd,rs,rt`rd = rs
and rd,rs,rtrd = rs & rt
xor rd,rs,rtrd = rs ^ rt
nor rd,rs,rt`rd = ~(rs

移位指令

指令格式含义
sll rd,rt,shamtrd = rt << shamt
srl rd,rt,shamtrd = rt >> shamt(逻辑右移)
sra rd,rt,shamtrd = rt >> shamt(算术右移)

特殊用法

指令格式含义
nor rd,rs,$0rd = ~rs
or rd,rs,$0rd = rs
sll $0,$0,0空操作

由于对 $0 的改写无实际意义,所以可以借助 $0 实现空操作。

数据传输指令

指令格式含义
lw rt,immediate(rs)rt = memory[rs + immediate]
sw rt,immediate(rs)memory[rs + immediate] = rt
lh rt,immediate(rs)rt = memory[rs + immediate]
lhu rt,immediate(rs)rt = memory[rs + immediate](零扩展)
sh rt,immediate(rs)memory[rs + immediate] = rt
lb rt,immediate(rs)rt = memory[rs + immediate]
lbu rt,immediate(rs)rt = memory[rs + immediate](零扩展)
sb rt,immediate(rs)memory[rs + immediate] = rt
lui rt,immediatert = immediate * 2^16
  • w:字,32 位
  • h:半字,16位
  • b:字节,8 位

MIPS 可以使用大端序和小端序,具体由系统设计人员决定。

若要生成 32 位立即数用于寄存器基址寻址,可以先用 lui 指令加载高位,再用 ori 或者 lw 指令填充低位,以完成 $12 = memory[$13 + 0xC] 为例,有以下两种方式:

# 方式一
lui $13,0x0060
ori $13,0x5000 # 简化版 ori 指令,同 ori $13,$13,0x5000
lw  $12,0xC($13)

# 方式二
lui $13,0x0060
lw  $12,0x500C($13)

无条件跳转指令

指令格式含义
j addressgoto address * 4
jal address$ra = PC + 4; goto address * 4
jr registergoto register

MIPS 的指令总是 32 位的,因此指令是 4 字节对齐的,32 位地址的低 2 位永远为 0。将 32 位目标地址右移 2 位,然后存储低 26 位,这 26 位的地址实际可以替代 PC 的低 28 位。

注意,使用 j 指令时,PC 的高 4 位是不变的,这意味着寻址界限为 256 MB,超过该界限时,应该使用寄存器跳转指令 (通常编译器会完成这些操作)。

可以使用符号地址来简化编程:

main: 
  ...
  j main

条件分支指令

条件跳转指令

指令格式含义
beq register1,register2,addressif (register1 == register2) goto PC + 4 + address * 4
bne register1,register2,addressif (register1 != register2) goto PC + 4 + address * 4
bltz register,labelif (register < 0) goto PC + 4 + address * 4
bgez register,labelif (register >= 0) goto PC + 4 + address * 4

条件跳转指令使用 PC 相对寻址。

条件置位指令

指令格式含义
slt rd,rs,rtif (rs < rt) rd = 1; else rd = 0(有符号数)
sltu rd,rs,rtif (rs < rt) rd = 1; else rd = 0(无符号数)
slti rt,rs,immediateif (rs < immediate) rt = 1; else rt = 0(有符号数)
sltiu rt,rs,immediateif (rs < immediate) rt = 1; else rt = 0(无符号数)

再次强调,虽然 sltiu 代表无符号版本,但立即数会先被符号展开,然后再作为无符号数比较。

其他指令

指令格式含义
mfhi rdrd = hi
mflo rdrd = lo
syscall系统调用

注意,在早期的 MIPS 处理器上,mfhimflo 指令后的两条指令,不得出现乘法和除法指令,其原因涉及 MIPS 流水线的工作方式。

伪指令

算术运算伪指令

指令格式含义
addu d,s,xd = s + x
subu d,s,xd = s-x
negu d,sd = -s
mul d,s,td = s * t(结果不超过 32 位)
div d,s,td = s / t(有符号数)
divu d,s,td = s / t(无符号数)
remu d,s,td = s % t(无符号数)
abs d,s`d =

其中,x 可以是寄存器、 16 位立即数或 32 位立即数,后面的 x 含义相同,不再重复。

逻辑运算伪指令

指令格式含义
not d,sd = ~s
or d,s,x`d = s
and d,s,xd = s & x
rol d,s,td = s << t(循环左移)
ror d,s,td = s >> t(循环右移)

数据传输伪指令

指令格式含义
move d,sd = s
li d,valued = valuevalue 可以是 16 位或 32 位)
la d,expd = address(exp)exp 通常是一个符号地址)
lw d,expd = memory[exp]
sw d,expmemory[exp] = d

伪指令还允许借助符号地址来寻址,示例如下:

li $t1,2                 # index 2
lb $v0,data($t1)         # $v0 = data[$t1]
...
  
data: .byte  6,34,12,-32, 90

条件分支伪指令

条件跳转伪指令

指令格式含义
b labelgoto label
beq s,x,labelif (s == x) goto label
beqz s,labelif (s == 0) goto label
bge s,x,labelif (s >= x) goto label (有符号数)
bgeu s,x,labelif (s >= x) goto label (无符号数)
bgez s,labelif (s >= 0) goto label (有符号数)
bgt s,x,labelif (s > x) goto label (有符号数)
bgtu s,x,labelif (s > x) goto label (无符号数)
bgtz s,labelif (s > 0) goto label (有符号数)
ble s,x,labelif (s <= x) goto label (有符号数)
bleu s,x,labelif (s <= x) goto label (无符号数)
blez s,labelif (s <= 0) goto label (有符号数)
blt s,x,labelif (s < x) goto label (有符号数)
bltu s,x,labelif (s < x) goto label (无符号数)
bltz s,labelif (s < 0) goto label (有符号数)
bnez s,labelif (s != 0) goto label
bne s,x,labelif (s != x) goto label

注意,所有的分支指令都只能跳转到分支附近的位置,它只使用 16 位来表示地址。

条件置位伪指令

指令格式含义
slt d,s,xif (s < x) d = 1; else d = 0
seq d,s,xif (s == x) d = 1; else d = 0
sge d,s,xif (s >= x) d = 1; else d = 0 (有符号数)
sgeu d,s,xif (s >= x) d = 1; else d = 0 (无符号数)
sgt d,s,xif (s > x) d = 1; else d = 0 (有符号数)
sgtu d,s,xif (s > x) d = 1; else d = 0 (无符号数)
sle d,s,xif (s <= x) d = 1; else d = 0 (有符号数)
sleu d,s,xif (s <= x) d = 1; else d = 0 (无符号数)
slt d,s,xif (s < x) d = 1; else d = 0 (有符号数)
slti d,s,immediateif (s < immediate) d = 1; else d = 0 (有符号数)
sltu d,s,xif (s < x) d = 1; else d = 0 (无符号数)
sltiu d,s,immediateif (s < immediate) d = 1; else d = 0 (无符号数)
sne d,s,xif (s != x)

浮点伪指令

指令格式含义
l.s fd,addressfd = memory[address]
s.s fd,addressmemory[address] = fd
li.s fd,valuefd = value
abs.s fd,fs`$d =
add.s fd,fs,ftfd = fs + ft
sub.s fd,fs,ftfd = fs-ft
mul.s fd,fs,ftfd = fs * ft
div.s fd,fs,ftfd = fs / ft
neg.s fd,fsfd = -fs
mov.s fd, fsfd = fs
mtc1 rs, fdfd = rs(直接赋值,不进行转换,注意此处目标寄存器和源寄存器顺序)
mfc1 rd, fsrd = fs(直接赋值,不进行转换)
c.eq.s fs, ftif (fs == ft) condition bit = 1; else condition bit = 0
c.lt.s fs, ftif (fs < ft) condition bit = 1; else condition bit = 0
c.le.s fs, ftif (fs <= ft) condition bit = 1; else condition bit = 0
bc1t labelif (condition bit = 1) goto label
bc1f labelif (condition bit = 0) goto label

部分 MIPS 处理器只允许在单精度浮点指令中使用 $f0$f2 、 … 、 $f30 寄存器。以上 .s 结尾的都是单精度浮点指令,对于双精度,将 s 替换为 d 即可,这样 MIPS 将使用寄存器对来进行运算,寄存器对的名称是 $f0$f2 、 … 、 $f30

其他伪指令

指令格式含义
nop空操作

汇编器指令

语法用途
.text表明代码段的开始
.globl表明标识符是一个全局符号
.data表明数据段的开始
.byte放置一个 8 位的整数,多个整数用逗号隔开
.word放置一个 32 位的整数,多个整数用逗号隔开
.space在内存中保留的字节数
.ascii放置一个 ASCII 字符串
.asciiz放置一个 C 风格的字符串 (以 \0 结尾的 ASCII 字符串)
.float放置一个 32 位的单精度浮点数,多个浮点数用逗号隔开

在 MIPS 中,栈是向下增长的。

通过以下代码实现 push 操作:

# PUSH the item in $t0:
subu $sp,$sp,4      #   point to the place for the new item,
sw   $t0,($sp)      #   store the contents of $t0 as the new top.

通过以下代码实现 pop 操作:

# POP the item into $t0:
lw   $t0,($sp)      #   Copy top item to $t0.
addu $sp,$sp,4      #   Point to the item beneath the old top.

子程序连接

基于堆栈的连接约定

  1. 调用子程序(由调用者完成): 1.1. 将需要保存的 $t0-$t9 寄存器入栈,子程序可能会修改它们。 1.2. 将参数放入 $a0-$a3。 1.3. 使用 jal 指令调用子程序。
  2. 子程序 prolog(由子程序在开头完成): 2.1. 如果该子程序需要调用其他子程序,将 $ra 入栈。 2.2. 将需要修改的 $s0-$s7 寄存器入栈。
  3. 子程序主体: 3.1. 子程序可以修改 $t0-$t9$a0-$a3 和 2.2 中保存的寄存器。 3.2. 如果子程序需要调用其他子程序,也必须遵守这些约定。
  4. 子程序 epilog(由子程序在返回前完成): 4.1. 将返回值放入 $v0-$v1。 4.2. 将 2.2 中保存的寄存器按与入栈时相反的顺序出栈。 4.3. 如果 $ra 在 2.1 中被保存,将它出栈。 4.4. 使用 ja $ra 指令返回。
  5. 从子程序重新获得控制权(由调用者完成): 5.1. 将 1.1 中保存的寄存器按与入栈时相反的顺序出栈。

基于栈帧的连接约定

以下约定假设栈只存储 32 位的数据。

  1. 调用子程序(由调用者完成): 1.1. 按数字顺序将需要保存的 $t0-$t9 寄存器入栈。 1.2. 将参数放入 $a0-$a3。 1.3. 使用 jal 指令调用子程序。
  2. 子程序 prolog(由子程序在开头完成): 2.1. 将 $ra 入栈。 2.2. 将调用者的帧指针 $fp 入栈。 2.3. 将需要修改的 $s0-$s7 寄存器入栈。 2.4. 初始化帧指针: $fp = $sp-space,其中 space 指变量占用空间,此处是变量个数的 4 倍。 2.5. 初始化栈指针: $sp = $fp
  3. 子程序主体: 3.1. 子程序可以修改 $t0-$t9$a0-$a3 和 2.3 中保存的寄存器。 3.2. 子程序根据 disp($fp) 的形式寻址来使用局部变量。 3.3. 子程序可以通过修改 $sp 在栈上存储数据。 3.4. 如果子程序需要调用其他子程序,也必须遵守这些约定。
  4. 子程序 epilog(由子程序在返回前完成): 4.1. 将返回值放入 $v0-$v1。 4.2. $sp = $fp + space。 4.3. 将 2.3 中保存的寄存器按与入栈时相反的顺序出栈。 4.4. 将调用者的帧指针 $fp 出栈。 4.5. 将 $ra 出栈。 4.6. 使用 ja $ra 指令返回。
  5. 从子程序重新获得控制权(由调用者完成): 5.1. 将 1.1 中保存的寄存器按与入栈时相反的顺序出栈。

进入子程序时栈结构如下图所示:

进入子程序时栈的结构