北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析

北航计算机学院-计算机组成原理课程设计-2021秋

PreProject-MIPS

MIPS 汇编程序解析


本系列所有博客,知识讲解、习题以及答案均由北航计算机学院计算机组成原理课程组创作,解析部分由笔者创作,如有侵权联系删除。


从本节开始,课程组给出的教程中增添了很多视频讲解。为了避免侵权,本系列博客将不会搬运课程组的视频讲解,而对于文字讲解也会相应地加以调整,重点在于根据笔者自己的理解给出习题的解析。因此带来的讲解不到位敬请见谅。


第一个汇编程序

现在,我们通过几个简单的汇编程序,理解汇编语言的形式和各个指令的用途。同学们可以把这些代码复制到自己的 MARS 编辑器中,并在自己的电脑上运行一遍,注意 MARS 调试功能的使用,使得同学们可以更清楚地看到寄存器中数值的变化,明白各个指令的意义。有关循环的一些内容,在后文中会有详细的解释。

下面以一个经典的题目为例,编写我们的第一个汇编程序。

题目内容:输出 ”Hello World“

文本代码

.data
str: .asciiz    "Hello World"

.text
la  $a0, str
li  $v0, 4
syscall

li  $v0, 10
syscall             

这是最简单的一个 MIPS 汇编程序,同学们可以把这段代码复制到 MARS 中,在运行之前确保(Mars->Settings->Memory Configuration)为 Compact,Data at Address 0。这个选项,代表数据(.data)段的地址是从 0x0 开始,程序(.text)段的地址是从 0x3000 开始。

下面我们一行一行地分析这段代码。

第 1 行 .data,代表变量的声明和分配从这里开始

第 2 行 str: .asciiz “Hello World”,代表分配了一定的内存空间,用来存储这个字符串,字符串最后有一个 ’\0’,str 是这个字符串的标签 (label)

第 4 行的 .text 代表程序从这里开始

第 5 行的 la,是一个扩展指令,在按 F3 快捷键之后进入 Execute 页面,可以看到这一个指令被转换成了 addi $4, $0, 0;同理,第 6 行和第 8 行的 li,也是一个扩展指令,这一个指令被转换成了 addiu $2, $0, 4addiu $2, $0, 10

第 7 行和第 9 行的 syscall,会根据当前的 $v0 寄存器的值,进行相应的操作,比如执行第 7 行 syscall 的时候,$v0=4,此时就会输出 $a0 寄存器指向的字符串,执行第 10 行 syscall 的时候,$v0=10,此时程序结束运行。有关 syscall 的说明,可以按快捷键 F1,在 MIPS -> Syscalls 页面中查到相应的用法。

这个 MIPS 汇编程序在运行的时候,先把 str 的地址 (0x0) 赋值给 $a0,然后令 $v0=4,执行syscall,输出 $a0 寄存器指向的字符串 ”Hello World”,直到遇到 ’\0’ 为止,最后令 $v0=10,执行 syscall,结束程序。

汇编语言与我们之前所学习的 C 语言还是有很大的区别,由于汇编语言的主体代码中并没有变量名、函数名、各种运算符号和语句块。我们仅能通过有限的 32 个寄存器以及众多的汇编指令和标签来实现我们之前所学的 C 语言所能够实现的功能。实际上 C 语言代码在经过编译后也会转换成汇编程序代码再进一步翻译成机器码,有兴趣的同学可以自行搜索相关资料进行学习,将来在操作系统和编译这两门课程中你们还将继续与这些汇编程序打交道。

如果认真阅读了上面的代码,想必已经体会到了汇编指令在可读性方面的巨大缺陷,所以请务必为自己编写的汇编程序代码加上注释,防止代码写完之后自己都看不懂。

一个简单的循环例子

下面以一个简单题目为例,说明在 MIPS 中如何使用循环。

题目内容:输入一个数 nn,输出 1+2+3+…+n1+2+3+…+n(保证 nn 和输出结果都在 int 的范围之内)

文本代码

.text
li  $v0,5
syscall                 # 输入一个整数,输入的数存到 $v0 中
move $s0, $v0           # 赋值,$s0 = $v0
li  $s1, 0              # $s1 用于存储累加的值,$s1 = 0
li  $t0, 1              # $t0 是循环变量

