(四)、MIPS指令系统及汇编语言
(1)指令系统的基本知识(指令格式、寻址方式)
(2)MIPS汇编语言
4.1 指令系统的基本知识
4.1.1 指令系统概述

4.1.2 指令格式
机器指令是计算机硬件可以执行的、表示一种基本操作的二进制代码。
-
指令格式:操作码 + 操作数(操作数地址)
- 操作码:指明指令的操作性质
- 操作数:指明操作数的位置(或操作数本身)
-
指令表示:
- 机器表示:二级制代码
- 符号化表示:助记符,如
MOV AX, BX
-
操作码结构:
-
固定长度操作码:操作码长度固定不变
①硬件设计简单;②指令译码时间开销较小;③指令空间效率较低
-
可变长度操作码:操作码长度随指令地址数目的不同而不同
①硬件设计复杂;②指令译码时间开销较大;③指令空间利用率较高
-
-
指令长度:
- 定长指令系统:如MIPS指令
- 变长指令系统:一般为字节的整数倍,如X86指令
4.1.3 寻址方式
(1) 什么是寻址?
寻址方式 就是根据 形式地址 计算出操作数 有效地址 的方法。
- 形式地址:指令给出直接的操作数的地址编码
- 有效地址:操作数实际在内存中存储的地址
(2) 怎么寻址?
-
寻址方式的确定
-
在操作码中给定寻址方式:如 MIPS 指令,指令中仅有一个主(虚)存地址的,且指令中仅有几种寻址方式。 Load/store 型机器指令属于这种情况。
-
指令中专门的寻址方式位:如 X86 指令,指令中有多个操作数,且寻址方式各不相同,需要各自说明寻址方式。

-
-
按照指定方式进行寻址
寻址方式 示意图 说明 操作数位置 访问操作数所需访存次数 立即寻址 

指令中 0 寄存器直接寻址 

寄存器 0 寄存器间接寻址 

存储器 1 基址寻址 

存储器 1 变址寻址 

存储器 1 相对寻址 

存储器 1 堆栈寻址 

存储器 1
4.2 MIPS汇编语言
4.2.1 MIPS指令系统
(1) MIPS R2000/R3000 寄存器结构

(2) MIPS 寄存器使用约定
- 为了保持硬件的简单,汇编语言不使用变量,操作数都是寄存器(registers),寄存器没有数值类型(与C语言等高级语言不同)
- MIPS有 32 个寄存器,每个寄存器存放数据的宽度为 32 bits(1个字)

(3) MIPS 指令格式
-
MIPS 只有3种指令格式,32位固定长度指令格式
原因:冯·诺伊曼计算机建立在两个原则之上——①指令与数值的表示形式一致;②全部程序可以被存储在内存中,像数据一样被读写。为了简化计算机系统的软/硬件,使得适用于数据操作的内存技术完全适用于指令操作,MIPS将指令也按照数据相同的”按字存储“的方式存储,即一条指令占32位,同时将32位划分为不同的字段,每一个字段提供指令的一部分信息,不同的划分方式形成了不同的指令格式——R、I、J,详细见下表。
-
MIPS 指令格式一览表
R (Register ) I (Immediate) J(Jump) 


opcode:与 func 组合起来,决定该条指令名(操作符),opcode等于0时代表所有R类型指令;
rs (Source Register):通常指定存放第一个操作数;
rt (Target Register):通常指定存放第二个操作数;
rd (Destination Register):通常指定存放计算结果的寄存器;
shamt:存储执行移位运算时要移的位数,该字段在不进行移位的指令中通常置0opcode : I类型指令中opcode可以唯一确定一条指令;
rs:表示唯一的操作数寄存器;
rt:指定存储计算结果的寄存器;
立即数:16bits,可表示$2^{16}$ 个不同的整数值,在addi,slti,sltiu等指令中中立即数通过位扩展(符号扩展)方式扩展到32位opcode:与前两者一致;
target address:26bits,利用字对齐,可以表示出32-bit地址的28位add,addu,sub,subu,and,or,jraddi,ori,lui,lw,sw,beq,bnqj,jal说明:5bits指定寄存器位置正好可以对应32 ($2^5$)个寄存器 说明:如果立即数超过16bits所能表示的范围,可以借助 lui指令——将一个16bit的立即数存入寄存器的高16位,并将寄存器的低16位置 0$New PC = {\ PC[31..28],Addr,00\}$
(4) MIPS 指令语法
- 注意 “指令语法” 与 “指令格式” 的不同:指令语法是程序员书写的语法规范,语法是固定的,用过约定好的规则使硬件实现更简单;而指令格式是指机器码(二进制代码)的格式
- 指令语法:
操作符, 目标, 源1, 源2- 操作符:指明操作的名称
- 目标:存放操作的结果
- 源1 (2):第一 (二) 个操作数
(5) MIPS 寻址方式
-
MIPS 没有间接寻址,且Load/Store指令采用单一寻址模式(基址寻址)
-
MIPS 寻址方式

