# RISC-V Instructions
> 資料來源:NCKU Jserv 教授開設之**計算機結構**課程-[RISC-V Instructions](https://docs.google.com/presentation/d/1qNVK8ULddo6luq0Rrjj_bmOShhcx9hlESN36AK91n6I/edit#slide=id.g1243db8a3f7_0_23)。
>
> 更詳細閱讀,可參照 [RISC-V Assembly Programmer’s Manual](https://github.com/riscv-non-isa/riscv-asm-manual/blob/main/src/asm-manual.adoc) 及 [The RISC-V Instruction Set Manual: Volume I: Unprivileged ISA](https://riscv.org/wp-content/uploads/2019/06/riscv-spec.pdf)。
:::info
大部分都學過,特別標記:
- RISC-V 提供 32 個整數暫存器(`x0`:必 0;`x1`-`x31`:可用)。
- **Addition / Subtraction**:`add` / `sub` / `addi`。
- **Bitwise**:`and` / `or` / `xor` / `andi` / `ori` / `xori`。
- **Shifts**:`sll` / `srl` / `sra` / `slli` / `srli` / `srai`。
- **Set Less Than**:`slt` / `sltu` / `slti` / `sltiu`。
- **Load**:`lw` / `lb` / `lh`。
- **Store**:`sw` / `sb` / `sh`。
- **Branch**:`beq` / `bne` / `blt` / `bge` / `bltu` / `bgeu`。
- **Jump**:
- `jal x1, Label`。
- `jalr x1, x5, 0`。
- `j Label`。
- `jr x1`。
:::
## Introduction to Assembly Languages
### Building a Computer from the Ground Up
> 最終,我們所寫的任何程式都需要在電路上運行才能發揮作用。
然而,Circuit-level programming 具有高度限制性。
- 使用 C 語言時,某人設計了這種程式語言,因此那個人可以完全控制允許的 operators ,並可以選擇如何定義這些 operators 以達到實用性:**為了「反映語言的需求」,基本元件被定義出來**。
- 一旦進入電路層級,我們使用的每個基本元件都是一塊「能夠以可預測且有用方式運作」的矽片:**為了「反映可用的元件」,語言需要被定義**。
早期的電腦基本上需要為每個想要運行的程式重新構建,因為不同的計算需要不同的電路。計算機科學的一大進步是軟體語言的創造。
與其為每個問題製造一個新的電路,不如製造一個能解決「執行以二進制資料形式儲存的一系列指令」問題的電路(稱為 CPU)。然後透過撰寫二進制資料指令來解決問題。
### Assembly Language
我們不希望在建造完成後改變 CPU,因此在設計 CPU 時,需要決定一組由 CPU 支援的特定指令集,並確定將每個指令轉換為二進位形式的方法。
不同的 CPU 實現不同的指令集。一個特定 CPU 所實現的指令集稱為**指令集架構(Instruction Set Architecture, ISA)**,由 ISA 定義的程式語言通常被稱為組合語言。
> 例如,Arm(行動裝置)、Intel x86(Core i9、i7、i5、i3)、IBM/Motorola PowerPC(舊款 Mac 電腦)、MIPS/LoongArch、RISC-V 等等。
C 一般被認為是比 Java 或 Python 更「低階」的語言,因為它與底層 CPU 更「接近」。
(語言自動化的部分較少,因此運行速度更快,但需要管理更多細節。)
然而,最終 C 仍被視為高階語言,因為有很多事情已為我們完成:
- C 允許你在一行程式碼中撰寫多個操作,並會為你分解執行。
- C 為你設置了 stack,並記錄了它存儲區域變數的位置。
- C 允許你直接呼叫函數,並且你可以預期呼叫函數不會影響你擁有的任何區域變數。
- C 允許你命名變數,並且會記錄該名稱,甚至記錄該名稱所對應的變數類型。
當你開始使用組合語言時,幾乎所有操作都是程式設計師的 **explicit instruction** 結果。
### Instruction Set Architectures
早期的指令集架構(ISA)設計趨勢,是為新型 CPU 添加越來越多的指令,以執行複雜的操作。(例如,VAX 架構中,甚至有一條用於「多項式相乘」的指令!)
RISC 的理念(Cocke IBM, Patterson, Hennessy, 1980 年代)-**Reduced Instruction Set Computing**:
- 保持指令集的精簡、簡單。
- 讓軟體透過「組合簡單的指令」來完成複雜的操作。
- 較簡單的 CPU 更容易進行迭代(允許更快速的開發),且通常比複雜的 CPU 運行得更快(我們通常受限於實現的「最慢指令」)。
### RISC-V
為何要學 RISC-V?
- RISC-V 相對簡單,因為其基本指令集中只有少數幾條指令,且指令本身遵循一致的格式。
- x86 是較為流行的語言(大多數筆記型電腦/桌上型電腦的基本 CPU),但它是 CISC 語言,並使用 Huffman 編碼來壓縮指令。
- RISC-V 相對流行,開源且越來越受歡迎。
- RISC-V 於 2010 年在加州伯克利大學發明。
> Jserv 的反白:~~If we teach students RISC-V, they’re more likely to use a RISC-V architecture in the future, thus allowing RISC-V to keep growing in popularity.~~
## RISC-V Programming Paradigm
一個 RISC-V 系統由兩個主要部分組成:
1. CPU,負責運算。
2. 主記憶體,負責長期資料存儲。
CPU 被設計得極為快速,通常能在每奈秒內完成一條指令,甚至更快。
> 注意:光在 1 奈秒內能傳播 30 公分;換句話說,光從我的筆電一端傳到另一端的時間,比 CPU 完成一條指令所需的時間還要長。
- 「存取主記憶體」通常需要數百甚至數千倍的時間。
- CPU 可以透過「暫存器」儲存少量記憶體。
### Registers
暫存器是專門設計來儲存少量資料的 CPU 元件。每個暫存器可以儲存 32 位元的資料(在 32 位元系統中)或 64 位元的資料(在 64 位元系統中)。
> 在這門課程中,我們只考慮 RV32(使用 32 位元暫存器)。
這些資料純粹是二進制的;在組合語言層級並不存在資料型態,因此如果我們使用暫存器來儲存某些東西,程式設計師需要自己記住該暫存器及其預期用途。
暫存器是硬體元件,因此**一旦製造了 CPU,就無法更改可用的暫存器數量**。
而 RISC-V 提供 **32 個整數暫存器**供使用。
附註(暫存器位於處理器內部):

- 暫存器編號從 0 到 31,並以數字來表示:`x0` – `x31`。
- 暫存器 `x0` 是特殊的,始終儲存 `0`(嘗試向該暫存器寫入資料將會被忽略)。因此,我們有 **31 個可用於資料儲存的暫存器**。
- 其餘的 31 個暫存器在行為上完全相同;不同暫存器之間唯一的區別是「我們在使用它們時所遵循的約定」。
- 稍後我們將給它們命名,以提示哪些約定應用於哪些暫存器。
### Instructions
- 每行 RISC-V 程式碼是一條指令,執行對暫存器的簡單操作。
> ⇒ 可查看 rv32emu 的 source code(*src/rv32_template.c*)。
- 指令通常以以下格式編寫:
- **`<instruction name> <destination register> <operands>`**
- 例如,`add x5, x6, x7` :將 x6 和 x7 中儲存的值相加,並將結果儲存到 x5 中。
- 在暫存器之間可以添加逗號(`add x5,x6,x7`),但這是可選的。
- **註解**使用 `#` 符號編寫。
- 在一行中的 `#` 之後的任何內容都會被忽略,其與 Python 的註解語法類似。
- 重要的是:**在 RISC-V 中,註解比其他語言更為重要**。在較高階的語言中,有時可以通過選擇變數名稱使程式碼具有自我說明性,但在 RISC-V 中,我們沒有變數名稱!
- 沒有註解的 RISC-V 程式碼幾乎無法正確除錯。如果不對程式碼進行註解,我們可能無法在辦公時間協助除錯。
#### Addition and Subtraction of Integers
加法例子:

減法例子:

結合 1:

> 注意:一行 C 程式碼可能會分解成多行 RISC-V 程式碼。
結合 2:

> 注意:通過這種方式使用 `x5` 和 `x6`,我們會覆蓋這些暫存器中原本的任何資料。
因此,我們通常會保留一些暫存器為空(即不包含任何重要資料),以協助進行「intermediate calculations」。
### Pseudo-instructions
我們的 ISA 中的每條指令都需要一些電路,因此我們希望盡可能地簡化 ISA。
然而,有一些指令非常常見,值得為它們提供簡寫,但這些指令可以使用其他指令來實現。
在這種情況下,我們提供指令的名稱,但在早期的組合器中,它們會被轉換為常規指令。
例如,`mv x5, x6` 是一條 pseudo-instruction,代表「將 x5 設置為 x6」。
等效的 normal instruction 是:`add x5, x6, x0`。
### Immediates
Immediates 是數值常數。
它們在程式碼中經常出現,因此有針對它們的特殊指令。
**Add Immediate**:
- `addi x3, x4, 10`(在 RISC-V 中)
- `f = g + 10`(在 C 中)
- 其中 RISC-V 的暫存器 `x3` 和 `x4` 對應到 C 變數 `f` 和 `g`。
> 語法與 `add` 相似,不同之處在於「最後的參數是常數,而不是暫存器」。
在 RISC-V 中**沒有 Subtract Immediate 指令**,
這樣可以將操作的類型限制到絕對最低限度:
- 如果某個操作可以被分解為更簡單的操作,就不應該包含它。
- `addi …, -X` = `subi …, X` ⇒ 因此沒有 `subi`。
- 例如:
`addi x3, x4, -10`(在 RISC-V 中)
`f = g - 10`(在 C 中)
### Register Zero
一個特定的立即數,數字零(0),在程式碼中出現得非常頻繁。
因此,**Register Zero(`x0`)被「hard-wired」為值 0**,
例如:
- `add x3, x4, x0`(在 RISC-V 中)
- `f = g`(在 C 中)
這是在硬體中定義的,因此指令 `add x0, x3, x4` 將不會執行任何操作!
## List of RISC-V Instructions
### Arithmetic Operators
基本的 RISC-V 指令集提供以下操作:
**Addition / Subtraction**:`add` / `sub` / `addi`。
**Bitwise**:`and` / `or` / `xor` / `andi` / `ori` / `xori`。
**Shifts**:`sll` / `srl` / `sra` / `slli` / `srli` / `srai`。
**Set Less Than**:`slt` / `sltu` / `slti` / `sltiu`。
> 注意:Multiplication / Division 不在基本指令集中,因其需要 $O(n^2)$ 的電路元件,因此它被放入可選擇的擴展中。
#### Shifts
##### Arithmetic Shifts vs. Logical Shifts
邏輯左移:將所有位元向左移動,根據需要附加零。
邏輯右移:將所有位元向右移動,根據需要添加零。
回顧:在處理 unsigned num 時,這分別等同於乘以 2 的某次方和除以 2 的某次方(向下取整)。
那麼,如果我們想對 signed num 實現相同的行為該怎麼辦?
- 對於左移,沒有改變。
- 對於右移,將零添加到數字的前面會將負數變成正數。
解決方案:如果**右移負數,則用 1 來填充前面的位元,以保持數字為負**。
> 為什麼這樣做有效?
如果用 n 位儲存數字 `-3`,則 2 的補數意味著這等同於無符號的位元模式 2ⁿ - 3。
例如,16 位模式為 `0b1111 1111 1111 1101`,而 20 位模式為 `0b1111 1111 1111 1111 1101`。
因此,要將一個負數從 16 位擴展到 20 位,我們可以在前面填充 1。
當進行右移擴展時也適用相同的原則。
注意:負數在最高有效位(MSB)中有 1。
結論:如果我們想要右移(或擴展)一個 signed num,則用 MSB 填充額外的位元以保持相同的表示值。
> 稱 **sign-extension**。
#### Set Less Than
`slt x5, x6, x7` 的意思是:如果 `x6 < x7`,則將 `x5` 設置為 1;否則,將 `x5` 設置為 0。
有兩個版本:`slt`(將運算元視為 signed)和 `sltu`(將運算元視為 unsigned)。
而 `slti` 和 `sltiu` 的功能相同,但使用 immediate 代替 `x7`。
### Memory
允許將資料從主記憶體傳輸到暫存器,或反之亦然。
它的語法與算術指令不同。
- 例如:`lw x5, 12(x7)`
> 意思是:將 `x7` 解釋為地址並加上 `12`(注意,由於 RISC-V 中沒有類型,因此不需要做 `12*sizeof(type)` 的計算)。獲取下一筆資料的 word(4-byte),並將結果儲存到 `x5` 中。
- 例如:`sw x5, 12(x7)`
> 意思是:將 `x7` 解釋為地址並加上 `12`。用目前儲存在 `x5` 中的值,覆蓋該位置的 4-byte 記憶體。
**Data Transfer**:

#### Load from Memory to Register

> `x15` – base register (pointer to A[0])
`12` – offset in bytes
如果需要使用變數偏移量,則需要「將其**加到原位址上,並偏移 0**」。
例如,如果我們想要做 `A[i]`,可以這樣做:
```c
add x5, x15, x16 # Assume i stored in x16
lw x10, 0(x5)
```
因為 RISC-V 的指令集不支援在指令中使用變數作為偏移量,所以需要先計算出 `A[i]` 的位址(透過 `add` 指令),然後使用 `lw` 指令從計算出來的位址中存取資料。
#### Store from Register to Memory

> `x15` – base register (pointer)
`12`, `40` – offsets in bytes
`x15 + 12` 和 `x15 + 40` 必須是 4 的倍數。
#### Loading and Storing Bytes
除了 word data transfers(`lw`、`sw`)之外,RISC-V 還有 **byte data transfers**:
- load byte:`lb`
- store byte:`sb`
- load/store half-word:`lh`, `sh`
例如:`lb x10, 3(x11)`
> 將 `x11` 解釋為位址並加上 3。
獲得下一筆資料的 byte,並將結果儲存到 `x10` 中。
RISC-V 還有 unsigned byte 載入(`lbu`、`lhu`),會進行 zero-extends 以填滿暫存器(normal `lb` and `lh` sign-extends)。
> 問:為什麼不提供 unsigned store byte `sbu` 呢?
### Control
#### Types of Branches
- `beq`:如果兩個暫存器的值相等,則 Branch。
- `bne`:如果兩個暫存器的值不相等,則 Branch。
- `blt`、`bge`、`bltu`、`bgeu`:分別用於「小於」和「大於或等於」。
> 注意:`bgt` 不是一條指令,因為如果我們想執行 `bgt x5, x6, Label`,可以改為使用 `blt x6, x5, Label` 來達成同樣的效果。
#### Unconditional Jumps
根據需要跳轉到「**標籤**」或「**儲存在暫存器中的位址**」,有兩個版本的指令。
這些指令還參考當前的 Program Counter(PC),該 Counter 存儲當前正在執行的程式碼行的位址。
例如:`jal x1, Label`(Jump and Link)
>將 `x1` 設置為 PC+4(當前行之後的程式碼行),然後跳轉到 Label。
例如:`jalr x1, x5, 0`(Jump and Link Register)
> 將 `x1` 設置為 PC+4,然後跳轉到位址 `x5 + 0` 處的程式碼行。
存在 pseudo-instructions,用於在不需要 link 的情況下使用:
- `j Label`
- `jr x1`
為何要 Link?
Link 允許我們 **「return」到跳轉前的位址**,通常用於模擬函式的行為:

> 我們跳轉到 foo,並將跳轉後的下一行位址儲存到 `x1`。
稍後,foo 執行「`jr x1`」,這會跳轉回 `x1` 所指向的位址,實際上達到「return」的效果。
這使得 foo 能夠像一個「函式」一樣運作(雖然存在一些關鍵的差異)。
### Miscellaneous
這些指令不屬於其他類別:
- `lui`, `auipc`:這兩個指令作為「輔助指令」,讓虛擬指令如「`li`」和「`la`」能夠運作(很少單獨使用)。
- `li x5, 0xDEADBEEF`:將暫存器 `x5` 設為立即數 `0xDEADBEEF`。
- `la x5, Label`:將 `x5` 設為 `Label` 所指向的程式碼位址。
- `ebreak`:除錯器專用的行為。在某些模擬器中,它會**在這行暫停程式流程,讓你進行單步除錯**(類似於在其他除錯器中設置斷點)。
- `ecall`:作業系統專用的行為。用來**處理程式無法自行完成的操作**,例子包括「輸出資料」或「使用 malloc 請求記憶體」。
### Extensions
RISC-V 提供了許多「可選擇由 CPU 實作」的延伸功能:
- **M**ultiplication/Division extension(對於基本指令集來說太慢)。
> `mul x5, x6, x7`:將 `x6` 和 `x7` 相乘,並將結果儲存在 `x5` 中。
- **F**loat extension(需要更多的電路)。
- **D**ouble extension(需要更多的電路且需要 64 位元的暫存器)。
- 其他針對不同應用的延伸功能。