loop:
bgt $t0, $s0, loop_end  # 这里用了一个扩展指令 bgt,当 $t0 > $s0 的时候跳转到 loop_end
add $s1, $s1, $t0       # $s1 = $s1 + $t0
addi $t0, $t0, 1        # $t0 = $t0 + 1
j   loop                # 无条件跳转到 loop 标签

loop_end:
move $a0, $s1           # 赋值,$a0 = $s1
li  $v0, 1              # $v0 = 1,在 syscall 中会输出 $a0 的值
syscall         
li  $v0,10              # $v0 = 10
syscall                 # 结束程序          

在运行之前确保(Mars -> Settings -> Delayed branching)前面不要打钩,即不考虑延迟槽,遇到跳转指令立刻跳转,不执行跳转指令之后的指令。(在 MIPS 教程和 P2 部分,如果不对延迟槽加以说明,我们都不用考虑延迟槽)

相比于”第一个汇编程序“小节中的输出 ”Hello world“ 的程序,这里多了 syscall 输入和输出整数的用法,以及 movebgt 扩展指令,在 Execute 页面中对照 Basic 列和 Source 列的不同,就可以明白那条扩展指令是什么意思。利用这些扩展指令,可以简化我们编写汇编代码的工作量,并使得我们的汇编代码更加清晰。

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第1张图片

在 MIPS 中使用数组

下面以一个简单题目为例,说明在 MIPS 中如何使用数组。

题目内容:输入一个整数 nn(2 \leq n \leq 102≤n≤10),接下来 nn 行,每行一个整数(在 int 范围内),在输出的时候先输出 The numbers are:,然后把这 nn 个整数按照输入顺序输出,两个数字中间一个空格,行末可以有空格。

样例输入

4
2
3
1
4

样例输出

The numbers are: 2 3 1 4

文本代码

.data
array: .space 40           # 存储这些数需要用到数组,数组需要使用 10 * 4 = 40 字节
                           # 一个 int 整数需要占用 4 个字节,需要存储 10 个 int 整数
                           # 因此,array[0] 的地址为 0x00,array[1] 的地址为 0x04
                           # array[2] 的地址为 0x08,以此类推。

str:   .asciiz "The numbers are:\n"
space: .asciiz " "

.text
li $v0,5
syscall                    # 输入一个整数
move $s0, $v0              # $s0 is n
li $t0, 0                  # $t0 循环变量

loop_in:
beq $t0, $s0, loop_in_end  # $t0 == $s0 的时候跳出循环
li $v0, 5
syscall                    # 输入一个整数
sll $t1, $t0, 2            # $t1 = $t0 << 2,即 $t1 = $t0 * 4
sw $v0, array($t1)         # 把输入的数存入地址为 array + $t1 的内存中
addi $t0, $t0, 1           # $t0 = $t0 + 1
j loop_in                  # 跳转到 loop_in

loop_in_end:
la $a0, str
li $v0, 4
syscall                    # 输出提示信息
li $t0, 0
loop_out:
beq $t0, $s0, loop_out_end
sll $t1, $t0, 2            # $t1 = $t0 << 2,即 $t1 = $t0 * 4
lw $a0, array($t1)         # 把内存中地址为 array + $t1 的数取出到 $a0 中
li $v0, 1
syscall                    # 输出 $a0
la $a0, space
li $v0, 4
syscall                    # 输出一个空格
addi $t0, $t0, 1
j loop_out

loop_out_end:
li $v0, 10
syscall                    # 结束程序

这里使用了 lwsw 指令,对内存进行读写操作。lwsw 要求地址对齐到字(即地址必须为 4 的倍数),否则会产生异常。由于字符串的长度不确定,并且打印字符串($v0 为 4)的 syscall$a0 的值没有对齐要求,因此在一般情况下,会把字符串的声明放在最后。同学们可以尝试把 str 的声明挪到 array 声明的前面,如果运行程序时产生了异常,则可以尝试让 str 字符串增加或减少一些字符,直到内存指令不再出现异常。

在 MIPS 中使用二维数组

下面再以一个简单题目为例,说明在 MIPS 中如何使用二维数组。

题目内容:输入两个数 mm 和 nn(1 \leq m \leq 81≤m≤8,1 \leq n \leq 81≤n≤8),代表有一个 mm 行 nn 列的矩阵,接下来 mnmn* 行每行一个 int 范围内的整数,依次输入矩阵的元素,最后输出这个矩阵(每两个数字中间一个空格,行末可以有多余空格,最后可以有多余回车)

