LOADING

加载过慢请开启缓存 浏览器默认开启

MIPS汇编

寄存器

32个通用寄存器(GPR)

编号从00000到11111

image-20240924103709031

$zero的值永远是0,无法改变

$1保留给汇编器,一般不使用此寄存器

三个特殊寄存器

  • PC:它用于存储当前 CPU 正在执行的指令在内存中的地址。需要注意的是,这个寄存器的值不能用常规的指令进行取值和赋值,但这并不意味着不能做到,只是麻烦一些。那么,怎样对其取值、赋值呢?这可以作为一个思考的题目,在之后的学习中自行探索。(提示:跳转)
  • HI:这个寄存器用于乘除法。它被用来存放每次乘法结果的高 32 位,也被用来存放除法结果的余数
  • LO:HI 的孪生兄弟。它被用来存放每次乘法结果的低 32 位,也被用来存放除法结果的

CP0寄存器

CP0 是一个系统控制协处理器,而 CP0 寄存器则是该协处理器工作时需要用到的一些寄存器。在我们的实验中,只会用到其中的 4 个寄存器:SR、Cause、EPC 和 PRId。

  • **SR:**用于系统控制,决定是否允许异常和中断
  • **Cause:**记录异常和中断的类型
  • **EPC:**保存异常或中断发生时的 PC 值,也就是发送异常或中断时 CPU 正在执行的那条指令的地址。当处理完成之后,CPU 会根据这个地址返回到正常程序中继续往下执行。
  • **PRId:**处理器 ID,用于实现个性的寄存器。

MIPS汇编指令

指令,即是由处理器指令集架构(Instruction Set Architecture,可以理解为计算机体系结构中对程序相关的部分所做的定义)定义的处理器的独立操作,这个操作一般是运算、存储、读取等。一个指令在 CPU 中真正的存在形式是高低电平,也可以理解为由 01 序列组成的机器码。

为了便于人类理解,指令一般由汇编语言来表示。汇编指令只是指令的一种表示形式而已,其实质是一样的。

指令的格式

在 MIPS 汇编语言中,指令一般由一个指令名作为开头,后跟该指令的操作数,中间由空格或逗号隔开。

指令的操作数一般为0~3个,每个指令有其固定的操作数个数。一般格式如下两种:

指令名 操作数 1, 操作数 2, 操作数 3

指令名 操作数 1, 操作数 3(操作数 2)//一般用于存取

所谓操作数,就是指令操作所作用的实体,可以是寄存器立即数标签,每个指令有其固定的对操作数形式的要求。标签最终会由汇编器转换为立即数。

  • 立即数:在指令中设定好的常数,可以直接参与运算,一般为16位二进制

  • 标签:表示一个地址,以供指令来引用。一般用于表示一个数据存取的地址(类似于数组名)、或者一个程序跳转的地址(类似于函数名,或者 C 语言中 goto 的跳转目标)。标签用如下的方式写出:

    name:

    name代表这个标签的名称,可以自行取名,这一行不占用内存

add $s0, $a0, $a1
addi $s0, $a0, 12
mult $s1, $s2
beq $a1, $a2, -2
blez $s1, -2
jr $ra
j 0x00003014
可写成如下形式
beq $a1, $a2, loop
j loop

这里的loop就是一个标签,代表一段代码的起始地址。在进行汇编时,汇编器会自动把标签转换为我们所需要的立即数,这样就不用我们自己去计算这些地址偏移量,简化了编程难度。

注意:标签标记的是一个位置,而不是某一行!跳转后会从标签的下一行开始逐行执行

注意:在 MARS 中,跳转指令只能使用标签来进行跳转,不能使用立即数!

由此可以看出,操作数的形式并非严格固定,而是具有一定的灵活度。虽然在 MIPS 标准指令集中,一条机器码指令的格式是固定的,但汇编器可以将多种形式的汇编指令转换为同样意思的机器码指令。因此,许多指令有比标准写法简单的写法。

机器码指令

计算机只能理解二进制形式的数据,而我们前面所说的汇编语言,最终就会转化为机器语言——也就是机器码指令, CPU 可以直接识别这种机器语言,从而去完成相应的操作。

在我们学习的 MIPS 汇编中,所有的指令长度均为 32位 ,即 4 字节,或者说 1 字。以16进制来表示就是8位16进制数。例如下面这段汇编指令:

.data
.text
.global main
main:
addi $t0, $0, 100
ori $t1, $0, 200
add $t2, $t1, $t2
sub $t3, $t2, $t1
lui $t4, 233
ori $v0, 1
ori $a0, 2333
mthi $t1
syscall
nop
loop:
j loop
nop
转换后的结果为(16进制)
20080064
340900c8
012a5020
01495822
3c0c00e9
34420001
3484091d
01200011
0000000c
00000000
08000c0a
00000000

