写在前面
本文是根据"MIPS Assembly Language Programming CS50 Discussion and Project Book. Daniel J. Ellard"总结的。其中有大量的翻译文体以及个人的看法想法,当然,内容没有书上那么详尽。
这一章节会涉及MIPS的变量的声明、数据的输入输出、取地址、分支跳转语句(用以实行循环、判断等),基本上对应于任何一门高级语言的最基本操作。
简介
机器语言
汇编语言
因为机器和汇编语言关系很近,每个不同的机器体系结构通常都有自己的汇编语言(事实上,每个体系结构可能有几个),并且每个都是唯一的。用 assember
(而不是机器语言)编程的优势在于,汇编语言更容易让人阅读和理解。
开始编程
在这一章节中不会正式介绍所有指令,只是为了熟悉汇编和部分算法
1. 初步了解汇编
涉及到的新指令/标签
#
/**/
li
add
main
addi
syscall
正文
1.1 注释
在开始编写程序的可执行语句之前,我们需要编写一个描述程序应该做什么的注释。
#和/**/都可以写注释
1.2 寻找正确的指令
由于MIPS体系结构的指令相对较少,很快你就会记住你需要的所有指令,但是随着你开始,你需要花一些时间浏览指令列表,寻找那些你可以用来做你想做的事情的指令。这些可以在第二部分找到。
- 加法运算需要三个操作数(operant)
li
(load immediate value):将32位常量(32-bit constant)放入指定寄存器中
# add.asm # begin of add.asm li $t1,1 #load 1 into register $t1 add $t0,$t1,2 #$t0 = $t1 + 2
1.3 补全程序
上面
(1)Label和main
使用不同形式的Label可以很好地将指令分类,在执行程序之前,我们需要告诉汇编程序从哪里开始。在SPIM
中,程序的执行从带有标签main的位置开始。
内存中的一个位置可能有多个标签。因此,为了告诉 SPIM
应该将label main分配给程序的第一条指令。
# add.asm # begin of add.asm main: li $t1,1 #load 1 into register $t1 add $t0,$t1,2 #$t0 = $t1 + 2 # end of add.asm
(2)syscall调用
程序的结尾与C类似,在C中可以调用exit函数来停止程序的执行,停止MIPS程序的一种方法是使用类似于在C中调用exit的方法。
使用 syscall
告诉 SPIM
它应该停止执行的程序,以及执行许多其他有用的事情的方法。syscall
指令暂停程序的执行,并将控制权传输到操作系统。然后,操作系统查看寄存器$v0的内容,以确定程序要求它执行的操作。
这通过在执行syscall指令之前将10(exit syscall的编号)放入 $v0
来完成的
# add.asm # begin of add.asm main: li $t1,1 #load 1 into register $t1 add $t0,$t1,2 #$t0 = $t1 + 2 # exit li $v0, 10 syscall # end of add.asm
2. 了解syscall
新指令/标签
.data
.text
move
syscall 1
syscall 5
正文
算法中我们还不知道该怎么做的部分是从用户那里读取数字,并打印出,这两个操作都可以通过系统调用完成。
2.1 读取和打印整数
在C中,我们如果要打印输出一个数字:printf("%d",x);
, 但是那么在 mips
是没有这样的一个print指令的,唯一一个可以用于输出的指令是syscall,在特定条件下它是可以起到输出的作用的。它的使用要配合其他的寄存器($v0),$v0中储存不同的值在调用syscall时会有不同的作用
$v0 = 1, syscall -> print_in (output)
$v0 = 10, syscall -> exit
(1)syscall 1
我们现在知道怎样将syscall的作用定义为打印输出(即完成了%d部分),但是我们要打印输出哪个寄存器的内容呢(即找出需要被打印的x)?在mips中syscall只能打印$a0中的内容(无法指定被打印的寄存器),为此,我们需要将x(位于某个寄存器)的值存储至$a0寄存器,供syscall调用和输出。MIPS中有一个move指令,它将一个寄存器的内容复制到另一个寄存器中。
# print_in: 1 addi $t0, $zero, 1 move $a0,$t0 #不推荐使用li给$a0赋值,那样就不是打印$t0的内容了,没有意义 li $v0,1 # 将1存储至$v0中,提示syscall的作用为打印整数 syscall
(2)syscall 5
# read and print Integer # $t0 -used to hold the first number # $t1 -used to hold the second number # $t2 -used to hold the sum of the $t1 and $t2 # $v0 -syscall paramenter and return value # $a0 -syscall parameter # start main: # get the first number from user, put into $to li $v0,5 #load syscall read_int(5 represents this) into $v0 syscall #make the syscall # 这之后在控制台输入一个整数并回车,用户输入的数据将被存储在$v0中 move $t0,$v0 #move the number(5) read into $t0 # get the second number from user, put into $t1 li $v0,5 #load syscall read_int(5 represents this) into $v0 syscall #make the syscall move $t1,$v0 #move the number(5) read into $t1 # sum, put into $t2 add $t2,$t0,$t1 # print out $t2 move $a0,$t2 # move the number to print into $a0 li $v0,1 # load syscall print_int into $v0 syscall # make the syscall # exit li $v0,10 # syscall code 10 is for exit syscall # end
2.2 hello world
新指令/标签
la
.asciiz
.ascii
.byte
$v0 = 4, syscall -> print_string (output)
我们需要将被打印字符串 "hello world" 的地址放入$a0中,将$v0的值设为4,再进行syscall的调用输出即可。接下来我们会学习如何定义一个(字符串)变量并使用它。
像是在C语言中一样,MIPS中也可以对地址进行调用,而这里我们定义了变量后如果想要对这个变量调用可以通过访问其变量名存储的地址来实现。指令la可以获取并存储变量地址。
字符串“Hello World”不应该是程序的可执行部分(包含要执行的所有指令)的一部分,该部分称为程序的文本段 (text segment
)。相反,字符串应该是程序使用的数据的一部分,按照惯例,它存储在数据段中。MIPS汇编程序允许程序员通过使用几个汇编程序指令来指定在程序中存储每个项的段。
为了在数据段中放置一些东西,我们需要做的就是在定义它之前放置一个· .data
。.data
指令和下一个 .text
我们还需要知道如何定义和为空结束的字符串分配空间。在MIPS汇编程序中,这可以使用 .asciiz
(ASCII,以零结尾的字符串)指令来完成。对于以非空结尾的字符串,可以使用 .ascii
指令(directive)
.data hello_msg: .asciiz "hello world " # 这里 依旧有效 .text main: la $a0, hello_msg # load the addr of hello_msg into register $a0 li $v0,4 syscall # exit li $vo,10 syscall
数据段中的数据被组装到相邻的位置。因此,有很多方法可以声明字符串“Hello World ”并获得相同的准确输出。尝试各种方法的输入输出可以很快地熟悉这门语言。
# Method 1 .data hello_msg: .ascii "Hello" .ascii " " .ascii "World" .ascii " " .byte 0 # a 0 byte # Method 2 .data hello_msg: .byte 0x48 # hex for ASCII "H" .byte 0x65 # hex for ASCII "e" .byte 0x6C .byte 0x6C .byte 0x6F # ... # and so on .byte 0xA # hex for ASCII newline .byte 0x0 # hex for ASCII NULL
2.3 条件执行
新指令/标签
bgt
正文
我们将编写的下一个程序将探讨在MIPS汇编语言中实现条件执行的问题。
下面我们先用占位符(placeholder comment)代表这个操作表示出该算法:
# start .text main: # Get first number from user, put into $t0. syscall # make the syscall. move $t0, $v0 # move the number read into $t0. # Get second number from user, put into $t1. li $v0, 5 # load syscall read_int into $v0. syscall # make the syscall. move $t1, $v0 # move the number read into $t1. # (placeholder comment) # put the larger of $t0 and $t1 into $t2. # Print out $t2. move $a0, $t2 # move the number to print into $a0. li $v0, 1 # load syscall print_int into $v0. syscall # make the syscall. # exit li $v0, 10 # syscall code 10 is for exit. syscall # make the syscall. # end
分支指令之一是 bgt
。bgt
指令有三个参数。前两个是数字,最后一个是标签。
如果第一个数字大于第二个数字,则应在标签处继续执行;
# bgt, branch greater than bgt $t0, $t1, t0_bigger # if &to>$t1, branch to t0_bigger move $t2, $t1 # else copy $t1 into $t2 b endif # and then branch to endif # b endif:一次判断赋值后直接跳转至结尾防止重复赋值 t0_bigger: move $t2, $t0 # copy $t0 into $t2 endif:
完整的程序:
# start .text main: # the first number from user/(console) li $v0,5 syscall move $t0,$v0 # the second number from user/(console) li $v0,5 #由于第一个数的获取,此时的$v0可能不是5,需要重新赋值 syscall move $t1,$v0 # judge bgt $t0,$t1,int_greater # if move $a0,$t1 # else li $v0,1 # set $v0 syscall # print(make syscall) b endif # branch to end int_greater: move $a0,$t0 # ifSo-then li $v0,1 # set $v0 syscall # print(make syscall) endif: # exit li $v0,10 # set $v0 syscall # exit(make syscall) # end
至此,本次内容基本结束,一些更为复杂的数据结构(如数组的声明)和算法(如对应于C语言中的switch)可以通过对下一章介绍的指令集的内容自行研究。