样例输入

2
3
1
2
3
4
5
6

样例输出

1 2 3 
4 5 6 

文本代码

.data
matrix: .space  256             # int matrix[8][8]   8*8*4 字节
                                # matrix[0][0] 的地址为 0x00,matrix[0][1] 的地址为 0x04,……
                                # matrix[1][0] 的地址为 0x20,matrix[1][1] 的地址为 0x24,……
                                # ……
str_enter:  .asciiz "\n"
str_space:  .asciiz " "

# 这里使用了宏,%i 为存储当前行数的寄存器,%j 为存储当前列数的寄存器
# 把 (%i * 8 + %j) * 4 存入 %ans 寄存器中
.macro  getindex(%ans, %i, %j)
    sll %ans, %i, 3             # %ans = %i * 8
    add %ans, %ans, %j          # %ans = %ans + %j
    sll %ans, %ans, 2           # %ans = %ans * 4
.end_macro

.text
li  $v0, 5
syscall
move $s0, $v0                   # 行数
li  $v0, 5
syscall
move $s1, $v0                   # 列数
# 这里使用了循环嵌套
li  $t0, 0                      # $t0 是一个循环变量

in_i:                           # 这是外层循环
beq $t0, $s0, in_i_end
li  $t1, 0                      # $t1 是另一个循环变量
in_j:                           # 这是内层循环
beq $t1, $s1, in_j_end
li  $v0, 5
syscall                         # 注意一下下面几行,在 Execute 页面中 Basic 列变成了什么
getindex($t2, $t0, $t1)         # 这里使用了宏,就不用写那么多行来算 ($t0 * 8 + $t1) * 4 了
sw  $v0, matrix($t2)            # matrix[$t0][$t1] = $v0
addi $t1, $t1, 1
j   in_j
in_j_end:
addi $t0, $t0, 1
j   in_i
in_i_end:
# 这里使用了循环嵌套,和输入的时候同理
li  $t0, 0

out_i:
beq $t0, $s0, out_i_end
li  $t1, 0
out_j:
beq $t1, $s1, out_j_end
getindex($t2, $t0, $t1)
lw  $a0, matrix($t2)            # $a0 = matrix[$t0][$t1]
li  $v0, 1
syscall
la  $a0, str_space
li  $v0, 4
syscall                         # 输出一个空格
addi $t1, $t1, 1
j   out_j
out_j_end:
la  $a0, str_enter
li  $v0, 4
syscall                         # 输出一个回车
addi $t0, $t0, 1
j   out_i

out_i_end:
li  $v0, 10
syscall

注意这里使用了宏,使得程序代码更为简洁,同学们可以在 Execute 页面中比较 Basic 和 Source 这两列的不同。

除此之外,同学们可以在 MARS 编辑器中按 F1 快捷键,阅读 MARS 的帮助文档,也可以在写程序的时候阅读一下代码补全的提示信息,从中也可以找到指令集文档中没有说明的指令,阅读这些帮助文档对同学们编写汇编程序有较大的帮助。MIPS 教程中只介绍了有限的几个指令,更多教程中没有介绍的指令还等待同学们的探索。

求 Fibonacci 数的程序

下面给出的是求前 12 个 Fibonacci 数的汇编程序。后面将以此为例,进行程序解析。

文本代码

.data
fibs: .space   48           # "array" of 12 words to contain fib values
size: .word  12             # size of "array"
space:.asciiz  " "          # space to insert between numbers
head: .asciiz  "The Fibonacci numbers are:\n"

.text
la   $t0, fibs              # load address of array
la   $t5, size              # load address of size variable
lw   $t5, 0($t5)            # load array size
li   $t2, 1                 # 1 is first and second Fib. number
sw   $t2, 0($t0)            # F[0] = 1
sw   $t2, 4($t0)            # F[1] = F[0] = 1
addi $t1, $t5, -2           # Counter for loop, will execute (size-2) times

loop:
lw   $t3, 0($t0)            # Get value from array F[n]
lw   $t4, 4($t0)            # Get value from array F[n+1]
add  $t2, $t3, $t4          # $t2 = F[n] + F[n+1]
sw   $t2, 8($t0)            # Store F[n+2] = F[n] + F[n+1] in array
addi $t0, $t0, 4            # increment address of Fib. number source
addi $t1, $t1, -1           # decrement loop counter
bgtz $t1, loop              # repeat if not finished yet.
la   $a0, fibs              # first argument for print (array)
add  $a1, $zero, $t5        # second argument for print (size)
jal  print                  # call print routine.
li   $v0, 10                # system call for exit
syscall                     # we are out of here.

