基础篇
基础篇
一次简单的 if 条件判断
在 C 语言中,常用 if 结构,在 MIPS 中如何实现呢?
考虑下面的代码:
if (a > 5) {
// do A;
}
// do B;
我们不得不提一个叫做 ble 的指令,如下图,其含义是若 $t1 寄存器的值小于等于 5,则跳转到 label 标签处;否则,执行 ble 的下一条指令。
ble $t1, 5, label
所以我们需要给源程序打上标签:
if (a > 5) {
// do A;
}
// label:
// do B;
经过这样打标签之后,程序的逻辑就变成了:
如果 a 的值小于等于 5,就跳转到 label 的位置,执行 B 部分
否则,不跳转,执行 A 部分
但是要注意一点,如果没有遇到跳转指令 或者 程序终止的 syscall 时,会顺序执行后面的代码。
也就意味着,在这里如果 A 部分执行完,会自然顺序执行到 B 部分。
运行逻辑与 C 语言示例代码一致。
转换成 MIPS 为:
# 假设 s0 当中存放着 a 的值
ble $s0, 5, label
# A
label:
# B
为了便于理解,我们可以看下面更具体的例子:
if (a > 5) {
printf("5");
}
printf("6");
转换成 MIPS 代码如下:
li $t1, 5
li $v0, 1 # 确立 syscall 为输出 a0 的值
ble $t1, 5, label
li $a0, 5 # 若 t1 不是小于等于 5,继续执行该语句
syscall # 输出 5
lable: # 若 t1 小于等于 5,跳转至此处
li $a0, 6
syscall #输出 6
相应的,还有一些等于、大于等于、大于、小于这些指令,使用方法与 ble 完全一致,可以在 MIPS 指令手册中查找,这里不再一一讲解。
条件判断通用实现
if - else
那有同学要问了 if 会写了,那 if - else 怎么办呢?
其实也很简单,我在下面给出一个 if - else 通用的实现方法。
考虑下面的例子:
if (condition) {
// do A;
} else {
// do B;
}
// do C;
同样的,我们给程序打上标签:
if (condition) {
// do A;
} else {
// labelB:
// do B;
}
// labelC:
// do C;
经过这样打标签之后,程序的逻辑就变成了:
如果 a 的值不满足 condition,就跳转到 labelB 的位置,执行 B 部分
执行 B 部分之后,程序自然来到 C 部分执行
否则,不跳转,执行 A 部分
执行完 A 部分之后,直接跳转到 labelC 的位置,执行 C 部分
运行逻辑非常正确。
转换成 MIPS 为:
# 判断是否满足 condition,不满足跳到 labelB
# A
j labelC
labelB:
# B
labelC:
# C
if - else if - else
考虑下面的例子:
if (conditionA) {
// do A;
} else if (conditionB) {
// do B;
} else {
// do C;
}
// do D;
其实与 if - else 是完全一致的,只需要稍加修改:
if (conditionA) {
// do A;
} else {
if (conditionB) {
// do B;
} else {
// do C;
}
}
// do D;
无非是在 else 当中又进行了一次嵌套。
一个 if 内有多个 condition
那又有同学问了,一个 if 里面有好几个 condition 进行与或非操作呢?
那我们分别来看一下。
非这里不必多说。不管是调整 condition 本身,还是使用相反的 MIPS 指令,都可以轻松解决。
与
if (conditionA && conditionB) {
// do A;
}
// label:
// do B;也很简单,不就是这俩条件都得满足才可以,改写一下:
if (conditionA) {
if (conditionB) {
// do A;
} else {
// do B;
}
}
// do B;相信看了上面的内容,这个地方也不难处理。
或
if (conditionA || condition B) {
// do A;
}
// label:
// do B;同样改写代码:
if (conditionA) {
// do A;
} else if (conditionB) {
// do A;
}
// do B;当然,这里并不是说要把 A 部分的代码写两遍,只需要运用跳转,跳转到相同的地方就可以。
循环
只要明白了前面所说的条件判断的实现方法,循环超级简单,可用判断语句实现。
while 语句
我们首先介绍无条件跳转 j(jump)指令。
j label
上面这条指令表示,不管程序当前是什么状态,只要运行到这条指令,就直接跳转到 label 标签所在的地方继续运行。
考虑下面的例子:
while (a < 5) {
// do A;
}
那么上面这段代码是不是就可以转换成:
// label:
if (a < 5) {
// do A;
}
j label
// next:
那么前面已经讲过条件判断的通用实现,这里是不是也就解决了呢?
loop:
bge $s0, 5, next
# do A
j loop
next:
for 语句
考虑下面的例子:
for (A; B; C) {
// do D;
}
对于上面的 for 语句,相信大家都有能力将它改成 while 语句(确信
A;
while (B) {
D;
C;
}
那就直接套用前面说的 while 就好了。
数组
MIPS 汇编语言只给了我们 32 个寄存器,要是想使用数组(整数数组、字符数组等)去存很多数据,应该怎么办呢?
数组定义和基本使用
在 MIPS 代码中,一般会分为两部分,.data 部分与 .text 部分,我们前面写的代码都属于 .text 部分。
.data
# 全局数据段的定义
.text
# 全局代码段的编写
但是当我们需要使用数组时,则要使用.data 段,像 C 语言那样,在内存当中申请一段连续的空间。
如下所示,在 .data 定义一个起始地址为 array,数组的长度是 100。考虑到 int 有 4 个字节,所以每个数字占用 4 个 space,所以这里写 .space 400。
.data
array: .space 400
后续代码的编写,我们需要放在 .text 段。
我们想要使用数组,首先需要将数组首地址赋值给一个寄存器,用 la (load address)指令
la $t1, array
使用这条指令之后,array 数组的首地址就被存放到 $t1 寄存器当中。
现在我们拿到了 array 数组的首地址,我们应该如何拿这个地址里面的内容呢?
需要使用 sw(save word)指令和 lw(load word)指令来进行赋值和取值。
使用 a(b) 的结构来进行寻址查询。下面我们解释一下这个结构:
括号里的寄存器(在这里就是寄存器 b)里存的应该是一个地址
使用括号,表示在内存中去找这个地址
a 表示偏移 a 个字节,可以理解成数组下标。例如 int 有 4 个字节,那么对于整数数组,偏移 4 个字节才是数组下标增加 1。
注意,这种使用方法当中,a 只能是立即数,不能是寄存器。
另一种方法是使用 数组名(偏移) 的方式来进行寻址。这种方法显得更加简单方便。
例如 array($t0),但是这种方式当中,偏移部分需要是寄存器,寄存器里的值就是偏移的位数。
下面的例子便是将 array[0] 赋值为 3,array[1] 赋值为 4 的两种方法。
.data array: .space 400 .text la $t1, array li $t2, 3 sw $t2, 0($t1) # array[0] = 3 li $t3, 0 sw $t2, array($t3) # array[0] = 3 li $t2, 4 addi $t3, $t3, 4 sw $t2, array($t3) # array[1] = 4 sw $t2, 4($t1) # array[1] = 4 lw $t2, 0($t1) # 把 array[0] 的值 load 到 t2 寄存器当中
按照前面的说法,4($t1) 可以理解为偏离 t1 寄存器中的地址 4 个空间。
对于 array[1] 的访问,还可以使用另一种方法:
.data
array: .space 400
.text
la $t1, array
addi $t1, $t1, 4
li $t2, 3
sw $t2, 0($t1)
那既然前面的数字表示偏移,我直接把括号内的地址加上要偏移的数,不也可以达到一样的效果。
为什么要这样做呢,这样做有什么好处吗?
考虑下面的代码:
int array[20];
for (int i = 0; i < 10; i++) {
array[i] = i;
}
如果采用刚才我们最开始所讲述的赋值方法,我们需要写十次赋值函数,因为sw $t2, 4($t1) 中的 4,不能用一个变量来代替。而如果我们采用现在所讲的赋值方法,仅需如此便可解决:
.data
array: .space 400
.text
la $t1, array # t1 存的是数组首地址
li $t2, 0 # t2 存的是代码中的 i 值
loop:
bge $t2, 10, end
sw $t2, 0($t1) # a[i] = i
addi $t1, $t1, 4 # 下一个数组元素地址(加 4 个单位空间)
addi $t2, $t2, 1 # i++
j loop
end:
字符/字符串
其实跟 int 类型大同小异。字符串也只不过是字符数组而已,换汤不换药。
只不过 1 个整数占 4 个字节,而 1 个字符只占 1 个字节。
需要注意的是,正因为 1 个字符只占 1 个字节,所以在内存中存取时,需要使用 sb(save byte)指令和 lb(load byte)指令。
另外,下标增加计算偏移时,每次也只需要增加 1 个空间即可。
.data
str: .space 400
.text
li $v0, 8
la $a0, str
li $a1, 400
syscall # 读字符串
la $t0, str
lb $a0, 0($t0) # 把 str[0] load 到 a0 寄存器
lb $a1, 1($t0) # 把 str[1] load 到 a1 寄存器