assembly 如何在QtSpim/MIPS中编写下面的递归序列

cfh9epnr  于 2023-03-08  发布在  其他
关注(0)|答案(1)|浏览(133)

我正尝试在MIPS/QtSpim中编写以下序列:
a(n)= a(n-1)+2a(n-2)
其中a(0)= a(1)= 1
这段代码应该提示用户输入n的值,并在控制台中显示结果。2我需要使用递归和嵌套过程链接来实现这段代码。
我试过下面的代码,但是如果输入n大于2,我总是遇到错误:

.data
a0: .word 1
a1: .word 1
n: .word 0
an: .word 0

.text
.globl main

main:
    # Prompt user to enter the value of n
    li $v0, 4
    la $a0, prompt
    syscall

    # Read the value of n from the user
    li $v0, 5
    syscall
    move $s0, $v0

    # Call the sequence function
    move $a0, $s0
    jal sequence

    # Display the result
    li $v0, 1
    lw $a0, an
    syscall

    # Exit program
    li $v0, 10
    syscall

sequence:
    addi $sp, $sp, -4
    sw $ra, 0($sp)
    beq $a0, 0, a0_case
    beq $a0, 1, a1_case
    addi $a0, $a0, -1
    jal sequence
    lw $t0, an
    addi $a0, $a0, -1
    jal sequence
    lw $t1, an
    add $v0, $t0, $t1
    sll $t1, $t1, 1
    add $t0, $t0, $t1
    sw $t0, an
    j end

a0_case:
    li $v0, 1
    sw $v0, an
    j end

a1_case:
    li $v0, 1
    sw $v0, an

end:
    lw $ra, 0($sp)
    addi $sp, $sp, 4
    jr $ra

.data
prompt: .asciiz "Enter the value of n: "

.text
mum43rcc

mum43rcc1#

该代码未遵循调用约定。您是要使用标准调用约定还是要创建自己的调用约定?
在任何情况下,寄存器都是混淆的,递归调用是对属于递归调用者的值的乱码。不用说,一个CPU寄存器一次只能保存一个值,所以,如果/当重新使用寄存器来保存一个不同的变量时,那么前一个变量的值就会丢失。

  • $a0不是一个安全的地方,因为每个递归调用都会修改寄存器,在第一个递归调用之后需要这个参数来进行第二个这样的调用,但是它已经被第一个递归调用清除了。
  • $t0对于临时数据来说也不是一个安全的地方,因为它在第二次递归调用之后就无法保存,因为广义地说,该函数还修改了$t0
jal sequence
    lw $t0, an        # t0, assigned here, will be wiped out by the next jal
                      # if the recursion is deep enough
    addi $a0, $a0, -1
    jal sequence
    lw $t1, an
    add $v0, $t0, $t1 # but this code expects it to have a meaningful value 
                      # e.g. as per the above assignment!
                      # but it doesn't since the recursive call 
                      # also used $t0 for its own purposes.
  • 使用全局变量an: .word 0来存储递归函数的返回值是不标准的,而且容易出错(虽然在这种特殊情况下可能有效)。返回值应该只在$v0中返回。广义上讲,全局变量不是线程安全的,不是可重入的,也不是递归友好的,因此根据调用约定使用CPU寄存器和调用堆栈,而不是使用全局变量。
  • 下面的代码序列设置了$v0an,但是设置的值不同。我没有看到任何地方使用了$v0,所以add指令没有任何意义。
lw $t1, an
     add $v0, $t0, $t1
     sll $t1, $t1, 1
     add $t0, $t0, $t1
     sw $t0, an
     j end

所以,你需要做的是:(a)提供并使用将在最初$a0中对n的函数调用之后继续存在的存储,并且还提供并使用用于第一递归调用的临时结果的存储(* 注:$ra * 已经存在)。(B)删除an全局变量,并将$v0专门用于返回值,以及(c)修复拼写错误。
让我们看看$ra寄存器,它在代码中得到了正确的使用。
$ra寄存器是做什么用的?它保存返回地址,该地址是在调用时创建的(例如通过jal)。它是一个参数,对高级语言隐藏,提供给被调用方,以便它知道动态返回到哪个调用方位置,也就是说,不管这次是谁进行调用。在旧的术语中,如果被调用方是简单的并且由于任何其它原因不需要$ra寄存器,则它将把调用方提供的返回地址留在$ra寄存器中,并且从那里使用它来返回到适当的调用方-然而,如果/当被调用方也进行函数调用时,这必然会重新调整$ra寄存器的用途(对于下一级调用),在最终返回时需要其原始值。因此,该方法是将$ra的入口值保存到堆栈本地存储器-堆栈本地存储器保存函数调用。从堆栈存储器恢复返回地址以便返回到调用者。从技术上讲,存储在内存中的值不必恢复到$ra寄存器,因为任何寄存器都将用作jr ..指令的操作数;然而,如果使用X1 M23 N1 X寄存器,则其可帮助某些硬件进行分支预测,且由于其在返回点处可用,因此通常是这样做的(但调用者不期望X1 M24 N1 X在从调用返回时保持任何有意义的值)。
因此寄存器X1 M25 N1 X用于存储分配模式&初始值存储在序言中并在结尾中恢复,尽管恢复的目的是使其能够立即使用而不是作为返回给调用者的益处。
你的变量n和两个函数调用结果相加的临时变量,可以用同样的方法处理。为两个字分配足够的存储空间,并从$a0初始化函数序言中存储的那些字中的一个。这意味着n的值现在既在$a0中又在栈本地存储器中。您可以使用$a0副本为第一个递归调用提供n-1。暂时不初始化在序言中分配的另一个字,因为在第一个递归调用返回之前不需要它。当第一个递归调用完成时,存储其返回值(从$v0)转移到堆栈局部存储器的该另一个新的字中,然后也从其堆栈局部存储器重新加载原始n值,并为第二递归调用计算n-2。从堆栈本地内存重新加载第一次调用的临时结果,并根据公式将其添加到第二次递归调用的$v0结果中。

聪明的读者可能会意识到,保存原始n和保存第一次调用的返回值实际上可以共享同一个内存位置(将递归使用的堆栈空间减少1/3),尽管加载n和保存$v0的顺序必须小心地交换以共享该内存位置。
此外,我还要指出,调用约定的复杂性对于任何进行函数调用的函数来说都是一样的,无论是否涉及递归--这些规则都是为了让普通函数共享快速CPU寄存器--只要系统有函数调用堆栈,递归就不会给调用约定增加额外的规则。
最后,我会指出标准的调用约定是为了允许一个函数调用另一个函数,而调用者不知道被调用者的实现细节,也不会发生寄存器冲突。这有利于单独编译,以及函数指针作为参数和在v表中。
然而,当调用方和被调用方的实现都可以提前知道,并且有时汇编程序员和编译器进行这些修改时,自定义调用约定可以改进函数调用。

相关问题