print:
add  $t0, $zero, $a0        # starting address of array
add  $t1, $zero, $a1        # initialize loop counter to array size
la   $a0, head              # load address of print heading
li   $v0, 4                 # specify Print String service
syscall                     # print heading

out:
lw   $a0, 0($t0)            # load fibonacci number for syscall
li   $v0, 1                 # specify Print Integer service
syscall                     # print fibonacci number
la   $a0, space             # load address of spacer for syscall
li   $v0, 4                 # specify Print String service
syscall                     # output string
addi $t0, $t0, 4            # increment address
addi $t1, $t1, -1           # decrement loop counter
bgtz $t1, out               # repeat if not finished
jr   $ra                    # return

1~7 行:变量声明与分配(伪指令)

8~14 行:初始化与寄存器分配

16~28 行:循环计算并保存 Fibonacci 数

30~47 行:输出提示语句和 Fibonacci 数

变量声明与定义

这部分使用到的就是在 MIPS 架构指令集一节中初步接触到的伪指令,在该程序中这些伪指令主要用途是标识数据段和代码段的位置,并为声明的数据分配空间。

.data
fibs: .space   48           # "array" of 12 words to contain fib values
size: .word  12             # size of "array"
space:.asciiz  " "          # space to insert between numbers
head: .asciiz  "The Fibonacci numbers are:\n"

.text

在 MARS 中的 Help 文档 MIPS -> Directives 一栏,对所有的伪指令都做了简要的介绍,如果大家想要进一步了解各个伪指令的具体用法,请自行上网搜索。接下来仅具体介绍程序中出现的这几条伪指令。(格式中 [XXX] 请用对应的数据替换)

1) .data

格式:.data [address]

说明:

  • 定义程序的数据段,初始地址为 address,若无 address 参数,初始地址为设置的默认地址。
  • 需要用伪指令声明的程序变量需要紧跟着该指令。比如该程序中的 fibs, size, space, head 这四个变量。

\2) .text

格式:.text [address]

说明:

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

\3) .space

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

说明:

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

\4) .word

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

说明:

  • 在内存数据段中以字为单位连续存储数据 data1, data2,… (也就是将 datax 写入对应的 1 个字的空间,注意 .word 和 .space 的区别)
  • 这段数据的初始地址保存在标签 name 中。计算方式与上面相同。
  • 在本例中,把需要计算的 Fibonacci 数的个数保存在了内存数据段,地址标签为 size。

\5) .asciiz

格式:[name]: . asciiz “[content]”

说明:

  • 以字节为单位存储字符串,末尾以 NULL 结尾。
  • 这个字符串在内存数据区的初始地址保存在标签 name 中。
  • 注意 .asciiz.ascii 这两条伪指令的区别。
  • .asciiz 由于是按字节存储,可能会导致之后分配的空间首地址无法字对齐的情况发生,请大家自行思考解决方法。
  • 本例中,声明了两个字符串,一个是输出时需要用到的空格符,一个是输出语句,地址标签分别为 space 和 head。

写入 1~8 行后点击扳手图标,就可以看到数据段空间分配的结果了。

仔细观察右边的 labels 部分,可以发现我们申请的四段空间是从 0x00000000 也就是数据段的首地址紧挨着分配得到的。

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第2张图片

把内存查看栏的 Hexadecimal Values 选项去掉就可以看到 fib 段总共分配了 48 字节(蓝色),全部用 0 填充,而 size 所对应的一个字的空间(黄色)写入了 12。剩下用红色框标注的部分是前面声明的字符串

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第3张图片

再把内存查看栏的 ASCII 勾选,可以查看其中的字符串部分。将内存中字符串的分布与 head 段声明的字符串做个对比,大家是否心中有一些疑惑,本来好好的字符串怎么变成了这副模样,联系计算机存储数据方式解释一下为什么会出现这种现象。这个例子也可以帮助大家更好地理解计算机储存数据的模式。

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第4张图片

宏的使用

什么时候要用到宏?

在我们的汇编代码中,可能会出现很多需要停止程序运行的地方,我们可以使用如下代码:

li  $v0, 10
syscall

并把这段代码复制粘贴到许多地方。但是这样的代码多了,会导致代码过于冗长,复用性差,不利于阅读。