机器码指令格式

在 MIPS 指令集中,指令分为三种格式:R 型、I 型和 J 型。

以下的32位指的是二进制数。

  • R型指令

    操作数最多,一般用于运算指令。例如addsubsll等。其格式如下:

    image-20240924114844056

  • I型指令

    I 型指令的特点是有 16 位的立即数。一般用于addisubiori等与立即数相运算的指令。(这里需要注意:在写汇编语言的时候,需要使用负号来标记负数,而不要和机器码一样认为首位的 1 就代表负数),或 beqbgtz 等比较跳转指令,因为它们要让两个寄存器的值相比并让 PC 偏移 offset 这么多,刚好利用了全部的字段。还有存取指令,例如 swlw,它们在使用时需要对地址指定一个偏移值,也会用到立即数字段。

    image-20240924115256457

  • J型指令

    很少,常见的为 jjal。他们需要直接跳转至某个地址,而非利用当前的 PC 值加上偏移量计算出新的地址,因此需要的位数较多。

    image-20240924115351775

严格来说,并非所有的指令都严格遵守上面三种格式,有的如 eretsyscall 指令一样没有操作数;有的如 jalr 指令一样某些字段被固定为某个值。不过,就大部分指令而言,都可按上面三种格式进行解释,某些字段被固定也可以按照格式来识别为 R、I、J 中的一种,因此这三种格式要着重理解。

解读:

  • **op:**也称 opcode、操作码,用于标识指令的功能。CPU 需要通过这个字段来识别这是一条什么指令。不过,由于 op 只有 6 位,不足以表示所有的 MIPS 指令,因此在 R 型指令中,有 func 字段来辅助它的功能。

  • func: 用于辅助 op 来识别指令。

  • rsrt、**rd:**通用寄存器的代号,并不特指某一寄存器。范围是$0~$31用机器码表示就是 00000~11111(将指令翻译成机器码时会用到)。

  • **shamt:**移位值,用于移位指令。

  • **offset:**地址偏移量。在跳转中,它的值就等于:从该条指令的下一条指令开始数,到目标label一共经过的指令条数(字数)。为了程序员的便利,MIPS只允许使用标签来跳转,不过我们仍然有手段计算offset的具体值,在此之前,我们需要先了解指令集中为什么是这样计算的。

    仔细观察跳转指令,以beq为例,

    目标地址 = 当前指令地址 + 4 + (offset × 4)

    当前指令地址 + 4就是下条指令指令地址

    当前指令地址 + 4 + (offset × 4)是目标地址,故offset就等于下条指令到目标lable经过的指令条数(字数)。

    为什么要拓展到32位?因为MIPS使用的是32位地址空间,这意味着内存地址都是32位的数字。而offset常为16位的数字,因此需要转换。

    题外话,在将offset × 4时,使用的手段是将它左移两位,这使得地址的最后两位永远是00。这吻合了MIPS里的存储方式,每条指令是4字节对齐的,每条指令的地址一定是4的倍数,最低两位只能是00。(这会影响到跳转有效地址的范围)

  • **immediate:**立即数

  • **address:**跳转目标地址,用于跳转指令。

扩展指令和伪指令

扩展指令

汇编器将一些常用、但标准指令集不提供的功能封装为一条指令;或者改变现有指令的操作数的形式或个数,使其以新的形式出现。需要注意的是,它们只是形式上是一条新指令,而实际上,在汇编器将其汇编之后,还是使用标准指令来实现的。

  • li指令:用来为某个寄存器赋值,如li $a0, 100就是将100赋给$a0寄存器
  • la指令:使用标签给寄存器赋值,标签本质上对应一个32位地址,la $t0, fibs就是将fibs这个标签的地址存入$t0

伪指令

伪指令是用来指导汇编器如何处理程序的语句,有点类似于其他语言中的预处理命令。伪指令不是指令,它并不会被编译为机器码,但他却能影响其他指令的汇编结果。

