从高级语言到汇编语言(MIPS)
C语言是如何转化为汇编语言的?这一步在电脑中是由汇编程序完成的,但是了解C语言到汇编语言的转换过程有利于我们更好的编写出性能更加优异的程序,因此下面我将逐步介绍从C到MIPS的核心思想和实现步骤。
一、存储结构
核心:在MIPS中,所有的操作数必须来自于寄存器
1、寄存器和存储器的信息沟通
我们可以将存储结构分为寄存器和存储器两个部分。存储器是一个巨大的仓库,存储着运算所需要的全部数据;而寄存器就是我们的工作台,我们只能在工作台上进行工作。由于工作台上的空间有限,所以我们必须将不用的东西放回仓库,即从寄存器中放回存储器中,同理,从存储器中取出我们需要的数据也是必要的。
2、寄存器详解
①寄存器的大小:一个寄存器的大小是32位,由于我们经常以一个寄存器为单位进行数据的存取,因此我们也将一个一个寄存器的大小称为一个“字”。
②寄存器的个数:我们通常所说的32位、64位操作系统指的就是这个操作系统中寄存器的个数,在MIPS一类典型的现代计算机中一般有32个寄存器。至于为什么只有32个这么少,是因为往往“少就是快”。
③寄存器的命名:MIPS为了区分32个寄存器,对寄存器进行了分类和命名,如下所示:
符号 | 名称 | 用途 |
---|---|---|
$zero | 零寄存器 | 总是返回 0 |
$at | 汇编器临时寄存器 | 由汇编器用来处理某些汇编指令 |
$v0-$v1 | 函数返回值寄存器 | 用来存放函数的返回值 |
$a0-$a3 | 函数参数寄存器 | 用于传递函数参数 |
$t0-$t9 | 临时寄存器 | 用于保存临时数据,在函数调用时不保留其内容 |
$s0-$s7 | 保存寄存器 | 用于保存重要的局部变量,在函数调用时保留其内容 |
$k0-$k1 | 操作系统保留寄存器 | 用于操作系统的中断和异常处理 |
$gp | 全局指针寄存器 | 用于存储全局数据区域的大致中间位置的地址 |
$sp | 栈指针寄存器 | 用于指向当前栈顶的地址 |
$s8 | 帧指针寄存器 | 用于指向函数栈帧的开始 |
$ra | 返回地址寄存器 | 用于存储函数调用后的返回地址 |
其中部分寄存器的具体作用我将在下文阐明
二、MIPS指令基本格式
1、三操作数形式
大部分MIPS指令都符合如下形式
操作名 寄存器 1 , 寄存器 2 , 寄存器 3 操作名\quad 寄存器1,寄存器2,寄存器3 操作名寄存器1,寄存器2,寄存器3
表示将寄存器2和3中的数据按照操作名指定的操作进行运算,然后将数据存入寄存器1中
三、实现算术操作
1、基础算术指令
①加法
示例:add $s1,$s2,$s3
含义:$s1=$s2+$s3
②减法
示例:sub $s1,$s2,$s3
含义:$s1=$s2-$s3
③立即数加法
示例:addi $s1,$s2,20
含义:$s1=$s2+20
2、编译示例
#c
f=(g+h)-(i+j) //假设f,g,h,i.j依次分配给$s0,$s1,$s2,$s3,$s4
#MIPS
add $t0,$s1,$s2
add $t1,$s3,$s4
sub $s0,$t0,$t1
四、实现数据交换
1、数据传送指令
①取字(load word)
示例:lw $s0,32($s3)
含义:以”$s3中存储的地址偏移32个字节“为首地址,取出一个字到$s0(32位)
详解:32是偏移量,是基于寄存器$3中存储的地址上偏移的位数。其计算方法如下:
MIPS按字节编址,即:1个字=4个字节=32比特,每一个字节拥有一个地址。偏移量就是首地址距离目标地址之间字节的个数。注意,偏移量必须是立即数。
②存字(save word)
示例:sw $s0,32($s3)
含义:以”$s3中存储的地址偏移32个字节“为首地址,将$s0中的数据存入一个字(32位)
2、编译示例
#C
A[12]=h+A[8]//假设A是一个存储了100个字的数组,首地址存储在$s3中,h在$s2中
#MIPS
ld $t0,32($s3)
add $t1,$t0,$s2
sw $t1,48($s3)
五、实现逻辑操作
1、逻辑操作指令①左移
示例:sll $t0,$s0,4
含义:将$s0中的数据左移4位,在新的位上填充0之后的数据放入$t0中,相当于乘以2的四次方(无符号数成立)。
②右移
示例:srl $t0,$s0,4
含义:将$s0中的数据右移4位,在新的位上填充0之后的数据放入$t0中,相当于除以二的四次方(无符号数成立)。
③按位与
示例:and $t0,$s0,$s1
含义:将s0中和s1按位相与,结果放入t0中
④按位或
示例:or $t0,$s0,$s1
含义:将s0中和s1按位相或,结果放入t0中
⑤或非
示例:nor $t0,$s0,$s1
含义:将s0中和s1按位相或后取反,结果放入t0中。注意,MIPS中没有直接取反的操作,这是为了保持三操作数的格式。实际上,如果一个数据或非上$zero,那么就相当于取反。
⑥补充:and、or均有andi、ori这样与上一个常数和或一个常数的版本,但是由于nor的主要作用是取反,因此没有设计nor一个立即数的指令。
六、实现条件分支和循环
1、标签和跳转
我们一般的指令格式为:操作名+若干操作数。在MIPS指令集执行的过程中,指令按照顺序逐一执行。但是在C语言的if-else语句中,我们需要进行”跳转“这一操作。如果条件满足,则直接跳转到指定指令执行。但是问题是,计算机如何知道跳转到哪一条指令执行呢?因此,MIPS引入标签来解决这一问题。请看下列语句:
E l s e : s u b $ s 0 , $ s 1 , $ s 2 Else:sub\quad \$s0,\$s1,\$s2 Else:sub$s0,$s1,$s2
在这一语句中,我们的指令前多了一个字段”Else:“,这个字段称之为标签。在程序需要进行跳转的时候,我们只需要指定跳转到某一个标签,然后在目的指令前加上这个标签,系统就会从标签处开始逐个执行指令。这为我们条件分支的实现奠定了基础。
2、有条件分支指令
①相等时跳转(Branch if Equal)
示例:bnq $s0,$s1,Else
含义:如果s0和s1相等,则跳转到标签Else
②不相等时跳转(Branch if Not Equal)
示例:bne $s0,$s1,Else
含义:如果s0和s1不相等,则跳转到标签Else
③小于时置位(Set on Less Than)
示例:slt $t0,$s0,$s1
含义:如果s0<s1,则t0=1,否则t0=0。这个指令看似与跳转没有什么关联,但是是实现跳转的基石,其作用稍后我会介绍。
3、无条件分支指令
①跳转指令(jump)
示例:j Exit
含义:执行到该条指令时,无条件跳转到标签Exit
4、伪指令
我们已经注意到,在上述我们提到了等于/不等于时跳转,但是没有提到大于/小于时跳转,这一部分功能也是很重要的,我们使用伪指令来书写这个指令。如下所示
示例:blt $s0,$s1,Else
含义:如果s0<s1,则跳转到标签Else
之所以称之为伪指令,是由于对于汇编程序而言,该条指令会被翻译为使用bnq和slt指令组合实现。指令集的精简有助于速度的提高,已有指令可以实现的功能没有必要新建一个指令实现。
5、编译示例
#C
while(save[i]==k)//save的基址储存在s0中,i和k存储在s1,s2中
i+=1;
#MIPS
Label:sll $t0,$s1,2 //偏移量为4*s1,我们使用逻辑左移实现乘法
add $t0,$t0,$s0 //将基址和偏移量相加得到目标地址,注意之所以进行这一步是因为lw指令的偏移量只能是立即数而不能是$t0
lw $t1,0($t0)
bne $t1,$s2,Exit //如果不相等则跳出循环
addi $s1,$s1,1 //i+=1操作
j Label //继续循环
Exit:后续指令
6、循环和switch/case语句
可以注意到,我们在编译示例中已经实现了循环,这说明仅仅只需要一点技巧,循环是可以通过上述介绍的条件分支指令来实现的,不需要额外的指令。而对于switch/case语句,我们可以使用一系列的条件分支语句,将switch/case语句当作if-else嵌套进行转换。当然,我们也可以采用更为有效的方法,即”转移地址表“,有兴趣的小伙伴可以自己去了解。
七、实现函数
函数是C语言的重要组成部分,而令人惊讶的是在MIPS指令集中,出了函数实现的过程稍显繁琐之外,其基本思想和C语言并没有多大差别。
1、控制权移交
在执行函数之前,控制权在主程序手中。而实行函数之后,控制权则在函数手中。函数执行完之后如何将控制权返回给主程序?这是一个问题。MIPS利用”跳转和连接指令“、”寄存器跳转指令“和返回地址寄存器$ra解决了这个问题(详见最开始关于所有寄存器的介绍)
示例:jal Address含义:Address指的是函数部分的执行地址,这一条语句将控制权移交给函数,并且将主函数需要执行的下一条指令的地址存储在$ra中
在这一步,我们实现了函数的调用,那么在函数执行的最末尾,我们需要交回控制权
示例:jr $ra
含义:无条件跳转到寄存器$ra中的地址处执行,这一步将控制权交回给了主函数。
2、接受参数与返回参数
在函数调用过程中,我们需要使用$a0~$a3来传递参数,即将需要传递给函数的参数放在这四个寄存器中;与此同时,将函数执行的返回值放入寄存器$v0和$v1中。此时,主程序从返回值寄存器中取得返回值,函数从参数寄存器中获得参数。
八、万恶之源——栈
我们在上述的所有部分中所执行的指令都是相对粗浅的那一部分。如何实现更复杂的程序依然是一个问题,例如考虑以下问题
函数需要使用寄存器进行计算,寄存器中原本的数据怎么办?
递归函数如何实现?一个递归函数可能重复调用一个函数几千次而保留每一次的数据,32个寄存器显然是不够的。
大型局部变量如何处理?这些值不适合存储在寄存器中,该如何暂时保存?
这些所有复杂的操作都可以通过栈这个概念来实现,这也是我称之为万恶之源的原因,从栈开始,一切都变得复杂了起来。
1、栈的基本实现
MIPS利用栈指针寄存器$sp来实现栈,通过栈指针在存储器中开启了一片区域,栈指针始终指向栈顶。实现过程如下:
addi $sp,$sp,4 //腾出一个字的空间,四个字节
sw $s0,0($sp) //将s0入栈
lw $s0,0($sp) //出栈
addi $sp,$sp,-4 //恢复栈顶指针
2、函数需要使用更多的寄存器
考虑以下问题,我调用了一个函数,传递给了他四个参数。但是现在问题是我原本的主程序使用了所有的保存寄存器,即主程序已经在工作台上摆满了他的东西,我要使用这个工作台,怎么办?
答案是使用栈指针和lw,sw指令,我将我需要用的寄存器全部使用sw指令保存到栈中,然后使用这些寄存器,最后按照先进后出的顺序恢复这些寄存器。需要注意的是,所有的保存寄存器都需要恢复,而暂时寄存器则不需要。
3、实现递归函数
递归函数的实现和上述更新工作台的步骤并没有什么不同,我们需要理解的是栈的工作原理,即为什么在第1000次递归的时候我们为什么能保持第一次调用的值不丢失?
每进入一次新函数,我们需要向栈中挤压往期寄存器,在函数执行结束时弹出”自己挤压的寄存器“。此时,栈顶指针指向的是上一级函数挤压的寄存器,而控制权立即会移交给上一级函数,上一级函数弹出自己的寄存器,这样重复,递归函数的实现就变得可行了。
4、实现大型局部变量的存储
大型局部变量依然存储在栈中,但是MIPS额外使用了一个寄存器:帧指针。在一个过程中,我们首先将参数寄存器压栈,然后将保存的返回地址压栈,接着将保存寄存器压栈,最后将局部数据或结构体压栈,在需要的时候按顺序弹出恢复。而帧指针指向的是这个过程中栈的底部,栈指针指向的是栈顶。这是由于在一个过程中栈指针的位置是一直在变的,但是帧指针的位置是一开始就确定了的。利用帧指针进行地址的计算效率明显更高。
九、注意事项
MIPS的指令集内显然不止这么多指令,我所介绍的这些仅仅只是呈现一个C程序转为MIPS指令集的大概框架,了解其执行的基本过程。其中如有错误,希望各位指正。
标签:示例,汇编语言,s1,s0,高级,t0,指令,寄存器,MIPS From: https://blog.csdn.net/hgylyc/article/details/136996019