(6) MIPS 指令介绍
-
Load / Store 指令(取数/存储指令)
-
格式:I 类型指令
-
分类:①取数指令:
LB(取字节),LBU(取不带符号字节),LH(取半字),LHU(取不带符号的半字),LW(取字),LWL,LWR等;②存储指令:SB(存字节),SH(存半字),SW(存字),SWL,SWR等
-
-
运算指令
- 格式:R 类型指令, I 类型指令
- 分类:①算数运算:
add,addu,addi,addiu,sub,subu,mul,mulu,div,divu,mfhi,mflo等;②逻辑运算:and,andi,or,ori,xor,xori,nor等;③移位运算:sll,srl,sra,sllv,srlv,srav等
-
跳转指令
- 格式:J 类型指令,R 类型指令
- 分类:
j,jal,jr,jalr
-
转移指令
- 格式:I 类型指令
- 分类:
beq(相等转移),bne(不等转移),blez(小于或等于0转移),bgtz(大于0转移),bltz(小于0转移),bltzal,bgezal等
-
特殊指令
- 格式:R 类型指令
- 分类:
syscall(系统调用),break(断点)
-
R类型指令编码示例
-
使用MIPS实现swap递归函数

4.2.2 MIPS汇编语言详解
(1) MIPS的数据通路与内存布局

(2) MIPS的寄存器传送的控制逻辑

