写在前面

  本文是根据"MIPS Assembly Language Programming CS50 Discussion and Project Book. Daniel J. Ellard"总结的。其中有大量的翻译文体以及个人的看法想法,当然,内容没有书上那么详尽。

  这一章节会涉及MIPS的变量的声明、数据的输入输出、取地址、分支跳转语句(用以实行循环、判断等),基本上对应于任何一门高级语言的最基本操作。

简介

机器语言

  正如我们在前一章中所看到的,计算机指令可以表示为位序列。一般来说,这是程序可能的最低表示级别——每条指令都相当于CPU的单个不可分割的动作。这种表示被称为机器语言,因为它是机器可以直接“理解”的唯一形式

汇编语言

  一个高一层的表示(而且对人类来说更容易使用)称为汇编语言。汇编语言与机器语言有着非常密切的关系,通常有一种直接的方法将汇编语言编写的程序翻译成机器语言。(此算法通常由一个名为汇编程序(assembler)的程序实现。)

  因为机器和汇编语言关系很近,每个不同的机器体系结构通常都有自己的汇编语言(事实上,每个体系结构可能有几个),并且每个都是唯一的。用 assember(而不是机器语言)编程的优势在于,汇编语言更容易让人阅读和理解。

开始编程

在这一章节中不会正式介绍所有指令,只是为了熟悉汇编和部分算法

1. 初步了解汇编

涉及到的新指令/标签

#

/**/

li

add

main

addi

syscall

正文

下面由一个add.asm为例开始了解汇编(语法结构等等),且暂时不严格要求指令之间的不同之处(后续会介绍具有类似功能的指令也会有一些差异)

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 补全程序

  上面两条指令执行我们想要的计算,但它们并不构成一个完整的程序。与C类似,汇编语言程序必须包含一些附加信息,这些信息告诉汇编程序的开始和结束位置。此信息的确切形式因汇编程序而异(请注意,对于给定的体系结构,可能有多个汇编程序,而对于MIPS体系结构,可能有多个汇编程序)。本教程假设 SPIM 被用作汇编和运行时环境。

(1)Label和main

  标签(Label)是内存中地址的符号名称。在MIPS程序集中,标签是符号名(遵循与C符号名相同的约定),后跟冒号(colon)。

  使用不同形式的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

  注意,SPIM 汇编程序不允许将指令名用作标签。因此,不允许使用名为add的标签,因为存在同名指令。(当然,由于指令名都很短而且相当通用(li, lw等),因此它们通常不会生成非常描述性的标签名。)

(2)syscall调用

  程序的结尾与C类似,在C中可以调用exit函数来停止程序的执行,停止MIPS程序的一种方法是使用类似于在C中调用exit的方法。

  但是,与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

syscall 10

正文

  算法中我们还不知道该怎么做的部分是从用户那里读取数字,并打印出,这两个操作都可以通过系统调用完成。

2.1 读取和打印整数

  在C中,我们如果要打印输出一个数字:printf("%d",x);, 但是那么在 mips 是没有这样的一个print指令的,唯一一个可以用于输出的指令是syscall,在特定条件下它是可以起到输出的作用的。它的使用要配合其他的寄存器($v0),$v0中储存不同的值在调用syscall时会有不同的作用

$v0 = 1, syscall -> print_in (output)

$v0 = 5, syscall -> read_in (into $v0, input)

$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

syscall 4

正文

$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\n" # 这里\n依旧有效
.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\n”并获得相同的准确输出。尝试各种方法的输入输出可以很快地熟悉这门语言。

# Method 1
.data
hello_msg: .ascii "Hello"
.ascii " "
.ascii "World"
.ascii "\n"
.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

b endif, endif 为标签名

正文

我们将编写的下一个程序将探讨在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

分支指令之一是 bgtbgt 指令有三个参数。前两个是数字,最后一个是标签。

如果第一个数字大于第二个数字,则应在标签处继续执行;

否则将在下一条指令处继续执行。

另一方面,b 指令只是分支到给定的标签。(无条件跳转)

占位符所在位置的程序:

# 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)可以通过对下一章介绍的指令集的内容自行研究。

最新文章

  1. caffe代码调试小结
  2. tomcat 常用优化配置
  3. 项目管理知识框架PMBOK(文字版)
  4. 如何处理json字符转换为字典
  5. 利用ajax.dll类库文件实现无刷新
  6. mysql有回滚,php没有回滚的说法
  7. Loadrunner监控Centos
  8. HTTP返回码总结 (zz)
  9. 4071: [Apio2015]巴邻旁之桥
  10. IOS开发效率之为Xcode添加常用的代码片段
  11. mybatis参数查询
  12. Git 在小团队中的管理流程(转)
  13. 天梯赛-L1-018. 大笨钟
  14. testng的使用
  15. [转载]ISO 8601规则
  16. scrapy之持久化存储
  17. Confluence 6 log4j 日志级别
  18. 2955 ACM 杭电 抢银行 01背包 乘法
  19. Pandas:让你像写SQL一样做数据分析
  20. 第8章 用SQL语句操作数据

热门文章

  1. spring-boot-route(六)整合JApiDocs生成接口文档
  2. 笔记本键盘按U键却变成了4
  3. 如何获取value值,获取属性值,设置属性值,
  4. 《流畅的Python》第二部分 数据结构 【序列构成的数组】【字典和集合】【文本和字节序列】
  5. LiteOS-任务篇-源码分析-任务调度函数
  6. rxjs入门7之其它操作符复习
  7. rxjs入门3之项目中ajax函数封装
  8. Elasticsearch(4):映射
  9. 多测师讲解python _string_高级讲师肖sir
  10. 如何设计一个牛逼的API接口