【问题标题】:How a recursive function works in MIPS?递归函数如何在 MIPS 中工作?
【发布时间】:2020-03-14 22:04:11
【问题描述】:

我是 MIPS 的新手(因为我开始为我的大学学习 MIPS 汇编)并且我在理解 MIPS 中的递归函数如何工作方面遇到了问题。

例如,我有这个程序(用 C 语言)用 MIPS 编写:

int fact (int n)
{
   if (n < 1) return 0;
   else return n * fact(n - 1);
}

谁能帮助我,用这个或另一个递归函数的例子,解释一下它是如何工作的?

【问题讨论】:

标签: function recursion assembly mips


【解决方案1】:

我想分享的第一件事是将其转换为 MIPS 的复杂性仅来自于函数调用的存在,而不是因为涉及递归——fact 是递归的,恕我直言是红色鲱鱼。为此,我将说明一个非递归函数,它具有您所说的递归函数的每一点复杂性:

int fact (int n)
{
   if (n < 1) return 0;
   else return n * other(n - 1);    // I've changed the call to "fact" to function "other"
}

我的改变不再是递归的!但是,此版本的 MIPS 代码看起来与您的 fact 的 MIPS 代码相同(当然,除了 jal fact 会更改 jal other)。这是为了说明翻译 this 的复杂性是由于函数内的调用,与被调用的对象无关。 (虽然 YMMV 具有优化技术。)


要理解函数调用,你需要理解:

  • 程序计数器:程序如何与程序计数器交互,尤其是在函数调用的上下文中。
  • 参数传递
  • 注册约定,一般

在 C 中,我们有明确的参数。当然,这些显式参数也出现在汇编/机器语言中——但也有在机器代码中传递的参数在 C 代码中不可见。例如返回地址值和堆栈指针。


这里需要的是对函数的分析(与递归无关):

参数n 将在函数入口的$a0 中。 n 的值在函数调用(到 other)之后是必需的,因为在函数调用返回 * 的右手操作数之前我们不能相乘。

因此,n* 的左侧操作数)必须在对 other 的函数调用中幸存下来,而在 $a0 中则不会——因为我们自己的代码将重新利用 $a0 来调用other(n-1),因为n-1 必须为此进入$a0

此外,(在 C 中,隐式)参数$ra 保存返回给调用者所需的返回地址值。同样,对other 的调用将重新利用$ra 寄存器,清除其先前的值。

因此,此函数(您的或我的)需要两个值才能在其主体内的函数调用(例如对 other 的调用)中继续存在。

解决方案很简单:我们需要的值(存在于寄存器中,这些值被我们正在做的事情或被调用者可能做的事情重新利用或清除)需要移动或复制到其他地方:在函数中存在的地方打电话。

内存可用于此目的,并且我们可以使用堆栈为这些目的获取一些内存。

基于此,我们需要在调用other 之后创建一个堆栈帧,该堆栈帧为我们需要的两件事(否则会被清除)留有空间。必须保存条目$ra(并稍后重新加载)以便我们使用它返回;此外,需要保存初始的n 值,以便我们可以将其用于乘法。 (堆栈帧通常在函数序言中创建,并在函数尾声中删除。)


正如机器代码(甚至一般的编程)中经常出现的情况,还有其他处理事物的方法,尽管要点是相同的。 (这是一件好事,优化编译器通常会根据特定情况寻求最佳方式。)


递归的存在或不存在不会改变我们需要将其翻译成汇编/机器语言的基本分析。递归极大地增加了堆栈溢出的可能性,但不会改变这种分析。


附录

需要明确的是,递归强制要求使用可动态扩展的调用堆栈——尽管所有现代计算机系统都提供了这样一个调用堆栈,因此在当今的系统上很容易忘记或掩盖这一要求。

对于没有递归的程序,调用堆栈不是必需的——局部变量可以分配给函数私有的全局变量(包括返回地址),这是在某些旧系统上完成的,比如 PDP-8,它确实做到了不为调用堆栈提供特定的硬件支持。


使用堆栈内存来传递参数和/或寄存器不足的系统可能不需要此答案中描述的分析,因为变量已经存储在嵌套函数调用的内存中。

现代寄存器丰富的机器上的寄存器分区创造了上述分析的要求。这些寄存器丰富的机器(大部分)在 CPU 寄存器中传递参数和返回值,这很有效,但有时需要进行复制,因为寄存器从一个函数重新用于另一个函数。

【讨论】:

    【解决方案2】:

    实现您描述的功能的一种方法是使用addi 分配内存来移动堆栈指针以分配(在开始时)和释放(在结束时)一些堆栈空间。然后sw 指令可以将寄存器保存到该空间中。通话后和/或准备返回时,使用lw 恢复它们。所以我们可以从这条指令开始分配一些内存:

    addi $sp, $sp, -8 在 $sp 寄存器中,我们求和 -8

    也就是说,我们需要 8 个字节,$ra 返回需要 4 个字节,int n 需要 4 个字节。现在,我们按以下方式分配:

    sw $a0, 4($sp) #we are saving the int with register $a0 in position 4
    sw $ra, 0($sp) #we are saving the return address with address $ra in position 0
    

    现在,我们需要一个临时变量来存储上面比较中的 1。然后我们有:

    addi $t0, $0, 2 在 $t0 寄存器中,我们将 2 与 $0 相加

    现在比较操作数是 slt,在我们的例子中:

    slt $t0, $a0, $t0在$t0寄存器中,我们比较$a0寄存器中包含的值和$t0寄存器中的值,如果$t0为1,否则为0

    对于如果$t0为零,我们需要有如下跳转结构(注意else是一个标签,这是一个按照规则要遵循的结构): obs.: $0 用于存储零

    beq $t0, $0, $t0, else 在 $t0 中我们查看它是否为零,如果是,我们继续我们的程序,如果不是,我们转到另一条指令,这是,否则。

    继续,我们现在必须返回 0,如下所示:

    `addi $v0, $0, 0

    最后我们必须恢复我们非常了解的堆栈。

    对于其他标签,我们从需要 n 变为 n-1 的概念开始,以下列方式:

    `addi $a0, $a0, -1 #这是,我们将$a0和-1添加到$a0

    我们必须使用 jal fact,因为很明显我们有一个递归。

    下一步是恢复我们知道的 return raint n 的地址,以及堆栈。 很明显,我们有一个乘法,对于这个主题,我们将应用下一条指令:

    `mul $v0, $a0, $v0 #这是,我们将 $a0 与 $v0 相乘,记住 v0 存储 fact(n-1)

    `mul $v0, $a0, $v0 #multiplies n and fact(n-1)

    我们必须记住,必须使用 jr $ra 才能返回。

    希望,我已经清除了一点。

    【讨论】:

    • swlwallocate 和释放空间,这是通过将 $spaddiu 与负或正常数一起移动来完成的。 swlw 使用该空间来保存和恢复该空间中的寄存器,正如您在第一段后解释的那样。
    • 你说得对,我同意,这只是我写的更直观的方式。
    • 在我看来,对于一般情况,它更有可能产生误导或混淆。我建议您编辑以更改它。
    • 你觉得它更直接一点吗?
    • 我指的是第一段的文本,而不是第一个代码块。我按照我的意思进行了编辑。如果您认为我的编辑没问题,您可能需要再次编辑以将其更多地表达为您自己的话。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-02-24
    • 1970-01-01
    • 2019-03-05
    • 2021-07-22
    • 2014-04-25
    • 2018-01-23
    • 2011-03-29
    相关资源
    最近更新 更多