我正在尝试将下面的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
所指的位置),而不是它的调用者。
其他时候,堆栈会变得一团糟,因此无法正常返回,所以我们要注意ra
和sp
。一旦呼叫和返回工作正常,您可以专注于其他与呼叫相关的问题,然后是算法问题。
假设事情没有太坏,并且您调用的函数返回:
注意您的函数调用:
在调用指令本身(
jal
)处停止,在结束之前,记下保存活动变量的寄存器中的值(例如,将在调用后使用的值),验证传递的参数是否正确(例如,arg寄存器),并记下sp
值。在您的案例中,t1
在案例3中的第二次递归调用之前是一个活动变量,因此您应该在这里记下该值。将转移到正在调用的函数上。如果在您的环境中可用的话,step-over的优点是它可以忽略进一步的递归来step-over整个调用。然而,在某些环境中,调试操作没有跨步骤,因此必须对其进行模拟,这可能意味着在调用后立即设置一个断点(并执行run或continue),或者在调用返回之前只执行一步。当它返回到正确的调用时,就像步骤结束一样,
sp
应该具有上面步骤中提到的相同值。返回后,验证返回值:
sp
寄存器是否具有调用前记录的值,寄存器中的活动变量也是如此。在您的案例中,您应该能够观察到,当输入值大到足以在递归中触发多个案例3时,t1
在从案例3的第二次递归调用返回时发生了更改。
或者,如果您了解调用约定,并且可以执行实时变量分析(定义的位置、使用的位置以及是否存在干预调用),则可以通过代码审查发现违反调用约定的情况。