函数调用
一.调用初印象
最早接触到函数调用是在选择排序程序中,教学视频中代码块来换回拼接导致我看了好几遍视频! 下面附上源码
| |
上述选择排序算法较为复杂,主体结构为main调用input,sort,output函数,在sort中又调用findmin子函数
在MARS中的运行配置:
- delayed branching
- initial program counter to global “main” if defined
- address configuration: compact ,data at address 0
需要注意的是多层函数调用时除了经典的父函数维护t寄存器,子函数维护s寄存器,还要对ra寄存器进行维护,经典的例子是在sort调用findmin过程中,$ra一开始存储的值是sort函数返回到主函数main下一条指令的地址,经过调用findmin,寄存器$ra中的值会被自动更新为findmin跳回到sort的指令地址,故需要保存跳回到main的指令地址,对$ra进行维护!!!
| |
二.函数调用
对于函数调用,可以看成在函数调用的这条语句,程序跳转到函数的内容处开始执行,执行完整个函数后再跳转回到调用处向下执行,这个跳转过程可以用汇编语言中的跳转指令和标签实现。
# function call
jal function_name
# function
function_name:
<function-content>
jr $ra
在这里我们的跳转指令选择jal而非j, jal = jump and link,相比j指令,jal多了将PC+4 写入$ra的过程,即记录跳转语句下一条语句的地址。当函数结束时,会返回到之前调用它的位置,并执行下一条指令。故我们常常搭配使用jal和j.
例如简单的C语言代码,计算两数相加
| |
翻译成汇编语言
| |
三.复用代码
| |
上面的C程序,sum代码是可以进行复用的,但是在我们原来的汇编代码中,操作的寄存器是固定的,即$s0,$s1,$s2,为了复用代码(可以对其他寄存器进行操作),就必须要让一些特定寄存器作为“接收器”,对于不同的参数,都采用同一组寄存器来存储它们的值,也就是我们说的函数传参寄存器$a0.$a1,$a2,$a3,同样,对于返回值,也需要指定特定的寄存器。
//利用宏进行代码复用
.macro end
li $v0,10
syscall
.end_macro
.macro printStr(%Str)
la $a0,%Str
li $v0,4
syscall
.end_macro
.data
space: .asciiz " "
.text
li $t0,1
li $t1,2
li $t2,3
li $t3,4
#传参 $a0,$a1作为桥梁作用
move $a0,$s0
move $a1,$s1
jal sum
move $s4,$v0
li $v0,1
move $a0,$s4
syscall
printStr(space)
move $a0,$s2
move $a1,$s3
jal sum
move $s5,$v0
li $v0,1
move $a0,$s5
syscall
end
sum:
#传参
move $t0,$a0
move $t1,$a1
add $v0,$t0,$t1
jr $ra
如果传参过程中$a0,$a1,$a2,$a3不够用(参数超过四个),可以利用栈$sp,将多余的参数存入内存中。
四.避免对外界造成影响
函数还有一个重要的功能是不对函数体外的变量造成不必要的影响
| |
汇编
.macro end
li $v0, 10
syscall
.end_macro
.text
li $s0, 2
li $s1, 3
li $t0, 4
move $a0, $s0 #传参
move $a1, $s1
jal sum
move $s4, $v0 #获得返回值
move $a0, $s4
move $a1, $t0
jal sum
move $s5, $v0
li $v0, 1
move $a0, $s5
syscall
end
sum:
#传参过程
move $t0, $a0 #t0被修改了
move $t1, $a1
#函数过程
add $v0 $t0, $t1
jr $ra
这里的问题在于,$t0在主函数中存储变量值,而在sum函数中用于接受参数,改变了原来的变量值,即函数对外部产生了影响!
所以我们需要保证函数不会对外部造成影响,方法就是应用栈,利用栈来保存和恢复函数中所使用的寄存器。
在哪里维护寄存器:
- t寄存器:在调用者中进行维护‘
- s寄存器:在被调用者中进行维护
1.在调用者中进行维护
.macro end
li $v0, 10
syscall
.end_macro
.text
li $s0, 2
li $s1, 3
li $t0, 4
move $a0, $s0 #传参
move $a1, $s1
#进行维护 入栈
sw $t0, 0($sp)
addi $sp, $sp, -4
jal sum
#出栈
addi $sp, $sp, 4
lw $t0, 0($sp)
move $s4, $v0 #获得返回值
move $a0, $s4
move $a1, $t0
sw $t0, 0($sp) #入栈
addi $sp, $sp, -4
jal sum
addi $sp, $sp, 4 #出栈
lw $t0, 0($sp)
move $s5, $v0
li $v0, 1
move $a0, $s5
syscall
end
sum:
#传参过程
move $t0, $a0
move $t1, $a1
#函数过程
add $v0 $t0, $t1
jr $ra
**在以上代码中,只涉及到对t寄存器的维护,故只需要在主函数中使用栈,在子函数中无需使用栈,**在发觉这一点之前,我对于主函数中对于栈指针$sp移动的操作表示迷惑,认为完全可以删去这两行代码。事实上,如果在一个参数的入栈与出栈之间没有对$sp进行任何操作,确实可以不移动$sp,但事实上,考虑到我们习惯上第一个参数入栈表达为 lw $s0 0($sp),即相对于栈指针地址没有偏移,而如果我们在子函数中这样存入,在主函数中又没有移动$sp,无疑会覆盖掉我们在那个位置上保存的参数,从而发生bug,(杠精当然可以说我会写lw $s0, 4($sp)),但这样写在主函数中维护寄存器数量很多时很费笔墨,不如移动栈指针来的简洁。当然,我们在子函数中同样需要注意栈指针的移动,在子函数结束时即使释放栈空间,防止覆写主函数中维护的参数。
2.在被调用者中维护
.macro end
li $v0, 10
syscall
.end_macro
.text
li $s0, 2
li $s1, 3
li $t0, 4
move $a0, $s0 #传参
move $a1, $s1
jal sum
move $s4, $v0 #获得返回值
move $a0, $s4
move $a1, $t0
jal sum
move $s5, $v0
li $v0, 1
move $a0, $s5
syscall
end
sum:
#入栈过程
sw $t0, 0($sp)
addi $sp, $sp, -4
#传参过程
move $t0, $a0
move $t1, $a1
#函数过程
add $v0 $t0, $t1
#出栈过程
addi $sp, $sp, 4
lw $t0, 0($sp)
#return
jr $ra
五.嵌套函数调用
嵌套函数调用的重要意识是用栈保存$ra,以保存向外层函数的跳转,这一点我在sort程序中就有发现,还是有一定理解能力(bushi).
嵌套函数的C语言例子
| |
其中cal函数嵌套调用sum函数。
.macro end
li $v0, 10
syscall
.end_macro
.text
li $s0, 2
li $s1, 3
move $a0, $s0
move $a1, $s1
jal cal
move $s5, $v0
li $v0, 1
move $a0, $s5
syscall
end
sum:
#传参过程
move $t0, $a0
move $t1, $a1
#函数过程
add $v0, $t0, $t1
#return
jr $ra
cal:
#传参过程
move $t0, $a0
move $t1, $a1
#调用sum的过程
move $a0, $t1
move $a1, $t0
jal sum
move $t2, $v0
#运算a-sum(b, a)
sub $v0, $t0, $t2
#return
jr $ra #错误的地址值 造成死循环
这段代码会陷入死循环!在cal调用sum时,$ra中的值由cal跳回主函数的地址由jal指令更改为sum跳回cal的地址,因此会陷入死循环。
所以我们可以总结出:在嵌套函数调用中,一旦一个函数不是叶子函数(调用逻辑的最低端),就需要保存和恢复$ra,以能够正常一层一层向上返回。
以下为在父一级函数中对$ra进行维护的正确代码
.macro end
li $v0, 10
syscall
.end_macro
.text
li $s0, 2
li $s1, 3
move $a0, $s0
move $a1, $s1
jal cal
move $s5, $v0
li $v0, 1
move $a0, $s5
syscall
end
sum:
#将 $t0 和 $t1 入栈
sw $t0, 0($sp)
addi $sp, $sp, -4
sw $t1, 0($sp)
addi $sp, $sp, -4
#传参过程
move $t0, $a0
move $t1, $a1
#函数过程
add $v0 $t0, $t1
#将 $t0 和 $t1 出栈
addi $sp, $sp, 4
lw $t1, 0($sp)
addi $sp, $sp, 4
lw $t0, 0($sp)
#return
jr $ra
cal:
#将 $ra 入栈
sw $ra, 0($sp)
addi $sp, $sp, -4
#传参过程
move $t0, $a0
move $t1, $a1
#调用 sum 的过程
move $a0, $t1
move $a1, $t0
jal sum
move $t2, $v0
#运算a-sum(b, a)
sub $v0, $t0, $t2
#将ra出栈
addi $sp, $sp, 4
lw $ra, 0($sp)
#return
jr $ra
六.递归函数调用
最后的部分:递归函数的汇编翻译,递归函数的本质是一个在函数体内调用自身的嵌套函数。
C语言阶乘
| |
汇编版本
# 程序结束
.macro end
li $v0, 10
syscall
.end_macro
# 从标准输入处得到一个整型变量,并存储到 %des 寄存器中
.macro getInt(%des)
li $v0, 5
syscall
move %des, $v0
.end_macro
# 向标准输出中写入一个数据,这个数据保存在 %src 寄存器中
.macro printInt(%src)
move $a0, %src
li $v0, 1
syscall
.end_macro
# 将寄存器 %src 中的数据入栈
.macro push(%src)
sw %src, 0($sp)
subi $sp, $sp, 4 #入栈栈指针向低地址移动
.end_macro
# 将栈顶数据出栈,并保存在 %des 寄存器中
.macro pop(%des)
addi $sp, $sp, 4 # 出栈栈指针向高地址移动
lw %des, 0($sp)
.end_macro
.text
main:
getInt($s0)
move $a0, $s0
jal factorial #最终结果存储在$v0中
move $s1, $v0
printInt($s1)
end
factorial:
# 入栈
push($ra)
push($t0)
# 传参
move $t0, $a0
#函数过程
bne $t0, 1, else
# 基准情况
if:
li $v0, 1
j if_end
# 递归情况
else:
subi $t1, $t0, 1
move $a0, $t1
jal factorial
mult $t0, $v0
mflo $v0
if_end:
# 出栈
pop($t0)
pop($ra)
# 返回
jr $ra
我们可以预想到,调用过递归函数的栈中,一定是整整齐齐的存储了一排$ra值