汇编语言是使用符号格式表示指令,可直接面向机器内存以及寄存器的程序设计语言。本部分主要学习内容包括:
1、以MIPS指令系统为例,学习并掌握MIPS指令集及汇编语言,能够使用MIPS汇编语言编写顺序、分支、循环等具有特定功能的结构化程序,掌握函数调用及栈操作等汇编程序设计方法。该部分内容请参考《数字设计和计算机体系结构》第6章
MIPS体系结构的寄存器
-
MIPS体系结构定义了32个寄存器
-
通用寄存器标志由$符号开头,寄存器表示有两种方式
-
直接使用该寄存器对应的编号,例如:从$0到$31
-
使用对应的寄存器名称,例如:$t0,$t1,……, $sp等
-
-
每个寄存器都有其特定的作用,有相应的规范,可参考《数字设计和计算机体系结构》第6章表6-1
MIPS汇编语句
-
MIPS汇编中一般有3类语句,通常是一个语句一行
(1)可执行指令(instruction),是处理器生成在运行时执行的机器码,告诉处理器该做什么,一条可执行指令对应一条机器指令
(2)伪指令(Extended (pseudo) instruction)和宏,简化编程人员的工作,由编译器编译成一条或多条可执行指令,常见的伪指令如move,la,li等(相应的伪指令在Mars中的Help中有解释)
(3)汇编器指令(伪操作)(Directives),不会被编译器编译成可执行指令(或机器指令),会被翻译成预处理指令,用于指示编译器工作,用来定义段、分配内存变量等;汇编器指令不是指令集的一部分
-
MIPS汇编语言指令格式: [ 标签:] 操作符 [ 操作数 ] [ #注释 ]
-
标签:(可选),标记内存地址,必须跟冒号。通常在数据和代码段中出现
-
操作符:定义操作(比如 add,sub,sll等)
-
操作数:指明操作需要的数据;可以是寄存器、内存变量或常数(即立即数);大多数指令有3个操作数
-
#注释:由 # 开头在1行内结束,提高程序可读性离不开它,使用它是一个好编程习惯的体现。
-
-
指令中出现的常量数值被称为立即数(有的操作符对应的操作数是常数:addi,lw,sw等,具体可以浏览Mars中的Help)
-
load指令和store指令
由于MIPS只能对寄存器与立即数进行运算,因此必须有特定的数据传输指令实现主存单元与寄存器的数据交换
并且可以区分一下lw,lb,lh,sw,sb,sh的区别
-
-
LOAD类指令:主存单元→寄存器
-
STORE类指令:寄存器→主存单元
-
-
跳转指令(用来实现判断以及循环)
-
-
b类型指令就是通过相应的寄存器判断是否符合跳转条件,符合就跳转到相应的标志上去(后文有讲标志)
例:beq $s0,$t0,loop(更多b类指令请浏览Help)
-
-
-
j类型指令只能单独跳转到相应的标志位,且跳转范围不如jar,jalr等指令
loop:
xxxx
xxxxx
j loop
-
这里的跳转需要注意$ra寄存器,他会记录每一次跳转之后的PC+4值。(这里的PC可以简单理解为当前程序执行的代码,+4就是下一条代码的位置)所以在函数调用中用到了j类指令,注意维护$ra的值(jr $ra是返回记录的相应位置)
-
-
-
循环的例子(可参考《数字设计和计算机体系结构》6.4.4节)
-
常见指令示例请参考《数字设计和计算机体系结构》第6章
MIPS汇编程序结构
汇编程序的结构一般包括数据段和代码段
-
数据段以汇编器指令.data为开始标志
-
代码段以汇编器指令.text为开始标志
基本程序模板
.data # 数据段,主要是数据变量声明
...
.text # 代码段
main: # 主程序入口
...
li $v0,10 #退出程序
syscall
可以注意到这段模板中main这个标志(lable)。在mips代码段中,需要跳转或函数调用的时候都需要标志的指示,来判断相应跳转的位置。
数据声明(数据定义)
-
为变量的存储划分内存
-
可能会有选择的为数据分配名字(标签)
-
-
语法
-
[名字:] 汇编器指令 初始值[,初始值]....
var1: .word 10
-
所有的初始值在内存中以二进制数据存储
-
-
数据汇编器指令
-
.byte汇编器指令,以8位字节存储数值表
-
.half 汇编器指令,以16位(半字长)存储数值表
-
.word 汇编器指令,以32位(一个字长)存储数值表
-
.word w:n汇编器指令,将32位数值w存入n个边界对齐的连续的字中
-
.float汇编器指令,以单精度浮点数存储数值表
-
.double 汇编器指令,以双精度浮点数存储数值表
-
-
字符串伪指令
-
.ascii 汇编器指令,为一个ASCII字符串分配字节序列
-
.asciiz 汇编器指令,与.ascii伪指令类似,但字符串以NULL结尾
-
.space n 汇编器指令,为数据段中n个未初始化的字节分配空间
-
字符串中的特殊字符(按照C语言的约定),“新行:\n,Tab:\t,引用:\”
-
数据定义的例子:
注:上图应该为.byte .asciiz .word
-
汇编程序为标签(变量)构建符号表
-
为数据段的每一个标签计算地址
-
内存对齐和字节排序
-
对齐:地址是空间大小的整数倍
-
字的地址是4的整数倍
-
地址的2位最低有效位必须是00
-
-
半字的地址是2的整数倍
-
-
.align n汇编器指令,对下一个定义的数据做2^n字节对齐
-
字节序和端
-
处理器对一个字内的字节排序有两种方法
-
小端字节排序
-
内存地址=最低有效字节的地址,例子:Intel IA-32,Alpha
-
-
大端字节排序
-
内存地址=最高有效字节的地址,例子:SPARC,PA-RISC
-
-
MIPS可以操作以上两种字节序
-
函数调用(过程调用)
-
swap 过程(C程序)
-
翻译成 MIPS 汇编语言
-
-
调用swap过程:swap(a,10)
-
将数组a的地址和10(第10个元素)作为参数传递
-
调用swap过程,保存返回地址 $31 = $ra
-
执行swap过程,返回对返回地址的控制
-
void swap(int v[] , int k)
{ int temp;
temp = v[k];
v[k] = v[k+1];
v[k+1] = temp;
} //C-code
swap:
sll $t0,$a1,2 # $t0 = k*4
add $t0,$t0,$a0 # $t0 = v+k*4
lw $t1,0($t0) # $t1 = v[k]
lw $t2,4($t0) # $t2 = v[k+1]
sw $t2,0($t0) # v[k] = $t2
sw $t1,4($t0) # v[k+1] = $t1
jr $ra # return
参数:
$a0 = v[]的地址
$a1 = k,
返回地址在$ra中
函数调用中两个重要的指令jal和jr
-
JAL(Jump-and-Link):调用指令
-
寄存器$ra=$31被JAL用来保存返回地址($ra=PC+4)
-
通过伪直接寻址转跳
-
-
JR(Jump Register):返回指令
-
跳转到在寄存器Rs(PC=Rs)中存储的地址所在指令
-
-
JALR(Jump-and-Link Register)
-
在Rd=PC+4中存储返回地址
-
跳转到在寄存器Rs(PC=Rs)中存储的地址所在过程
-
用于调用方法(地址仅在运行时可知)
-
参数传递
-
汇编语言中的参数传递比高级语言中复杂
-
将所有需要的参数放置在一个可访问的存储区域
-
然后调用过程
-
-
会用到两种类型的存储区域
-
寄存器:使用通用寄存器(寄存器方法);内存:使用栈(栈方法)
-
-
参数传递的两种常用机制
-
值传递:传递参数值:引用传递:传递参数的地址
-
按照约定,参数传递通过寄存器实现
$a0 =$4 .. $a3=$7用来做参数传递
$v0=$2. . $v1=$3用来表示结果数据
-
其它的参数/结果可以放在栈中
-
-
运行时栈用于
-
不适合使用寄存器时用来存储变量/数据结构
-
过程调用中保存和恢复寄存器
-
实现递归
-
-
运行时栈通过软件规范实现
-
栈指针 $sp = $29(指向栈顶):帧指针 $fp = $30(指向过程帧)
-
栈
-
由于寄存器只有32个,因此编译器绝大多数情况下不可能把函数需要的所有局部变量都分配在寄存器
栈指针使用的顺序是从上往下存储。栈指针$sp一般在函数调用的时候会用到,在每次调用函数的时候,要将相应的$ra寄存器的值以及相应保存当前状态的寄存器的值存入到栈中,在需要返回的时候,又将相应的值取出,进行返回。
请参考《数字设计和计算机体系结构》第6章 6.4.6节
递归
首先需要理解递归本身的性质或运行流程,在此基础上进行mips编写
如有疑问,也可浏览课程网站观看快速排序讲解视频
-
递归流程:判断是否终止递归、进入终止输出并回溯 或 进入包含递归模块的核心部分
-
递归模块重点:递归终止条件、递归进入变量存储、递归回溯
-
以实现下列C代码的递归程序为例
-
$ra中存储着jal指令的下一指令,即本次递归结束时的下一条指令地址
sw $ra, 0($sp) # 保存返回地址
addi $sp, $sp, -4 # 栈指针下移四字节
sw index, 0($sp) # 保存函数参数index
addi $sp, $sp, -4 # 栈指针下移四字节
sw i, 0($sp) # 保存函数参数i
addi $sp, $sp, -4 -
按照存储顺序的逆序将本递归的参数取回来继续完成本次递归函数
sw $ra, 0($sp) # 保存返回地址
addi $sp, $sp, -4 # 栈指针下移四字节
sw index, 0($sp) # 保存函数参数index
addi $sp, $sp, -4 # 栈指针下移四字节
sw i, 0($sp) # 保存函数参数i
addi $sp, $sp, -4 # 栈指针下移四字节 -
递归回溯后的操作:
bne $t4, 8, nxt
li $s1, 0
nxt:
add $s2, $s1, $s0
sw $s2, fib_array($t4)
move $s1, $s0 #前前一个
move $s0, $s2 # $s0 前一个
jr $ra
-
系统调用
-
系统调用可以简单的理解为发挥执行输入,输出,结束程序的作用(具体也浏览Help)
-
参数所使用的寄存器:$v0, $a0,$a1
-
返回值使用: $v0
详细用法(部分):
Service | Code in $v0 |
---|---|
print integer | 1 |
print float | 2 |
print double | 3 |
print string | 4 |
read integer | 5 |
read float | 6 |
read double | 7 |
read string | 8 |
sbrk(allocate heap memory) | 9 |
exit(teminate execution) | 10 |
e.g. Print out integer value contained in register $t2
li $v0, 1
move $a0, $t2
syscall
e.g. Read integer value, store in RAM location with label int_value (presumably declared in data section)
li $v0, 5
syscall #输入一个整数。并把整数值赋给$v0寄存器
e.g. Print out string (useful for prompts)
.data
string1 .asciiz "Print this.\n"
.text
li $v0, 4
la $a0, string1 #将要打印的字符串地址赋值 $a0 = address(string1)
syscall
e.g. To indicate end of program, use exit system call
li $v0, 10 # system call code for exit = 10
syscall
课下题目分析
最大公约数
-
考察程序基本结构是否理解(输入,输出,判断条件)
-
考察通过用寄存器保存相应的值并进行计算
-
考察对于相关指令的使用
-
循环的建立
字符串逆置
-
考察对于字符串的读写(参考syscall里的用法)
-
难度较为简单,不用想复杂了
汉诺塔
-
考察对于栈的了解
-
考察对于递归的理解
-
考察jal,jr,$ra以及$sp的理解与使用
-
难度较为复杂,不过在了解完递归后,会非常清晰