RISC-V递归函数调试



我正在尝试将下面的C++代码转换为RISC-V,尽管代码有效,但结果不正确,我无法解决问题。

用C++编写递归函数

int func(int x)
{
if(x>20)                             //case1
return 2*x+func(x/5);
else if (10<x&&x<=20)                //case2
return func(x-2)+func(x-3);      
else if (1<x&&x<=10)                 //case3
return func(x-1)+func(x-2);
else if (x==0)                       //case4
return 1;
else if(x==1)                        //case5
return 5;
else                                 //case6
return -1;
}

我的RISC-V代码的一部分

main:
li a7,5            #get input and store in register a0
ecall
jal ra,func        #jump to func
addi a0,a1,0       #move return value of func to a0
li a7,1            #print integer in a0
ecall
func:
addi sp,sp,-8
sw ra,4(sp)
sw a0,0(sp)
blt a0,zero,case6 #otherwise
beq a0,zero,case4 #x=0
addi t0,zero,1  #t0=1
beq a0,t0,case5 #x=1
addi t0,t0,9    #t0=10
ble a0,t0,case3 #1<x<=10
addi t0,t0,10   #t0=20
ble a0,t0,case2 #10<x<=20
beq zero,zero,case1 #x>20

在函数中,我根据a0(表示x)的值将它们分为六种情况,让它们分支到不同的标签。

以其中两个标签为例

case3:
addi a0,a0,-1          #x-1
jal ra,func            #func(x-1)
addi t1,a1,0           #move result of func(x-1) to t1
lw a0,0(sp)            #load orginal x
lw ra,4(sp)            #load orginal return address
addi a0,a0,-2          #x-2
jal ra,func            #func(x-2)
addi t2,a1,0           #move result of func(x-2) to t2
lw a0,0(sp)            #load orginal x
lw ra,4(sp)            #load orginal return address
addi sp,sp,8           #pop up the stack
add a1,t1,t2           #return value a1 = func(x-1)+ func(x-2)
jalr zero,0(ra)
case4:
addi a1,zero,1         #return result 1 in a1
addi sp,sp,8
jalr zero,0(ra)

在计算之后,我发现输入是0还是1或者<0,结果是正确的,我认为是因为它们是叶过程,但如果我的输入大于3,结果是不正确的,所以我不知道我的问题在哪里,我应该在哪里查找并修复错误?

如果您的意图是遵循正确的调用约定,而不是创建自己的,则禁用寄存器分析。调用约定定义参数寄存器、返回寄存器、调用保留寄存器和调用阻塞寄存器;还返回值传递和堆栈处理的一些要求。

在下面我做了一些注释:

addi a0,a0,-1          #x-1
jal ra,func            #func(x-1)
addi t1,a1,0           #move result of func(x-1) to t1 ******(1)
lw a0,0(sp)            #load orginal x
lw ra,4(sp)            #load orginal return address    ******(2)
addi a0,a0,-2          #x-2
jal ra,func            #func(x-2)
addi t2,a1,0           #move result of func(x-2) to t2 ******(3)
lw a0,0(sp)            #load orginal x                 ******(4)
lw ra,4(sp)            #load orginal return address
addi sp,sp,8           #pop up the stack
add a1,t1,t2           #return value a1 = func(x-1)+ func(x-2) ******(5)
jalr zero,0(ra)

(1)t1是一个调用失败的寄存器,因此不能保证它能在后续调用中幸存下来——因为它恰好是一个递归调用,我们知道谁可能会破坏t1:至少在情况3中是func。这里,t1可能被调用破坏是仅仅调用任何东西的一个特性——它不一定需要递归被破坏,因为任何被调用方都可以假设使用t1而不保留它(根据调用约定的定义,将t1指定为调用被破坏)。

在最简单的方法中,从对func的第一次调用返回的值应该存储在堆栈内存中(这将需要在堆栈帧中添加一个字),并在对func的第二次调用之后重新加载。

在另一种方法中,它可以存储在sN寄存器中,但这仍然需要额外的堆栈帧槽,加上一些保存&还原添加到序言和尾声中。(堆栈内存被指定为可以在函数调用中幸存下来,而被调用失败的寄存器则不然。)如果变量的值被重复使用,sN寄存器是一个优势,例如,当涉及循环时,但像这里的情况一样,只有一种使用(值的消耗),一个简单的堆栈槽是合适的。一旦保留(稍后恢复)sN寄存器就可以用作常规寄存器,因此可以移除堆栈帧负载&可以使用另一种方法在循环中找到的存储——这是通过将load&函数序言和尾声中的存储(每个函数的调用只执行一次)。

(2) 在这里重新加载ra寄存器是没有意义的,两条指令后它将立即被您自己的jal所破坏。

(3) 没有充分的理由将a1移到另一个寄存器(例如t2),因为当需要从第二次调用到func的返回值(4条指令之后)时,该值在a1中仍然是可用的。

(4) 调用约定不需要恢复aN寄存器——调用者应该假设这些寄存器也被调用过,并且您的代码正确地不依赖于它们在调用后是相同的。所以,这是不必要的,但无害。

(5) 您选择在a1中返回值,只要您一直在这样做/期望这样做,这是可以的;然而,需要明确的是,这与标准调用约定相反,后者将在a0中返回值。


如何调试调用其他函数的函数:

如果事情太糟糕了,被调用的函数无法返回到它被调用的位置,那么您必须单步执行,特别要记住ra寄存器以及它是如何工作的。

在一台返回地址链接在寄存器(RISC V、MIPS、ARM)中完成的机器上,在没有保留返回地址值的情况下完成的嵌套调用本身可能会成功,但调用者已经丢失了返回地址,而是将返回到自己的最后一个调用站点(这是被破坏的ra所指的位置),而不是它的调用者。

其他时候,堆栈会变得一团糟,因此无法正常返回,所以我们要注意rasp。一旦呼叫和返回工作正常,您可以专注于其他与呼叫相关的问题,然后是算法问题。

假设事情没有太坏,并且您调用的函数返回:

注意您的函数调用:

  1. 在调用指令本身(jal)处停止,在结束之前,记下保存活动变量的寄存器中的值(例如,将在调用后使用的值),验证传递的参数是否正确(例如,arg寄存器),并记下sp值。在您的案例中,t1在案例3中的第二次递归调用之前是一个活动变量,因此您应该在这里记下该值。

  2. 转移到正在调用的函数上。如果在您的环境中可用的话,step-over的优点是它可以忽略进一步的递归来step-over整个调用。然而,在某些环境中,调试操作没有跨步骤,因此必须对其进行模拟,这可能意味着在调用后立即设置一个断点(并执行run或continue),或者在调用返回之前只执行一步。当它返回到正确的调用时,就像步骤结束一样,sp应该具有上面步骤中提到的相同值。

  3. 返回后,验证返回值:sp寄存器是否具有调用前记录的值,寄存器中的活动变量也是如此。在您的案例中,您应该能够观察到,当输入值大到足以在递归中触发多个案例3时,t1在从案例3的第二次递归调用返回时发生了更改。


或者,如果您了解调用约定,并且可以执行实时变量分析(定义的位置、使用的位置以及是否存在干预调用),则可以通过代码审查发现违反调用约定的情况。

最新更新