伪指令的主要用途是标识数据段和代码段的位置,并为声明的数据分配空间,以及初始化常量

  • .data

    格式:.data[address]

    说明:

    • 定义程序的数据段,初始地址为address,若无address参数,初始地址为设置的默认地址
    • 需要声明的程序变量需要紧跟着该指令
  • .text

    格式:.text[address]

    说明:

    • 定义程序的代码段,初始地址为address,若无address参数,初始地址为设置的默认地址
    • 该指令后面就是程序代码
    • 在 MARS 中如果前面没有使用 .data 伪指令,可以不使用 .text 直接编写程序代码,代码将放置在前面设置的代码段默认地址中,但如果前面使用了 .data 伪指令,务必在代码段开始前使用 .text 进行标注。
  • .space

    格式:[name]: .space [n]

    说明:

    • 申请n个字节未初始化的内存空间,类似于其他语言中的数组声明
    • 这段数据的初始地址保存在标签name
    • name的地址是由.data段的初始地址加上前面所申请的空间大小计算得出的。由于前面申请的空间大小不定,有可能会出现后来申请的空间没有字对齐的情况,从而在使用 sw,lw 一类指令时出现错误,所以在申请空间时尽可能让 n 为 4 的倍数,防止在数据存取过程中出现问题。

    用法:用来声明一个可能用到的空数组

  • .word

    格式:[name]: .word [data1],[data2]...

    说明:

    • 在内存数据段中以为单位连续存储数据data1data2,…即,将datax写入对应1个字的空间
    • 这段数据的初始地址保存在标签name

    用法:用来存储一个或多个常量,以供以后使用

  • .asciiz

    格式:[name]: .asciiz "[content]"

    说明:

    • 字节为单位存储字符串,末尾以NULL结尾
    • 这个字符串在内存数据区的初始地址保存在标签name
    • 注意与.ascii伪指令的区别
    • .asciiz 由于是按字节存储,可能会导致之后分配的空间首地址无法字对齐的情况发生

    用法:用来存储一个字符串,以供以后使用

可以把宏理解为浓缩的代码段,汇编器会把所有的宏语句替换为相应的代码段

不带参数的宏

.macro macro_name
# 代码段
.end_macro

如:

.macro done
li $v0, 10
syscall
.end_macro

在需要程序停止运行的地方,使用 done 语句,就可以让程序在那里退出。

带参数的宏

.macro macro_name(%parameter1, %parameter2, ...)
# 代码段
.end_macro

带参数的宏在 macro_name 后面有若干个用括号括起来的形式参数,每两个形式参数中间用逗号隔开,参数名前面有一个百分号。

如:

.macro  getindex(%ans, %i, %j)
    sll %ans, %i, 3
    add %ans, %ans, %j
    sll %ans, %ans, 2
.end_macro

宏定义

与C语言中类似,一般用于常量的定义,是.eqv

用法:.eqv EQV_NAME string

汇编器会把所有 EQV_NAME 的地方替换成 string,这可以用来定义一些常量。

条件语句

.text
li $t1, 100             #t1 = 100
li $t2, 200             #t2 = 200
slt $t3, $t1, $t2       #if(t1 < t2) t3 = 1 
beq $t3, $0, if_1_else  #if($t3 == 0) ->if_1_else
nop
#do something
j if_1_end              #jump to end
nop
if_1_else:
#do something else

if_1_end:
li $v0, 10
syscall

循环语句

.text
li $t1, 100 #n
li $t2, 0 #i

for_begin1: #for (int i = 0; i < n; i++)
beq $t2, $t1, for_end1
#do something
addi $t2, $t2, 1 #i++
j for_begin1

for_end1:
li $v0, 10
syscall

一些细节

  • 小端存储和大端存储

    小端存储是指最低有效字节放在内存的最低地址,最高有效字节放在内存的最高地址

    eg.假设某条指令的机器码为0x00852020,在小端存储格式中,这4个字节在内存中的存储顺序应该是:

    地址
    0x1000 0x20
    0x1001 0x20
    0x1002 0x85
    0x1003 0x00

    值得一提的是,小端存储仅指某条指令的机器码内部存储方式,多条指令之间依旧是递增存储的,如下:

    add $s0, $a0, $a1 #机器码为0x00852020
    addi $s0, $a0, 12 #机器码为0x2080000C
    
    地址
    0x1000 0x20
    0x1001 0x20
    0x1002 0x85
    0x1003 0x00
    0x1004 0x0c
    0x1005 0x00
    0x1006 0x80
    0x1007 0x20
  • slt

  • 调用函数时配套使用jaljrjr能带你回到原函数

  • 编写程序时,十进制表示的立即数用负号区分正负,十六进制表示的立即数会被视为一个补码表示的数

  • 一个数的补码已知,它的相反数的补码:取反加1

  • 在测试中,ori指令可以当作limove来使用

  • lui指令的全称应该是:将立即数加载到高位,且将低位置0

指令 Opcode RegWrite RegDst ALUSrc Branch MemWrite MemtoReg ExtOp ALUControl 类型
add 000000 1 1 0 0 0 0 X 010 R
sub 000000 1 1 0 0 0 0 X 110 R
ori 001101 1 0 1 0 0 0 0 001 I
lw 100011 1 0 1 0 0 1 1 010 I
sw 101011 0 X 1 0 1 X 1 010 I
beq 000100 0 X 0 1 0 X X X I
lui 001111 1 0 1 0 0 0 X 011 I
nop

拓展元件:0是无符号拓展,1是符号拓展