同学们在编写汇编程序时,尤其时有关矩阵的程序,可能会频繁地使用具有较高相似度的代码段,尤其是在通过行数和列数获取矩阵中的元素的时候,需要频繁地计算(行数 * 最大列数 + 列数) * 4这样的数,代码如下(假设矩阵的最大列数为 8,行数存在 $t0 中,列数存在 $t1 中,最终结果计算到 $t2 中):

sll $t2, $t0, 3
add $t2, $t2, $t1
sll $t2, $t2, 2

在编写程序的时候,我们可能会把这一小段代码复制多次并粘贴到不同的地方,对于矩阵乘法而言可能还要改一下寄存器的编号,稍微一个不注意就会产生难以发现的 bug。因此,我们应当想办法杜绝这种问题的发生,提高代码的复用性。

宏 (macro) 就是用来解决这个问题的方案,我们可以使用宏来提高代码的复用性,让这种问题得以解决。

宏的用法

宏分为两种,不带参数的宏和带参数的宏。

不带参数的宏,定义的方式如下:

.macro macro_name
# 代码段
.end_macro

第一个例子适合用不带参数的宏解决,我们可以定义这样一个宏:

.macro done
li  $v0, 10
syscall
.end_macro

此时,在需要程序停止运行的地方,使用 done 语句,就可以让程序在那里退出。汇编器会把所有的 macro_name 语句替换成代码段;在第一个例子中,汇编器把所有的 done 语句替换成

li  $v0, 10
syscall

这样就实现了代码复用。

带参数的宏,定义的方式如下:

.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

其中 %i 代表行数,%j 代表列数,%ans 就是计算出来的结果 ((%i * 8 + %j ) * 4)((%i∗8+%j)∗4)。使用 getindex($t2, $t0, $t1) 来调用这个宏,汇编器会用这段代码替换它,同时 %ans 被替换成 $t2,%i 被替换成 $t0,%j 被替换成 $t1,因此最终会被替换成

sll $t2, $t0, 3
add $t2, $t2, $t1
sll $t2, $t2, 2

在矩阵乘法中,只需要替换调用宏的语句,问题就会被轻松解决,同时代码的复用性得到了提高,代码也更容易被人读懂。

在汇编程序中,还有一种和C语言中 #define 类似的宏定义,一般用于常量的定义上,那就是 .eqv.eqv 用法如下:

.eqv EQV_NAME string

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

在 P8 中,可能需要用汇编语言对数码管进行一些控制,控制数码管显示数字,每一个数字都有自己的编码,因此可以用 .eqv 对这些编码进行定义,例如:

.eqv    TUBECHAR_0  0x7E
.eqv    TUBECHAR_1  0x30
.eqv    TUBECHAR_2  0x6D

使得代码更加清晰,并且避免了代码中出现各种意义不明的数字。

伪指令测试

伪指令测试1

以我们求F的程序为例,假如要改成输出前20个斐波那契数,需要修改哪些伪指令?(多选)

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第5张图片

A. .data

B. .space

C. .word

D. .asciiz

E. .text

答案:BC

伪指令测试2

在我们给出的Fibonacci程序中,对于字符串的存储,我们使用的是.asciiz。 现在我们尝试使用.ascii这条伪指令,代码如下。

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第6张图片

这段代码执行完会输出什么?

答案:hello world

伪指令测试3

下列程序执行完之后,$t0寄存器的值是多少?(用16进制表示:0x00000000,如有字母,请使用小写)

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第7张图片
答案:0x04030201

伪指令测试——asciiz

我们使用.asciiz与.ascii伪指令来进行输出,代码如下

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第8张图片

输出结果为:

A. one,two,three,four!

B. one,two,two,three,three,four!

C. one,two,two,three,four!four!

D. two,four!

答案:C

分配寄存器

这部分对应着代码中的 8~14 行,主要是做一些初始化工作,并为寄存器分配功能。

la   $t0, fibs        # load address of array
la   $t5, size        # load address of size variable
lw   $t5, 0($t5)      # load array size
li   $t2, 1           # 1 is first and second Fib. number
sw   $t2, 0($t0)      # F[0] = 1
sw   $t2, 4($t0)      # F[1] = F[0] = 1
addi $t1, $t5, -2     # Counter for loop, will execute (size-2) times