(3) MIPS 汇编语言中的 算术、逻辑、移位运算
-
整数加减法
- 加法:
add $s0, $s1, $s2 - 减法:
sub $s3, $s4, $s5
- 加法:
-
0号寄存器、立即数
-
0号寄存器:定义
$0为数字0,也可写作$zero -
立即数:即数值常量,MIPS针对立即数设置了专门指令,如
addi $s0, $s1, 10需要注意的是,MIPS没有立即数的减法,可以用立即数加实现
-
-
算数运算的溢出
- 可检测出溢出异常的指令:
add,sub,addi - 不会检测出溢出异常的指令:
addu,subu,addiu - MIPS中的C编译器会使用
addu,addiu,subu,即不检查溢出异常
- 可检测出溢出异常的指令:
-
移位运算
- 逻辑左移
sll:左移并且补0,位移量为立即数 - 逻辑右移
srl:右移并且补0,位移量为立即数 - 算数右移
sra:右移并且在空位做符号扩展填充,位移量为立即数 sllv,srlv,srav:移位量存储在寄存器中,处理方式与立即数位移量类似
- 逻辑左移
(4) MIPS 汇编语言中的 数据存取
-
MIPS算术指令只能操作寄存器,不能直接操作内存,需要通过数据存取指令在内存与寄存器之间传输数据
-
数据存取指令
-
内存到寄存器:
lwlw $t0, 12($s0):$s0称为基址寄存器,12称为偏移量 -
寄存器到内存:
swsw $t0, 12($s0):与lw相同
-
-
字节的存取指令
lb $s0, 3($s1):把内存中的某个地址(3 + s1中的地址值)所存储的数值拷贝到s0的低地址字节上(其余24位使用符号扩展)sb $s0, 3($s1):把s0的低地址字节所存储的数值存储到内存中的某个地址(3 + s1中的地址值)上
-
寻址:现代计算机按字节编址,32-bit 字地址按 4 递增
-
字对齐:对象的起始位置一定要是字长的整数倍,故MIPS中
lw和sw指令的基址寄存器的值与偏移量都应该是4的倍数
(5) MIPS汇编中的 分支、循环
-
条件分支指令
beq $1, $2, Label1:相当于C语言中的if($1==$2) goto L1;bne $1, $2, Label1:相当于C语言中的if($1!=$2) goto L2;
-
无条件分支指令
j Label:无条件跳转到标签label所在的代码,相当于goto Label
-
不等式指令
slt $1, $2, $3:相当于$1 = ($2 < $3) ? 1 : 0;slti:slt的立即数版本
(6) 一些小练习
-
IF - ELSE 语句
// C语言原代码 if (i == j) f = g + h; else f = g - h;# MIPS汇编实现代码如下 beq $s3, $s4, Label_True # branch i==j sub $s0, $s1, $s2 # f=g-h (false) j Label_Finish # goto Fin Label_True: add $s0, $s1, $s2 # f=g+h (True) Label_Finish: -
SWITCH 语句
// C语言原代码 switch (k) { case 0: f = i + j; break; case 1: f = g + h; break; case 2: f = g - h; break; case 3: f = i - j; break; } // 简化为IF-ELSE语句 if (k == 0) f = i + j; else if (k == 1) f = g + h; else if (k == 2) f = g - h; else f = i - j;# MIPS汇编实现代码如下 bne $s5, $0, Label1 # branch k!=0 add $s0, $s3, $s4 # k==0 so f=i+j j Label_Exit # end of case so Exit Label1: addi $t0, $s5, -1 # $t0=k-1 bne $t0, $0, Label2 # branch k!=1 add $s0, $s1, $s2 # k==1 so f=g+h j Label_Exit # end of case so Exit Label2: addi $t0, $s5, -2 # $t0=k-2 bne $t0, $0, Label3 # branch k!=2 sub $s0, $s1, $s2 # k==2 so f=g-h j Label_Exit # end of case so Exit Label3: addi $t0, $s5, -3 # $t0=k-3 bne $t0, $0, Label_Exit # branch k!=3 sub $s0, $s3, $s4 # k==3 so f=i-j Label_Exit: -
DO - WHILE 语句
// C语言原代码 do { g = g + A[i]; i = i + j; } while (i != h); // 改写为GOTO语句 Label_Loop: g = g + A[i]; i = i + j; if (i != h) goto Label_Loop;# MIPS汇编实现代码如下 Label_Loop: sll $t1, $s3, 2 #$t1= 4*i add $t1, $t1, $s5 #$t1=addr A lw $t1, 0($t1) #$t1=A[i] add $s1, $s1, $t1 #g=g+A[i] bne $s3, $s2, Label_Loop #if i!= h goto Label_Loop
(7) MIPS 支持函数功能的指令
-
可同时执行跳转和存储返回地址的指令
jal Label:执行步骤 - ①link:将下一条指令地址存入$ra;②jump:向指定的Label跳转- 一般配合
jr $ra使用,即jal Label存指令到$ra,并跳转到调用函数的头部,函数执行完毕后使用jr $ra跳转到调用处的下一条指令,完成一次函数的调用
-
嵌套调用的实现
- 嵌套调用会覆盖
$ra中的返回地址信息,故需要另寻他路——使用 Stack 栈
- 嵌套调用会覆盖
-
寄存器
$sp始终指向栈空间最后被使用的位置(可以理解为”栈指针“,栈从下往上递增,栈指针地址递减)- 调用规则:①将需要保存的值压入栈中;②指定参数(如果需要的话);③
jal调用函数;④从栈中恢复相关的值 - 调用过程中的规则:①通过
jal指令调用 , 使用jr $ra指令返回;②最多可接受4个入口参数——$a0,$a1,$a2,$a3;③
# C语言 原代码 int sumSquare(int x, int y) { return mult(x, x) + y; } # MIPS汇编实现 sumSqure: addi $sp, $sp, -8 # space on stack sw $ra, 4($sp) # save ret addr sw $a1, 0($sp) # save y add $a1, $a0, $zero # prep args jal mult # call mult lw $a1, 0($sp) # restore y add $v0, $v0, $a1 # mult()+y lw $ra, 4($sp) # get ret addr addi $sp, $sp, 8 # restore stack jr $ra mult: #mult函数 - 调用规则:①将需要保存的值压入栈中;②指定参数(如果需要的话);③
(8) MIPS 通用寄存器使用规范
- 通用寄存器分配(见 4.2.1(2)寄存器使用约定 )
- 特殊寄存器(系统维护)
$at:编译器随时可能使用,最好不要用$k0~$k1:操作系用随时可能使用,最好不要用$gp,$fp:自动维护,无需操作
- 保存寄存器(程序员维护)
$0:不能改变,恒为 0$s0-$s7:如果被修改了需要恢复。如果被调用函数改变了这些寄存器的值,则必须在函数返回之前将这些寄存器的原始值恢复$sp:如果被修改了需要恢复。栈指针在jal执行之前或之后必须是指向的同一地址,不然调用函数无法从栈里正确恢复数据
- 易变寄存器(程序员维护)
$ra:会改变。jal会自动更改这个寄存器值,但调用函数需要手动将其值保存在栈上$v0~$v1:会改变。始终保存最新的返回值$a0~$a3:会改变。调用函数如果在调用完成后还要用到这些寄存器中的值,就要在调用前将这些值保存在自己的栈空间内$t0~$t7:会改变。任何函数在任何时候都可以更新这些寄存器中的值;与$a0~$a3类似,调用函数时需手动将这些值保存在栈空间内以防止丢失(被覆盖)
(9) MIPS 汇编程序
-
汇编语言语句
- 可执行指令:为处理器生成在运行时执行的机器码,告诉处理器该做什么
- 伪指令、宏:由汇编程序翻译成真正的指令,简化编程人员的工作
- 汇编伪指令:当翻译代码时为汇编程序提供信息,用来定义段、分配内存变量等;不可执行 ——汇编伪指令不是指令集的一部分
-
程序模板
-
.DATA:伪指令,定义程序的数据段,程序变量需要在该伪指令下定义,汇编程序会分配和初始化变量的存储空间 -
.TEXT:定义程序的代码段 -
.GLOBL:伪指令,声明 一个符号为全局的,可被其它文件引用,通常用来声明一个程序的 main 过程

