我正在嘗試將下面的 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
在 func 中,我根據 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,結果不正確,我不知道在哪里我的問題是,我應該在哪里查看以嘗試修復錯誤?
uj5u.com熱心網友回復:
如果您的意圖是遵循正確的呼叫約定,而不是創建自己的呼叫約定,那么您的暫存器分析就會關閉。呼叫約定定義了引數暫存器、回傳暫存器、呼叫保留暫存器和呼叫破壞暫存器;還回傳值傳遞和堆疊處理的一些要求。
在下文中,我在下面做一些注釋:
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:func至少在第 3 種情況下。這里t1可能被一個呼叫破壞是僅僅呼叫任何東西的一個特征——它不一定要求遞回被破壞,因為任何被呼叫者都可以在t1不保留它的情況下假設使用(根據呼叫約定的定義,它指定t1為 call-clobbered)。
在最簡單的方法中,第一次呼叫回傳的值func應該存盤在堆疊記憶體中(這將需要堆疊幀中的一個附加字)并在第二次呼叫func.
在另一種方法中,它可以存盤在一個sN暫存器中,但這仍然需要一個額外的堆疊幀槽,以及在序言和結尾添加一些保存和恢復。(堆疊記憶體被指定用于函式呼叫,而呼叫破壞的暫存器則不是。)sN如果變數的值被重復使用,例如當涉及到回圈時,暫存器是一個優勢,但是對于這里的情況,僅用于一種用途(值的消耗),一個簡單的堆疊槽是合適的。一旦保存(后來恢復)sN暫存器可以用作常規暫存器,因此可以使用另一種方法洗掉可能在回圈內找到的堆疊幀加載和存盤 - 這可以通過在函式序言和尾聲中放置加載和存盤來實作(每個函式只執行一次)呼叫)。
(2) 這里沒有必要重新加載ra暫存器,稍后它會立即被你自己的jal兩條指令破壞。
(3) 沒有充分的理由移動a1到另一個暫存器(例如t2),因為當需要來自第二次呼叫的回傳值時func(4 條指令之后),該值仍然可以在a1.
(4) 呼叫約定不需要恢復aN暫存器——呼叫者應該假設這些暫存器也被呼叫破壞,并且您的代碼正確地不依賴于它們在呼叫后是相同的。所以,這是不必要的,但無害的。
(5) 您選擇在 中回傳值a1,只要您一直這樣做/期望這樣做就可以了;但是,需要明確的是,這與標準呼叫約定相反,后者將回傳a0.
如何除錯呼叫其他函式的函式:
如果事情嚴重到被呼叫的函式沒有回傳到它被呼叫的地方,那么你必須單步執行,特別要記住ra暫存器以及它是如何作業的。
在回傳地址鏈接在暫存器中完成的機器上(RISC V、MIPS、ARM),在沒有保留回傳地址值的情況下完成的嵌套呼叫本身可能會成功,但呼叫者已經丟失了回傳地址,而是回傳到它自己的最后一個呼叫站點(因為那是被破壞的地方ra)而不是它的呼叫者。
其他時候,堆疊被弄亂了,所以不能正常回傳,所以我們要注意ra和sp。一旦呼叫和回傳作業正常,您就可以專注于其他與呼叫相關的問題,然后是演算法問題。
假設事情沒有太壞并且您呼叫的函式回傳:
觀察你的函式呼叫:
停在呼叫指令本身
jal( ),并注意sp值。在您的情況下t1,在情況 3 的第二次遞回呼叫之前是一個實時變數,因此您應該在此處記下該值。跳過正在呼叫的函式。如果在您的環境中可用,跨步的優點是它可以忽略進一步的遞回來跨整個呼叫。但是,在某些環境中,沒有單步除錯操作,因此您必須對其進行模擬,這可能意味著在呼叫后立即設定斷點(并執行運行或繼續),或者單步執行直到它回傳. 當它像跨步一樣回傳到正確的呼叫時,
sp應該具有與上述步驟中記錄的相同的值。回傳時驗證回傳值:
sp暫存器具有呼叫前記錄的值,對于暫存器中的實時變數也是如此。在您的情況下,您應該能夠觀察到,t1當輸入值大到足以在遞回中觸發多個案例 3 時,從案例 3 的第二次遞回呼叫回傳時會發生變化。
或者,如果您了解呼叫約定并且可以執行實時變數分析(定義的位置、使用的位置以及是否存在中間呼叫),您可以通過代碼審查發現呼叫約定違規。
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/459717.html