至于 MIPS 汇编中常用的 32 个通用寄存器都有什么功能,大家可以另外查阅资料,这里就不一一例举。

  • 前两行所使用的 la 指令是 Mars 在标准指令集基础上扩充的一条指令,用于获取标签所指向的地址。比如 la $t0, fibs 就将 fibs 这个标签对应的地址 0x00000000 存入了 $t0 寄存器,用于之后向里面存入 Fibonacci 数。
  • la $t5, sizelw $t5, 0($t5) 两行使用 $t5 寄存器保存所需要计算的 Fibonacci 数的总个数。首先用 la 将 size 的地址载入 $t5 中,再用 lw 指令从内存中的 $t5 所保存的地址 (因为偏移量为 0) 开始读取一个字 (也就是 12) 写入 $t5 中。
  • li 指令也是一条扩展指令,可以直接往寄存器中写入一个 32 位的立即数,在这里向 $t2 这个寄存器中写入了 1,也就是斐波拉契数列的前两个元素的值,11、12 行是把前两个已知的 Fibonacci 数存入到 fibs 数组空间中,也就是 $t0 寄存器中所保存的地址对应的空间。
  • 最后一行是使用 $t1 寄存器保存剩余需要计算的 Fibonacci 数的个数,之后每计算出一个 Fibonacci 数,$t1 就会自减。当 $t1 减为 0 时,代表计算任务已经完成。

循环运算

这部分功能比较单一,就是循环计算出 Fibonacci 数并将其保存至 fibs 数组中,对应着 loop 标签下的 16~28 行。

loop:
lw   $t3, 0($t0)      # Get value from array F[n]
lw   $t4, 4($t0)      # Get value from array F[n+1]
add  $t2, $t3, $t4    # $t2 = F[n] + F[n+1]
sw   $t2, 8($t0)      # Store F[n+2] = F[n] + F[n+1] in array
addi $t0, $t0, 4      # increment address of Fib. number source
addi $t1, $t1, -1     # decrement loop counter
bgtz $t1, loop        # repeat if not finished yet.
la   $a0, fibs        # first argument for print (array)
add  $a1, $zero, $t5  # second argument for print (size)
jal  print            # call print routine.
li   $v0, 10          # system call for exit
syscall               # we are out of here.
  • 前两个 sw 指令是从 fibs 数组中提取前两个 Fibonacci 数 F(n-1)F(n−1) 和 F(n-2)F(n−2),add $t2, $t3, $t4 根据公式 F(n)=F(n-1)+F(n-2)F(n)=F(n−1)+F(n−2) 计算出新的 Fibonacci 数,然后通过 sw $t2, 8($t0) 存入到数组空间中 (注意这三条 lw 指令的偏移量,可以联想 C 语言中数组指针的用法)
  • 接下来两行 addi $t0, $t0, 4addi $t1, $t1, -1 是修改 $t0$t1 的值用于下次循环计算。(对 $t0 的操作可以联想 C 语言中指针的自增操作)
  • bgtz 指令的含义是当寄存器中的值大于 0 时跳转到标签所在的位置,也就是说当 $$t1>0 $也就是运算还没有结束时,每次运行到bgtz 指令都会跳转到 loop 标签处计算下个 Fibonacci 数。
  • jal print 表示跳转到 print 处开始输出,并将下一行的地址保存在 $ra 寄存器中,使得在调用完成 print 函数后还可以返回到下一行执行结束程序的部分代码。
  • 这里的最后两行是 MARS 中结束程序的标准流程,更多的 syscall 用法请自行阅读 MARS 帮助文档,下一小节将更详细地介绍系统调用的相关注意事项。

C代码翻译

C代码翻译

与以下C代码的语义完全等价的汇编是什么?___________________(在空格中填入对应选项的大写字母,不要包含空格等其他任何字符) (提示:bgtz是大于0则跳转,blez是小于等于0则跳转,div会将除法结果放置于hi和lo两个特殊寄存器中,mfhi会将hi寄存器的结果存入指定通用寄存器中) 假设a被存放在a0寄存器中,b被存放在a1寄存器中,v0存放返回值,ra存放了返回地址。

注意:选项中出现的j指令是gcc给出的汇编的用法(请不要模仿),等价于我们的jr。

C代码:
北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第9张图片

A.

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第10张图片

B.

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第11张图片

C.

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第12张图片

D.

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第13张图片

答案:B

系统调用