-
-
数据定义
-
目的:为变量的存储划分内存(可能会有选择的为数据分配标签)
-
语法:[名字:] 伪指令 初始值 [, 初始值 ] ……
-
数据伪指令
.BYTE:以 8 位字节存储数值表.HALF:以 16 位(半字长)存储数值表.WORD:以 32 位(一个字长)存储数值表.WORD w:n:将 32 位数值 w 存入 n 个边界对齐的连续的字中.FLOAT:以 单精度浮点数存储数值表.DOUBLE:以 双精度浮点数存储数值表 -
字符串伪指令
.ASCII:为 一个 ASCII 字符串分配字节序列.ASCIIZ:与 .ASCII 伪指令类似 , 但字符串 以 NULL 结尾.SPACE n:为 数据段中 n 个未初始化的字节分配空间 -
实例
.DATA var1: .BYTE 1, 2,'Z' str1: .ASCIIZ "My String\n" var2: .WORD 0x12345678汇编程序会为标签(也可理解为变量)构建符号表,为每一个数据段的标签计算地址,如下图。

-
-
内存对齐、字节序
-
.ALIGN n- 对下一个定义的数据做 $2^n$ 字节对齐 -
对齐:字的地址是4的整数倍(即地址的2位最低有效位必须是 $00_b$),半字的地址是2的整数倍
-
字节序、端
①小端字节排序:内存地址 = 最低有效字节 的地址,例子 : Intel IA-32, Alpha

②大端字节排序:内存地址 = 最高有效字节 的地址,例子 : SPARC, PA-RISC

▲注意:MIPS 可以操作以上两种字节序
-
-
系统调用
-
syscall:MIPS提供特殊的syscall指令,从操作系统获取服务 -
作用:程序通过系统调用实现输入输出
-
syscall系统服务流程:①从$v0寄存器中读取服务数;②从$a0,$a1等寄存器中读取参数值(如果有)③发送syscall指令;④从结果寄存器中取回返回值(如果有) -
程序示例 与 系统服务列表
move $a0, $s0 #copy value of $s0 to $a0 li $v0, 1 #load the service number syscall #system service start

-
-
参数传递
-
寄存器方法(使用通用寄存器
$a0~$a3) -
栈方法(使用栈
$sp)栈适用于以下情况:①不适用使用寄存器时,用来存储变量 / 数据结构;②过程调用中保存和恢复寄存器;③实现递归
-
(10) 一些小练习
-
选择排序
# Objective: Sort array using selection sort algorithm # Input: $a0 = pointer to first, $a1 = pointer to last # Output: array is sorted in place ########################################################## sort: addiu $sp, $sp, 4 # allocate one word on stack sw $ra, 0($sp) # save return address on stack top: jal max # call max procedure lw $t0, 0($a1) # $t0 = last value sw $t0, 0($v0) # swap last and max values sw $v1, 0($a1) addiu $a1, $a1, 4 # decrement pointer to last bne $a0, $a1, top # more elements to sort lw $ra, 0($ sp) # pop return address addiu $sp, $sp, 4 jr $ra # return to caller # Objective: Find the address and value of maximum element # Input: $a0 = pointer to first, $a1 = pointer to last # Output: $v0 = pointer to max, $v1 = value of max ########################################################## max: move $v0, $a0 # max pointer = first pointer lw $v1, 0($v0) # $v1 = first value beq $a0, $a1, ret # if (first == last) return move $t0, $a0 # $t0 = array pointer loop: addi $t0, $t0, 4 # point to next array element lw $t1, 0($t0) # $t1 = value of A[ ble $t1, $v1, skip # if (A[i] <= max) then skip move $v0, $t0 # found new maximum move $v1, $t1 skip: bne $t0, $a1, loop # loop back if more elements ret: jr $ra -
递归过程
int fact(int n) { if (n < 2) return 1; else return (n*fact(n-1)); }fact: slti $t0,$a0,2 # (n<2)? beq $t0,$0,else # if false branch to else li $v0,1 # $v0 = 1 jr $ra # return to caller else: addiu $sp,$sp, -8 # allocate 2 words on stack sw $a0,4($sp) # save argument n sw $ra,0($sp) # save return address addiu $a0,$a0, -1 # argument = n-1 jal fact # call fact(n-1) lw $a0,4($sp) # restore argument lw $ra,0($sp) # restore return address mul $v0,$a0,$v0 # $v0 = n*fact(n-1) addi $sp,$sp,8 # free stack frame jr $ra # return to caller
