我正在尝试编写一个MIPS程序,它接受用户输入(整数n),然后打印所有的降序数字,直到1,然后打印所有的升序数字,直到n。
基本上,如果我输入3,输出将是:3 2 1 1 2 3
在C#中,代码为:
using System;
public static class LearnRecursion
{
public static void Main()
{
int n;
Console.Write("Enter an integer: ");
n = Convert.ToInt32(Console.ReadLine());
RDemo(n);
}
public static void RDemo(int n)
{
if (n < 1)
{
return;
}
else
{
Console.Write("{0} ", n);
RDemo(n - 1);
Console.Write("{0} ", n);
return;
}
}
}
我试过这样实现MIPS程序:
.data
### Declare appropriate strings and the space character for I/O prompts ###
input: .asciiz "Enter an integer : "
space: .asciiz " "
newline: .asciiz "\n"
.text
main:
### call procedure for printing the user prompt ###
li $v0, 4
la $a0, input
syscall
#read input from user
li $v0, 5
syscall
move $s0, $v0 #store the user input into saved register
move $a0, $s0 #move saved user input integer as argument for RDemo
jal RDemo
j exit
# recursive RDemo method
# excpects integer argument (from user input) in $a0
#returns when n<1
RDemo:
#make space for 4 registers on the stack
addi $sp, $sp, -12
sw $ra, 0($sp) #return adress
sw $s0, 4($sp) #saved register (original n)
sw $a0, 8($sp) #argument (user parsed n)
#base case: n < 1 return
blez $a0, RDemoReturn
#print n
li $v0, 1
move $a0, $a0
syscall
la $a0, space
li $v0, 4
syscall
#call RDemo with n-1
addi $a0, $a0, -1
jal RDemo
li $v0, 1
move $a0, $s0 #$s0 or $a0 ?
syscall
la $a0, space
li $v0, 4
syscall
RDemoReturn:
lw $ra, 0($sp)
lw $s0, 4($sp)
lw $a0, 8($sp)
addi $sp, $sp, 12
jr $ra
exit:
li $v0, 10
syscall
它最终打印了一个无限循环,只有原始整数n,然后是一堆看起来像地址的数字,例如24567等。
有人知道我的程序出了什么问题吗?
1条答案
按热度按时间bxjv4tth1#
这段代码犯了几个与调用约定相关的错误。特别是,它涉及到谁拥有什么寄存器以及何时拥有。
在单步调试过程中,您会注意到这些错误是寄存器中的错误值,但可能不明白正确的方法应该是什么。
函数prologue保存了
$s0
、$a0
和$ra
。$s0
和$a0
中的一个是不必要的,但都没有被正确使用。让我们分析一下
n
,根据调用约定,它位于$a0
中RDemo
的顶部,该寄存器可以用于第一个系统调用以打印编号n
,但是,正如@Jim在上面指出的,打印空格将$a0
重新用作指向要打印的文本的指针,这样做会清除$a0
。因此n
的值至少从$a0
丢失。n
必须在打印空格后仍然存在--我们注意到函数序言确实存储了n
(从$a0
)到堆栈中,它将在系统调用打印该空间后幸存下来。因此,最简单的事情是在打印该空间后,从该堆栈位置重新加载它,并且在计算递归调用的X1 M16 N1 X之前。您会发现递归调用也会通过在
$a0
中传递n-1
来彻底破坏$a0
,因此在递归调用之后再次使用n
之前,您需要这个lw
。(将$a0
加1以恢复其原始值可能很诱人,但这将要求被调用方保留其参数寄存器,并且这不是调用约定的一部分,即,这违反了调用约定。)在这种情况下,
$s0
是完全不必要的,也是未使用的,因此不需要在序言/尾声中保存和恢复它。此外,虽然在序言中有必要保存
$a0
,但在结语中没有必要恢复它:即X1 M25 N1 X是X1 M26 N1 X的参数,根据调用约定的定义,该参数属于X1 M27 N1 X的该调用/激活而不是其调用者-退出时的恢复对调用者是有益的,但是在参数的情况下不是这样,因为这些参数被给予被调用者以按照他们的意愿来处理,并且调用者(应该)不期望接收回他们所提供的值这满足临时寄存器集的定义,即调用被破坏(并且有时被错误地标记为调用者保存)。作为另一种方案,我们可以使用调用保留寄存器,例如
$s0
,以在系统调用和其他调用之间保持n
活动。在这种情况下,函数入口时在$a0
中找到的n
应复制到$s0
中。作为函数序言的一部分(但是 * 在 * 将X1 M33 N1 X保存到堆栈之后,以便在退出时可以恢复X1 M34 N1 X的原始值)。从那时起,只要代码需要
n
,就可以在$s0
中找到它(因为你把它放在那里了),所以,例如,如果/当你想把它放回$a0
中时,就使用move $a0, $s0
。这个场景中的序言应该保存传入的
$s0
,并在结尾中恢复它,但不需要保存(或恢复)$a0
。我想指出的是,无论调用是递归的还是简单地调用一个替代例程,上面讨论的所有内容都适用,比如
RDemo
调用RDemo2
,RDemo2
调用RDemo3
...递归可能很难遵循,但是我们使用与任何函数调用相同的规则(调用约定的规则),换句话说,递归不添加额外的规则。当然,在编写小程序的时候,我们总是可以发明我们自己的调用约定。编译器有时候会在知道调用者和被调用者的内部细节的时候这样做。然而,如果你的目标是学习(1)标准的调用约定,(2)调用保留寄存器和调用重写寄存器之间的区别,那么就按照上面的逻辑和分析来做。
我们需要正确地将变量和临时变量分配到正确类型的寄存器(call-clobbed vs. call-preserved)的分析是live-variable analysis的一种形式。
具体来说,我们要看一个变量在函数调用中是否是活的,从技术上讲,这个分析要把变量的定义(对变量的赋值)和变量的使用(查询变量的值)匹配起来,如果一些使用是在调用之后,而到达的定义是在调用之前,那么这个变量在(函数)调用中是活的。
当一个变量在函数调用中不存在时,我们可以自由地使用暂存/调用重写寄存器,它们更适合于这样的变量,因为它们比调用保留寄存器具有更少的序言和尾声开销。
然而,如果一个变量在调用中是活的,那么它有特殊的要求,即它的值需要在函数调用保留存储中,要么在调用保留寄存器中,要么在局部堆栈内存中。在调用保留寄存器和局部堆栈内存之间做出一个好的选择与该变量的实际使用有关。
调用保留寄存器增加了序言和尾声的开销,因为它的原始值必须返回给调用者;然而,使用局部堆栈空间至少具有初始化的开销。
通常,当在涉及函数调用的循环语句中使用变量时,调用保留寄存器比堆栈更好,反之亦然:当变量不在循环语句中使用时,本地堆栈内存更好。
完整的分析取决于具体的环境,这些环境会根据实际代码和特定的动态负载而变化,因此我们可以计算指令和停顿(静态和/或动态近似),以比较给定变量的两种方法。编译器进行这种类型的分析是为了做出存储选择。