(四)、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
,jr
addi
,ori
,lui
,lw
,sw
,beq
,bnq
j
,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算术指令只能操作寄存器,不能直接操作内存,需要通过数据存取指令在内存与寄存器之间传输数据
-
数据存取指令
-
内存到寄存器:
lw
lw $t0, 12($s0)
:$s0
称为基址寄存器,12
称为偏移量 -
寄存器到内存:
sw
sw $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