MIPS指令集和汇编
MIPS指令集和汇编
⼀、寄存器与内存
1.1 字节与字
关于对字(word)的理解,我⼜有了新的认识,word是⼀种操作单位,⽽且是最常见的操作单位(不是最⼩的),内存的地址是⽤⼀个字(32位⼆进制数)来表⽰的,⼀条指令的长度是⼀个字,寄存器的⼤⼩也是⼀个字,⽴即数的⼤⼩也是⼀个字,甚⾄对于那些不⾜⼀个字长度的数据,我们都需要给它补齐,然后再对它进⾏操作。所以字最⼤的特点就是它的通⽤性。
那么什么是字节(byte)呢?字节是指令集的最⼩单位。这个观点是花了很久才意识到的,因为我知道数据是按⼆级制表⽰的,那么如果把位(bit)作为最⼩单位,岂不是理所当然。但其实就是不是的,这可能是因为我们需要更简洁更优雅的指令抽象,所以就舍弃了对单独⼀个位的操作,就好像数字电路舍弃了异步和连续⼀样。注意,字节不但是操作的最⼩单位(可能不准确,我还没想到太明显的操作字节的指令),⽽且是指令集的最⼩单位,⽐如我们说0x00000000和0x00000001这两个地址差1,那么这个1就是“1个字节”,也就是这两个地址之间差了8个bit。⽽不是1个bit。再⽐如说,我们都说int是4字节,c
har是1字节,我们从来不说int是32位,char是8位,可见更⾼层次抽象的⾼级语⾔,就更不把位当回事请了。
然后我们还需要⼀些直观的认识,⼀个字节是8bit,也就是说,他的能表⽰256个不同的状态,如果写成⼀个⼗六进制数,那么他可以表⽰任何⼀个两位的⼗六进制数,也就是长成这个样⼦ XX。在MIPS中,⼀个word是由四个字节组成的,也就是说,我们去写⼀个⼗六进制数,它应该长成这个样⼦ 0x XX_XX_XX_XX 。⼀个word的⼤⼩是跟⼀个int⼀样⼤的,同时,它跟MIPS中的⽴即数是⼀样⼤的,也就是说,⼀个word刚好能存⼀个数字(开数组的时候会⽤到)。但是可以存4个字符,这是因为字符只占⼀个字节。
因为字符是四分之⼀word,所以就造成数据存储的时候可能就会不规整了,就好像原本严丝合缝的砖之间突然了⼀堆碎⽯⼦,就没办法规整了。没有办法规整导致的后果就是我们极可能错误操作,所以⼀定要把字符串留到最后写(先写数组,这样就相当于先砌墙,后堆沙⼦),甚⾄需要多打⼏个空格。
1.2 ⼤端与⼩端
所谓的⼤端模式(Big-endian),是指数据的⾼字节,保存在内存的低地址中,⽽数据的低字节,保存在内存的⾼地址中。所谓的⼩端模式(Little-endian),是指数据的⾼字节保存在内存的⾼地址中,
⽽数据的低字节保存在内存的低地址中,这种存储模式将地址的⾼低和数据位权有效地结合起来,⾼地址部分权值⾼,低地址部分权值低,和我们的逻辑⽅法⼀致。
那么MIPS中是怎样的呢?⾸先我们需要明⽩MARS中地址的显⽰⽅法。我们运⾏下⾯的代码:
.data
str: .ascii "12345678"
得到的结果是这样的
也就是说,MARS对地址的显⽰⽅式,在⼀个字内是逆序的,在字之间是顺序的,如下图
⼀0x000000030x000000020x000000010x00000000⼆0x000000070x000000060x000000050x00000004然后我们运⾏下⾯代码来检测MIPS的⼤⼩端问题
.data
num: .space 4
.text
li $t1, 0x11223344
sw $t1, num
得到的结果如图
也就是说,⾼位的11,被存在了0x00000003,低位的44被存在了0x00000000。所以MIPS是⼩端存储。
⼩端存储对于数字来说是⾃然的,但是对于字符串,就是逆序的。⽐如储存 “love”,会得到如下结果:
1.3 寄存器为主
寄存器为主是⼀种MIPS指令集给我的直观感受。第⼀、有很多指令都是以寄存器为对象的,⽐如lw,
就是将内存的内容加载到寄存器
中,sw就是将寄存器中的数据存到内存中,li就是将⽴即数加载到寄存器中。第⼆、对寄存器的操作最多,我们可以复制⼀个寄存器中的值到另⼀个寄存器,可以给寄存器⼀个⽴即数,可以运算寄存器中的值,这些都是内存实现不了的。所以在设计的时候,不仅在速度上,寄存器更加占优势,⽽且在指令集完备性上,寄存器也远远优于内存。
⼆、汇编语⾔细节
2.1 寄存器的数字表⽰
在MIPS中,寄存器都是⽤5位⼆进制数表⽰的,刚好对应32个通⽤寄存器。还是⼗分合理的。
2.2 ⾸地址
当我们进⾏load和store操作的时候,操作的都是⼀个数据段,但是这些语句的操作数都是⼀个地址,那么数据段和地址对应的点是怎样对应起来的呢?是这样的,以lw为例,他修改的以address到address+3的地址,也就是说,address是数据段的⾸地址。
在lh,lb指令中,加载的数据段是半个或者四分之⼀个word,那么就会涉及拓宽数据的问题,这些拓宽都是符号拓展。
2.3 变量的作⽤范围
汇编的所有变量都是全局可见的,从这个⾓度讲,⽆论这个变量是存在寄存器,还是内存,都是符合全局变量概念的。我在这⾥不想讨论⾼级语⾔的全局变量和局部变量在MIPS上与内存,栈之类结构的对应关系。我想要讨论的是我们在实现算法的时候,需要的变量的不同特点。我们既需要那种对全局可见的,很稳定的(⽐如图的顶点个数,数据的组数和⼤⼩)变量,算法意义上的 “全局变量”。⼜需要⽣命周期很短的,随叫随到的,不会对其他值造成影响的变量(⽐如迭代变量i),算法意义上的 局部变量。这些东西,现在感觉还是都放在寄存器中吧。太过于微妙的把握我还不太清楚。
对于“局部变量”,⼀定要注意每次使⽤前,初值对不对,就好像要每次使i的时候都要赋初值⼀样。
三、汇编流控制
3.1 总论
流控制有两个我认为⽐较重要的特点,⼀个是标签的使⽤,因为汇编的流控制本质上没有循环,分⽀判断、函数调⽤,所以跳转就是唯⼀控制流的⽅法,跳转最重要的参数就是地址,进⽽就是标签,我们把常见的⾼级语⾔翻译成汇编的时候,⼀定要注意需要设置⼏个标签,标签设置的位置。⽽不是局限于跳转的指令是哪⼀个。
另⼀个是对⾼级语⾔的抽象。有时候,其实⽤汇编直接写,要⽐翻译⾼级语⾔要好(在函数调⽤⽅⾯很明显),但是我们还是要翻译,这就是抽象简洁性原则。
3.2 if 结构
if结构只有⼀个需要打标签的地⽅,就是在if语句的结尾,这不难理解,因为跳转的⽬的是跳过if的内容,实现满⾜条件则执⾏
.text
li $t0 1
li $t1 2
slt $t2, $t0, $t1
beqz $t2 if_end #如果t0 < t1,执⾏if语句
nop
#if_statement
li $v0, 1
move $a0, $t0
syscall
if_end:
可以看到,只有⼀个标签,就是if_end,5-7⾏是if表达式的判断,9-11⾏是if执⾏的内容。
3.3 if-else 结构
if-else与if不同,它除了分⽀语句(b打头的指令),还需要跳转指令(j打头的指令),是因为执⾏完if的内容后,还需要跳过else的内容。下⾯是⼀个求绝对值并输出的程序。
.text
li $t0 1
li $t1 2
slt $t2, $t0, $t1
beqz $t2 else  #如果t0 < t1,执⾏if语句
nop
#if_statement
sub $t3, $t1, $t0汇编指令有多少个
j if_end #这⾥⽐单纯的if多了⼀个跳转
#else_statement
else:
sub $t3, $t0, $t1
if_end:
li $v0, 1
move $a0, $t3
syscall
可以看到,⼀共需要两个标签。else标签⽤于分⽀到else语句,if_end标签⽤于在执⾏完if_statement以后,跳过else_statement,所以⼀共是两个标签,不可以忘记⼀个。
3.4 for 循环
关于for循环,还是有很多易错点的,让我们先看⼀个简单的for循环,⽤于计算1-10累加求和。
.text
#i = 1
li $t0, 1
for_begin:
sle $t1, $t0, 10
beqz $t1, for_end
#for_statement
add $s0, $s0, $t0
#i++
addi $t0, $t0, 1
j for_begin
for_end:
li $v0, 1
move $a0, $s0
syscall
我们看到,这⾥⽤了for_begin、for_end两个标签,⼀个分⽀指令,⼀个跳转指令。需要注意的是,for_begin的位置要在给迭代变量赋初值之后,不然的话,就会每次都给i赋初值,⽆法⾛出循环。
当程序变得复杂的时候,⽐如计算1-10中偶数累加的时候:
.text
li $t0, 1
for_begin:
sle $t1, $t0, 10
beqz $t1, for_end
#for_statement
andi $t2, $t0, 1 #偶数t2就是0,奇数t2就是1
bgtz $t2, if_end #这个bgtz可以⽤来跟beqz对⽐,是逻辑判断的对照
nop
#if_statement
add $s0, $s0, $t0
if_end:
#i++
addi $t0, $t0, 1
j for_begin
for_end:
li $v0, 1
move $a0, $s0
syscall
这⾥要思考的就是,if_end应该出现在哪⾥,是跟for_end重合吗?不是,⽽是要出现在i++之前。道理很简单,但是我经常记不住。
3.5 函数调⽤
3.5.1 块思想
我们⾸先来⼀个简单的sum函数,并且⽤最快捷(不是最规范)的⽅法实现它。
C语⾔原型如下:
int sum(int a,int b)
{
int tmp = a + b;
return tmp;
}
int main()
{
int a =2;
int b =3;
int sum1 =sum(a, b);
printf("%d", sum);
return0;
}
错误的汇编代码如下:
.text
#main
li $s0, 2
li $s1, 3
jal sum
li $v0, 1
move $a0, $s2
syscall
sum:
add $s2, $s0, $s1
jr $ra
因为代码是顺序执⾏的,所以如果把sum写在下⾯的话,在执⾏完main以后,还会⾃动执⾏sum,然后就陷⼊死循环了。
我们⼜两种解决办法,⼀个是把sum提到main之前,但是这种⽅法不本质,因为造成bug的原因是我们默认程序结束是在最后⼀⾏代码,这在编程中是不太⽅便的,不如我们⾃⼰调⽤命令使程序结束,这样才本质,修改如下:
.macro end
li $v0, 10
syscall
.
end_macro
.text
li $s0, 2
li $s1, 3
#函数调⽤过程
jal sum
li $v0, 1
move $a0, $s2
syscall
end
sum:
add $s2, $s0, $s1
jr $ra
这样的18-20⾏,才是真正的,我们规定了⼊⼝和出⼝,只有按照我们规定的⽅法才能进⼊和出去的程序块(不会因为顺序执⾏⽽进⼊)。
3.5.2 复⽤思想
但是写好了程序块,是不是就意味着写好了函数的功能了呢?其实没有,因为我们没有办法实现代码的复⽤,⽐如C程序
int sum(int a,int b)
{
int tmp = a + b;
return tmp;
}
int main()
{
int a =2;
int b =3;
int c =4;
int d =5;
int sum1 =sum(a, b);
printf("%d", sum1);
sum2 =sum(c, d);
printf("%d", sum2);
return0;
}
按道理,这个是可以复⽤sum的代码的,这也是函数最⼤的功能,但是我们原来的那个函数操作的是s0,s1,s2,显然没有办法操作其他寄存器,所以为了复⽤代码,就必须引⼊传参的技术,也就是函数能⽤的数都是形参寄存器(a0-a3)传给他的。
有完整代码:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。