本部分的功能就是输出提示语句和 12 个 Fibonacci 数,为了实现这些功能,这里使用了 MIPS 汇编中的系统调用。这些系统调用是专门用来实现输入 / 输出,文件读取和终止运行等功能的一些指令,在 Mars 的 Help 文档中同样有详尽的介绍与使用说明。

这些系统调用的模式大都相同,一般都是$a0$v0 寄存器赋值,执行 syscall 指令,然后汇编器就会根据 $v0 寄存器中的值进行不同的操作。下面会对程序中出现的进行解析。

print:
add  $t0, $zero, $a0  # starting address of array
add  $t1, $zero, $a1  # initialize loop counter to array size
la   $a0, head        # load address of print heading
li   $v0, 4           # specify Print String service
syscall               # print heading

out:  
lw   $a0, 0($t0)      # load fibonacci number for syscall
li   $v0, 1           # specify Print Integer service
syscall               # print fibonacci number
la   $a0, space       # load address of spacer for syscall
li   $v0, 4           # specify Print String service
syscall               # output string
addi $t0, $t0, 4      # increment address
addi $t1, $t1, -1     # decrement loop counter
bgtz $t1, out         # repeat if not finished
jr   $ra              # return
  1. 字符串输出 代码:

    la $a0, addr
    li $v0, 4
    syscall
    

    说明:

    • 首先把要输出的字符串在内存中的首地址赋给 $a0 寄存器,然后汇编器就会根据 $a0 中的地址将字符串输出。
    • 在内存中存储的字符串是以 NULL(’\0’) 作为结束符,输出时遇到这个结束符就会停止。
  2. 整数输出

    代码:

    li $v0, 1
    syscall
    

    说明:

    • 这个系统调用的功能就是把 $a0 寄存器中的数据以整数的形式输出。
  3. 结束程序

    格式:

    li $v0, 10
    syscall
    

    说明:

    • 结束程序

指令解析:

  • 前两个 add 指令表示从 $a0, $a1 计算出需要输出的数组的首地址和大小。

  • la $a0, head 表示获取需要输出的提示信息字符串的首地址。

    la   $a0, head        # load address of print heading
    li   $v0, 4           # specify Print String service
    syscall               # print heading
    

    这三行使用字符串输出系统调用将字符串输出。

  • lw $a0, 0($t0) 表示从内存中获取一个斐波拉契数,接下来的两行为使用系统调用将其输出。

  • mips la $a0, space # load address of spacer for syscall li $v0, 4 # specify Print String service syscall # output string

    这三行表示输出一个空格字符。

  • 接下来两行 addi 指令是为下次循环做准备,利用 bgtz 指令判断是否输出已经结束,如果没有结束跳回到 out 标签处进行下次输出。

  • jr $ra 表示完成输出返回到原代码 27 行处结束程序,即

    li    $v0, 10
    syscall
    

系统调用测试

系统调用测试1

在使用系统调用输出字符串时,汇编器是如何确定要输出的字符串的?

代码如下:

在这里插入图片描述

A. 将$a0寄存器中存储的数据作为要输出的字符串

B. 将$v0寄存器中存储的数据作为要输出的字符串

C. 以$a0寄存器中存储的地址作为要输出字符串在内存中的首地址,根据这个首地址从内存中以字节为单位读取数据,一直读到null也就是“\0”结束,这些读取的数据就是要输出的字符串。

D. 以$v0寄存器中存储的地址作为要输出字符串在内存中的首地址,根据这个首地址从内存中以字节为单位读取数据,一直读到null也就是“\0”结束,这些读取的数据就是要输出的字符串。

答案:C

系统调用测试2

下列系统调用的执行结果是:

li $v0,10
syscall

A. 输出数字10

B. 输出字符串”10”

C. 输出ASCII码10对应的字符

D. 退出程序

答案:D

系统调用测试3

单以下列代码而言,我们输入的字符串保存在内存中的首地址为多少?(数据段的首地址为0x00000000,请查看MIPS帮助文档)

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第14张图片

A. 0x00000000

B. 0x00000010

C. 0x00001000

D. 没有正确答案

答案:C

系统调用测试4

单以下列代码而言,我们最多能够读取多少个字符?

北航计算机组成原理课程设计-2021秋 PreProject-MIPS-MIPS 汇编程序解析_第15张图片

A. 8

B. 10

C. 15

D. 16

答案:C

你可能感兴趣的:(计算机组成,buaa,mips)