RISC-V Procedures

資料來源:NCKU Jserv 教授開設之計算機結構課程-RISC-V Procedures

可回來查閱 RISC-V Symbolic Register Names

RISC-V Function Call

Six Fundamental Steps in Calling a Function

  1. arguments 放在函式可以存取的位置。
  2. 將「控制權」轉交給函式。
  3. 取得函式所需的(本地)儲存資源。
  4. 執行函式所需的任務。
  5. return value 放在呼叫程式碼可以存取的位置,並恢復使用過的暫存器;釋放本地儲存資源。
  6. 將「控制權」交回給原點,因為一個函式可能從程式的多個點被呼叫。

RISC-V Function Call Conventions

  • 暫存器比記憶體快,因此應善加利用它們。
  • a0a7 (x10 - x17):8 個參數暫存器,用來傳遞參數及兩個 return value(a0 - a1)。
  • ra1 個 return address 暫存器,用來回到到原點(x1)。
  • 另外還有 s0-s1 (x8-x9) 和 s2-s11 (x18-x27): saved 暫存器(稍後會進一步解釋)。

RISC-V Function Call Instructions

  • 呼叫函式:使用 jump and link 指令(jal)。
    • 「link」的意思是建立一個位址或連結,指向呼叫的地方,讓函式能夠返回到正確的位址。
    • 「jump」到指定位址,並同時將下一條指令的位址儲存到 rd 暫存器中:
      ​​​​​​​​jal rd, FunctionLabel
      
  • 函式回傳:使用 jump register 指令(jr)。
    • 無條件跳躍到 ra 暫存器指定的位址:
      ​​​​​​​​jr ra
      
    • 組譯器的簡寫:ret = jr ra

小總結:

事實上,只有兩個指令:

  • jal rd, Labeljump-and-link # rd = pc+4; pc += imm
  • jalr rd, rs, immjump-and-link register # rd = pc+4; pc = R[rs1]+imm
    • 正如我們即將看到的,「天下沒有白吃的午餐」,因此可能沒有足夠的位元讓 Label 指向我們想跳躍的遠處。
    • 使用 jalr 時,我們跳躍到暫存器 rs 的內容加上立即值 imm(就像基底指標加上偏移量),並且將 rd 設置為和 jal 相同的方式。

注意:j, jrret 是 pseudo-instructions!

Stack

在呼叫函數之前,需要一個地方來保存舊的值,返回時還原它們,並刪除這些值。

理想的選擇是 Stack:一種 LIFO 的佇列。

  • Push:將資料放入 Stack。
  • Pop:從 Stack 中取出資料。

Stack 位於記憶體中,因此需要一個暫存器指向它。
RISC-V 中的 Stack Pointer 為 spx2)。
慣例上,Stack 從高位址向低位址增長,執行 Push 時減少 sp,執行 Pop 時增加 sp

Stack frame 包含:

  • Return「instruction」位址
  • 參數(引數)
  • 其他區域變數的空間

stack frame 是連續的記憶體區塊, Stack Pointer 會指示 stack frame 的底部位置
圖片

當程式執行完畢後,stack frame 會從 Stack 中移除,釋放記憶體以供未來的 stack frame 使用。

RISC-V Function Call Example

C Code

int Leaf (int g, int h, int i, int j)
{
	int f;
	f = (g + h)(i + j);
	return f;
}

Parameter variables g, h, i, and j in argument registers a0, a1, a2, and a3, and f in s0.
Assume need one temporary register s1.

Assembly Code

Leaf: 	addi sp,sp,-8  # adjust stack for 2 items
	sw s1, 4(sp)   # save s1 for use afterwards
      	sw s0, 0(sp)   # save s0 for use afterwards

      	add s0,a0,a1   # f = g + h
      	add s1,a2,a3   # s1 = i + j
      	sub a0,s0,s1   # return value (g + h)(i + j)

      	lw s0, 0(sp)   # restore register s0 for caller 
      	lw s1, 4(sp)   # restore register s1 for caller
      	addi sp,sp,8   # adjust stack to delete 2 items
      	jr ra 	       # jump back to calling routine

Stack Before, During, After Function

圖片

Nested Calls and Register Conventions

Nested Procedures

int sumSquare(int x, int y) {
    return mult(x, x) + y;
}

有個名為 sumSquare 的函式,現在 sumSquare 呼叫了 mult 函式。
此時,sumSquare 需要 return 的 addr. 儲存在 ra 中,但在呼叫 mult 時,ra 會被覆寫。

因此,在呼叫 mult 之前,必須先將 sumSquare 的 return address 存放到 Stack 中,以避免被覆寫。

Register Conventions

  • 呼叫者(CalleR):指發起呼叫的函式。
  • 被呼叫者(CalleE):指被呼叫的函式。

當 CalleE 執行完畢 return 時,CalleR 需要知道哪些暫存器可能會被改變,哪些則保證不會被改變。

暫存器慣例(Register Conventions):這是一組普遍接受的規則,說明在執行函式呼叫(jal)後,哪些暫存器會保持不變,哪些可能會被改變。

為了減少因暫存器溢出與恢復所造成的昂貴 load 和 store 操作,RISC-V 的 function-calling convention 將暫存器分為兩類:

  1. 函式呼叫後,保留的暫存器
    • 呼叫者可以依賴其值不變。
    • sp(堆疊指標)、gp(全域指標)、tp(執行緒指標)。
    • Saved registers:s0 - s11s0 也作為 fp,即框架指標),
  2. 函式呼叫後,不保留的暫存器
    • 呼叫者無法依賴其值保持不變。
    • Argument / Return registers:a0 - a7ra(返回位址暫存器)。
    • Temporary registers:t0 - t6

RISC-V Symbolic Register Names

圖片

Register:Numbers hardware understands。
ABI Name:Human-friendly symbolic names in assembly code.

RV32 指令

圖片