我们清楚,不论什么高级语言编写的源程序,最终都必须“翻译“成指令形式表示的机器语言,才能够在计算机上运行。而机器语言程序的每条指令都是由一串由0、1组成的二进制数字序列,所以要读懂一个机器语言程序很费劲,这很糟糕,为了能直观的表示机器语言程序,引入了一种与机器语言一一对应的符号化表示语言,称为汇编语言。
汇编语言
汇编语言中,用助记符来表示指令操作码的含义,用标号、变量名称、寄存器名称、常数等表示操作码或地址码。
以下简要介绍MIPS指令系统和MIPS汇编语言。
MIPS
MIPS(Microprocessor without Interlocked Pipeline Stages):无互锁流水级的微处理器,是一种采取精简指令集(RISC)的指令集架构(ISA)。
精简指令集(RISC):用一系列简单的指令来完成一项任务。
复杂指令集(CISC):用尽可能少的指令来完成尽可能多的任务。
MIPS寄存器种类
MIPS提供了32个32位通用寄存器,寄存器编号占5位(32个),各寄存器名称、编号、功能见下表。
REGISTER | NAME | USAGE |
---|---|---|
$0 | $zero | 常量0 |
$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 | 为OS保留 |
$28 | $gp | 全局指针 |
$29 | $sp | 堆栈指针 |
$30 | $fp | 帧指针 |
$31 | $ra | 过程调用返回地址 |
- 寄存器的汇编表示以$符号开始,可以使用名称,亦可以使用编号。
- MIPS中用PC指出下一条指令地址。
- MIPS的存储器按字节编址
- 对于存储器数据,其操作数地址为32位,通过一个寄存器内容加16位偏移量得到
- 16位偏移量是带符号整数,故可访问的地址空间大小为4GB
- 采用大端方式存储数据,数据要求按字边界对齐,通过Load/Store指令访问存储器数据
- 对于立即操作数(16位),需要对其进行符号位扩展(32位)运算。
MIPS指令格式
指令分为三种类型:R-型(Register)、I-型(Immediate)和J-型(Jump)。三种类型的指令的最高6位均为6位的opcode码
R-型指令
R型指令操作码OP是000000,接着用连续三个5位二进制码来表示三个寄存器(rs、rt,rd)的地址,然后用一个5位二进制码(shamt)来表示移位的位数(如果未使用移位操作,则全为0),最后为6位的function码决定操作类型。
R-型指令的寻址方式只有一种,就是寄存器寻址。
I-型指令
I型指令是立即数型指令,I型指令则用连续两个5位二进制码(rs,rt)来表示两个寄存器的地址,然后是一个16位二进制码来表示的一个立即数二进制码。
I-型指令的寻址方式有4种:寄存器寻址、立即数寻址、相对寻址、变址寻址。
J-型指令
J-型指令主要是无条件跳转寻址。
J型指令用26位二进制码来表示跳转目标的指令地址(实际的指令地址应为32位,其中最低两位为00)。
指令中给出的是26位的直接地址,只要将当前PC的高四位拼上26位直接地址,最后添两个0即可得到跳转目标地址。
之所以要在跳转目标最后两位添0的原因是:
- MIPS存储单元采用字节编址,一条指令占据4个存储单元。
- 指令地址总是4的倍数,即指令的最后两位总是0。
最后两位00不必在指令中显式给出,想要实现此功能只需要在数据通路中添加00的电路即可。
MIPS汇编语言程序结构
下面以一段MIPS汇编语言程序为例说明程序格式和运行过程。
数据声明
- 数据段以.data为开始标志。
- 声明变量后,即在主存中分配空间。
格式:name: type value(s)
- name: 变量名
- type:数据类型(.word,.ascii,.data, .byte等)
- .space 需要指明(bytes)
- value:初始值
例:
1 | .data |
说明:
- ’.data’部分定义了一个名为’pof2’的数组,数组中包含了六个整数,分别是’1,2,8,4,16,10’
- 一个变量’num’用来存储数组元素个数
- 一个’max’变量用来存储最大值
程序主体
程序入口以main:为标志
然后,’.text’部分开始定义程序中的主函数’main’。
1 | .text |
该函数首先使用’lw’指令从变量’num’中加载数组元素的个数,并使用’la’指令将数组’pof2’的地址存储在寄存器’$s2’中。另外,使用’la’指令将变量’max’的地址存储在寄存器’$s6’中。
1 | la $s2,pof2 # 数组地址存入寄存器$s2 |
使用’move’指令将寄存器’$s0’和’$t7’的值都设置为’0’。其中,寄存器’$s0’用于存储当前数组元素的下标,寄存器’$t7’用于存储当前处理的数组元素的地址。然后使用lw指令将第一个数组元素的值加载到寄存器’$S5’中,作为目前最大值的初始值。
1 | move $s0, $zero # 正在遍历数组元素的序号i |
接下来是循环部分
- 使用标签’for_loop’定义一个循环,该循环的目的是遍历整个数组并找出最大值。首先使用’sltu’指令比较’$s0’和’$s1’(数组元素的个数),显然,按照我们的想法就是如果当前遍历的数目小于数组元素总个数则继续,反之则停止,将比较结果存储在寄存器’$t1’中。(如果’$s0’小于’$s1’,则’$t1’为1,如果’$s0’大于等于’$s1’,则’$t1’为0,因为‘$s0’是从0开始的)
- 使用’beq’指令判断’$t1’的值是否为0,如果为0,则跳转到标签’exit’处,即退出循环,如果’$t1’的值为1,sll指令是逻辑左移指令,将’$s0’中存储的值左移两位,保证是4的倍数存储寄存器’$t7’,将’$s2’中地址加上偏移量放入’$t7’中,得到pof2[i]的地址,同时将’$s0’中值加1,将($t7)中元素中值取出放入’$t3’中,使用’slt’指令比较当前数组元素的值和已知的最大值,将比较结果存储在寄存器’$t2’中。如果当前数组元素的值比已知的最大值大,则跳转到标签’max2’处更新最大值,并继续进行循环,否则,直接跳转到标签’loop’处继续进行循环。
- 在标签’max2’:使用’move’指令将当前数组元素的值存储在寄存器’$s5’中,作为新的最大值,然后跳转到loop处进行循环。
1 | for_loop: sltu $t1, $s0, $s1 #遍历完t1为0,没遍历完t1为1 |
当不满足条件时循环已经结束,执行’exit:’部分,’sw’指令将寄存器’$s5’中值存入max所在地址处,’li’指令将系统调用号设置为1,负责在控制台打印一个整数,move指令将’$s5’寄存器中数值传入寄存器’$a0’中,syscall系统调用执行与存储在寄存器’$v0’中的系统调用号相应的代码,将打印出数组中的最大值。li $v0 10 将退出程序的系统调用号加载到寄存器’$v0’中,syscall调用系统调用以退出程序。
1 | exit: sw $s5,0($s6) #将最大值存入max所在地址 |
系统调用号
在MIPS汇编中,不同的系统调用对应于不同的系统调用号,这些号码都是整数变量,定义在MIPS操作系统的头文件中,以下是一些常见的MIPS系统调用和他们的系统调用号。
系统调用 | 系统调用号 | 描述 |
---|---|---|
‘print_string’ | ‘4’ | 在控制台打印一个字符串 |
‘read_int’ | ’5‘ | 在控制台读取一个整数 |
‘print_int’ | ’9‘ | 分配一段内存并返回指向新内存的指针 |
‘exit’ | ’10‘ | 终止程序并返回操作系统 |
当系统想要调用系统调用时,需要将相应的系统调用号存储在寄存器’$v0’中,并根据系统调用的要求,将参数存储在存储器’$a0’、’$a1’、’$a2’中,然后执行’syscall’指令以触发相应的系统调用。
过程帧和堆栈
- 过程帧是过程在运行时的存储区域,通常包括返回地址、调用者保存的寄存器、局部变量和参数等。
- 栈是从高到低存储的,记录堆栈生长的方向一般是向下,即堆栈指针$sp的值减小表示栈帧增长。
- 帧和栈的关系是,每当一个过程被调用时,都会为它分配一个新的帧,这个帧被存储在栈中。
- 调用结束后,帧被销毁,同时栈指针$sp也随之恢复。