# RISC-V Procedures > 資料來源:NCKU Jserv 教授開設之**計算機結構**課程-[RISC-V Procedures](https://docs.google.com/presentation/d/18F1vHY5Mv5E6WU6BvktXzkVbZrTZ3jQUbkWOjEPGE4I/edit#slide=id.p1)。 :::info 可回來查閱 **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 - 暫存器比記憶體快,因此應善加利用它們。 - `a0` – `a7` (`x10` - `x17`):**8 個參數暫存器**,用來傳遞參數及兩個 return value(`a0` - `a1`)。 - `ra`:**1 個 return address 暫存器**,用來回到到原點(`x1`)。 - 另外還有 `s0`-`s1` (`x8`-`x9`) 和 `s2`-`s11` (`x18`-`x27`): saved 暫存器(稍後會進一步解釋)。 ### RISC-V Function Call Instructions - **呼叫函式**:使用 jump and link 指令(`jal`)。 - 「link」的意思是建立一個位址或連結,指向呼叫的地方,讓函式能夠返回到正確的位址。 - 「jump」到指定位址,並同時將下一條指令的位址儲存到 `rd` 暫存器中: ```c jal rd, FunctionLabel ``` - 從**函式回傳**:使用 jump register 指令(`jr`)。 - 無條件跳躍到 `ra` 暫存器指定的位址: ```c jr ra ``` - 組譯器的簡寫:`ret = jr ra`。 小總結: 事實上,只有兩個指令: - `jal rd, Label` – **jump-and-link** `# rd = pc+4; pc += imm` - `jalr rd, rs, imm` – **jump-and-link register** `# rd = pc+4; pc = R[rs1]+imm` - 正如我們即將看到的,「天下沒有白吃的午餐」,因此可能**沒有足夠的位元**讓 Label 指向我們想跳躍的遠處。 - 使用 `jalr` 時,我們跳躍到暫存器 `rs` 的內容加上立即值 `imm`(就像基底指標加上偏移量),並且將 `rd` 設置為和 `jal` 相同的方式。 > 注意:`j`, `jr` 和 `ret` 是 pseudo-instructions! ### Stack > 在呼叫函數之前,需要一個地方來**保存舊的值**,返回時還原它們,並刪除這些值。 理想的選擇是 **Stack**:一種 LIFO 的佇列。 - Push:將資料放入 Stack。 - Pop:從 Stack 中取出資料。 Stack 位於記憶體中,因此需要一個暫存器指向它。 RISC-V 中的 Stack Pointer 為 `sp`(`x2`)。 慣例上,Stack 從高位址向低位址增長,執行 Push 時減少 `sp`,執行 Pop 時增加 `sp`。 Stack **frame** 包含: - Return「instruction」位址 - 參數(引數) - 其他區域變數的空間 stack frame 是連續的記憶體區塊, Stack Pointer 會**指示 stack frame 的底部位置**。 ![圖片](https://hackmd.io/_uploads/S1BzP6ekJe.png =30%x) 當程式執行完畢後,stack frame 會從 Stack 中移除,釋放記憶體以供未來的 stack frame 使用。 ## RISC-V Function Call Example ### C Code ```c 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 ```c 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 ![圖片](https://hackmd.io/_uploads/rkf3Oae1yx.png =80%x) ## Nested Calls and Register Conventions ### Nested Procedures ```c 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 - 呼叫者(Calle**R**):指發起呼叫的函式。 - 被呼叫者(Calle**E**):指被呼叫的函式。 > 當 Calle**E** 執行完畢 return 時,Calle**R** 需要知道哪些暫存器可能會被改變,哪些則保證不會被改變。 **暫存器慣例(Register Conventions)**:這是一組普遍接受的規則,說明在執行函式呼叫(`jal`)後,哪些暫存器會保持不變,哪些可能會被改變。 為了減少因暫存器溢出與恢復所造成的昂貴 load 和 store 操作,RISC-V 的 function-calling convention 將暫存器分為兩類: 1. 函式呼叫後,**保留的暫存器**: - 呼叫者可以依賴其值不變。 - `sp`(堆疊指標)、`gp`(全域指標)、`tp`(執行緒指標)。 - Saved registers:`s0 - s11`(`s0` 也作為 `fp`,即框架指標), 2. 函式呼叫後,**不保留的暫存器**: - 呼叫者無法依賴其值保持不變。 - Argument / Return registers:`a0 - a7`、`ra`(返回位址暫存器)。 - Temporary registers:`t0 - t6`。 ### RISC-V Symbolic Register Names ![圖片](https://hackmd.io/_uploads/rJlusagkJe.png =90%x) > **Register**:Numbers hardware understands。 > **ABI Name**:Human-friendly symbolic names in assembly code. ## RV32 指令 ![圖片](https://hackmd.io/_uploads/B1sknaeJ1e.png =70%x)