MIPS阶乘汇编代码分析
#压栈入参
addiu $sp,$0,0x10010080
设置栈指针**$sp**
addiu $s0,$0,5 #n=5
sw $s0,0($sp)
addiu $sp,$sp,-4
因为是求5的阶乘,所以初始参数为5
jal FACT
nop
j END
nop
FACT为阶段函数,nop是mips的规范跳转之后必须跟nop
FACT:
#压栈返回地址
sw $ra,0($sp)
addiu $sp,$sp,-4
#读取入参
lw $s0,8($sp)
#压栈返回值
sw $0,0($sp)
addiu $sp,$sp,-4
#递归base条件
bne $s0,$0 ,RECURSION
nop
#读取返回地址
lw $t1,8($sp)
#出栈:返回值,返回地址
addiu $sp,$sp,8
#压栈返回值
addiu $s0 ,$zero,1
sw $s0,0($sp)
addiu $sp,$sp,-4
jr $t1
nop
上面是完整FACT代码,下面逐个分析
#压栈返回地址
sw $ra,0($sp)
addiu $sp,$sp,-4
ra保存的是pc的值,这句话就是将ra的值保存进栈。一般会和下面这句连用
jal FACT
nop
jal表示跳转的同是把pc的值保存进去ra,所以上面两段代码配合就可以做到记录返回地址的作用。我们要知道在上面一段代码后面就是计算阶乘代码,所以每次压栈这个地址都是表示要进一次阶乘计算代码。而跳到FACT之后呢,紧接着就是压栈ra,然后将栈指针减4,指向一个存储区域。
#读取入参
lw $s0,8($sp)
读取入参之前,会有两次压栈,比如5这个参数压栈之后,后面就会压栈ra(存在jal指令)所以参数在当前sp的前8个,此时s0保存前一个参数。
#压栈返回值
sw $0,0($sp)
addiu $sp,$sp,-4
这里就是用0来占个位置,后续会复写该值。
#递归base条件
bne $s0,$0 ,RECURSION
nop
bne指令当s0不等于0是,跳到RECURSION。否则知心下面的语句
#读取返回地址
lw $t1,8($sp)
#出栈:返回值,返回地址
addiu $sp,$sp,8
#压栈返回值
addiu $s0 ,$zero,1
sw $s0,0($sp)
addiu $sp,$sp,-4
jr $t1
nop
读取返回地址,就是没次执行jal FACT这行代码所对应的pc位置。
出栈返回值和返回地址:当触发递归终止条件是此时返回值应该为0,因为一直用0来占位置。返回地址就上一次执行jal FACT的那个地址。
之后然如返回值1,因为阶乘终止条件会返回1,所以1就是返回值。将1压入栈,此时会覆盖上一个返回地址的位置。可能有人会奇怪,返回地址不是已经出站了吗?其实返回地址仍然在栈顶,因为每次压栈sp都会减4,所以一般来说sp指的位置都是没有指的。所以返回地址应该是在sp-12到s-8的那个位置,前两个分别是空和返回值。但是此时1会覆盖此返回值,并且sp-4。此时递归终止条件逻辑就写完了。
RECURSION:#recursion
addiu $s1,$s0,-1
sw $s1,0($sp)
addiu $sp,$sp,-4
jal FACT
nop
lw $s0,20($sp)
lw $s1,4($sp)
lw $t1,16($sp)
mult $s1,$s0
mflo $s2
#出栈
addiu $sp,$sp,16
sw $s2,0($sp)
addiu $sp,$sp,-4
jr $t1
nop
上面是递归代码,下面对其进行分析
addiu $s1,$s0,-1
sw $s1,0($sp)
addiu $sp,$sp,-4
这三句其实就是参数减一然后将新的参数压入栈,栈指针-4。对应到高级语言就是f(n-1)的部分;
jal FACT
nop
jal会把 当前pc的值给ra然后跳到fact,这里紧跟着后面就是讲ra的值压入栈,其实就调用地址入栈后面出栈计算紧跟着就是弹出当时压进去的调用地址。
lw $s0,20($sp)
lw $s1,4($sp)
lw $t1,16($sp)
上面三句要结合栈的情况来看,假设我们初始参数不是5而是1,ra前面的数表示不同深度的调用地址
100 | 96 | 92 | 88 | 84 | 80 | 76 |
---|---|---|---|---|---|---|
1 | 1-ra | 0 | 0 | 2-ra | 0 |
88表示的是当前深度的参数,80为当前深度的返回值(只是占了一个位置),现在我们会进入下面的代码
#读取返回地址
lw $t1,8($sp)
#出栈:返回值,返回地址
addiu $sp,$sp,8
#压栈返回值
addiu $s0 ,$zero,1
sw $s0,0($sp)
addiu $sp,$sp,-4
jr $t1
nop
t1此时为2-ra,紧接着80和76会被出栈,栈顶为84。然后会把1压入栈也就是覆盖84的2-ra(2-ra已经被保存所以不用担心),压栈返回值之后栈是下面这样。
100 | 96 | 92 | 88 | 84 | 80 |
---|---|---|---|---|---|
1 | 1-ra | 0 | 0 | 1 |
然后跳到t1也就是2-ra,代码如下1
lw $s0,20($sp)
lw $s1,4($sp)
lw $t1,16($sp)
mult $s1,$s0
mflo $s2
#出栈
addiu $sp,$sp,16
sw $s2,0($sp)
addiu $sp,$sp,-4
jr $t1
nop
s0就是100地址对应的1,s1就是84,t1就是1-ra。紧接着就是s1*s0然后保存到s2,将前面四个参数出栈,再将s2保存到栈顶,此时栈是下面这样。
100 | 96 |
---|---|
1 | 1 |
最后跳到1-ra 重复执行上面的步骤,结果为1然后返回。
可能我说的比较啰嗦,但是从这个例子就能明白递归的原理,无非就是保存每次的调用地址。最后在一次次进入调用地址。
完整代码如下:
#压栈入参
addiu $sp,$0,0x10010080
addiu $s0,$0,5 #n=5
sw $s0,0($sp)
addiu $sp,$sp,-4
jal FACT
nop
j END
nop
FACT:
#压栈返回地址
sw $ra,0($sp)
addiu $sp,$sp,-4
#读取入参
lw $s0,8($sp)
#压栈返回值
sw $0,0($sp)
addiu $sp,$sp,-4
#递归base条件
bne $s0,$0 ,RECURSION
nop
#读取返回地址
lw $t1,8($sp)
#出栈:返回值,返回地址
addiu $sp,$sp,8
#压栈返回值
addiu $s0 ,$zero,1
sw $s0,0($sp)
addiu $sp,$sp,-4
jr $t1
nop
RECURSION:#recursion
addiu $s1,$s0,-1
sw $s1,0($sp)
addiu $sp,$sp,-4
jal FACT
nop
lw $s0,20($sp)
lw $s1,4($sp)
lw $t1,16($sp)
mult $s1,$s0
mflo $s2
#出栈
addiu $sp,$sp,16
sw $s2,0($sp)
addiu $sp,$sp,-4
jr $t1
nop
END:
结果如上图 16进制78就是120