# 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 個整數暫存器**供使用。 附註(暫存器位於處理器內部): ![圖片](https://hackmd.io/_uploads/Sypo4O1kJe.png =90%x) - 暫存器編號從 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 加法例子: ![圖片](https://hackmd.io/_uploads/rkG7v_JJkl.png =60%x) 減法例子: ![圖片](https://hackmd.io/_uploads/Hkq7Pdyy1e.png =60%x) 結合 1: ![圖片](https://hackmd.io/_uploads/ry-_wuJkyl.png =60%x) > 注意:一行 C 程式碼可能會分解成多行 RISC-V 程式碼。 結合 2: ![圖片](https://hackmd.io/_uploads/ByojDd1kJg.png =60%x) > 注意:通過這種方式使用 `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**: ![圖片](https://hackmd.io/_uploads/ry6b01xy1g.png) #### Load from Memory to Register ![圖片](https://hackmd.io/_uploads/r1cEMKy1yg.png =60%x) > `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 ![圖片](https://hackmd.io/_uploads/SJEOrtk1yl.png =70%x) > `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」到跳轉前的位址**,通常用於模擬函式的行為: ![圖片](https://hackmd.io/_uploads/SJ6EqYyJyl.png =30%x) > 我們跳轉到 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 位元的暫存器)。 - 其他針對不同應用的延伸功能。