為什麼要先認識暫存器#

在學習 RISC-V 指令集之前,我們需要先建立對暫存器的基本認識:每個暫存器的特殊用途,以及呼叫函式前後該如何保存各個暫存器的內容。這兩件事其實就是 RISC-V 的呼叫慣例(Calling Convention),這些規範在編譯器的實作上都看得到,理解後對嵌入式系統軟體開發很有幫助。

本章前半部聚焦在通用暫存器、Caller save 與 Callee save 的觀念;後半部則會逐一走過 RV32I 指令集中的各類指令。

通用暫存器#

下圖列舉了 RISC-V 處理器中的通用暫存器:

RISC-V 通用暫存器

針對上圖,補充以下幾個重點:

x0 暫存器#

x0 暫存器的數值為 0 且無法更動,我們可以利用它做到類似 mov 的作用:

add a0, a1, x0   # a0 = a1

Return Address(ra)#

Return Address 存放函式執行結束時返回的位置。假設有兩個函式 a、b:

main() -> a()
a() -> b()

因為 main() 呼叫了函式 a(),在跳進函式 a() 執行前,main() 會將 ra 暫存器的值放到 Stack 上。此時,如果函式 a() 又呼叫了函式 b(),在呼叫之前我們會將函式 a() 的 return address 儲存到 Stack,避免 ra 暫存器被函式 b() 複寫後無法返回到 main() 繼續執行。由此可知,ra 由呼叫者維護暫存器的狀態,這類特性的暫存器都是 Caller save

在這邊如果不考慮 Stack pointer,Stack 內部看起來會是:

   +------------------------------+
   |   Data stored by function b  |
   +------------------------------+
   | Return Address of function a |
   +------------------------------+
   |     Return Address of main   |
 --+------------------------------+--

Stack pointer(sp)#

每個 Process 都會有屬於自己的區域變數,這些變數同樣會被存放在 Stack 中,Stack pointer 會指向 Stack 中屬於此 Process 的位址。不過,這部分與 ra 會有些不同,sp 暫存器被定義為 Callee save:當被呼叫的函式修改 sp 暫存器的內容之前,必須先將內容保存下來,並在函式返回前恢復原本的值。

如果以 main() 呼叫 function a() 來看,流程如下:

main() 將當前 ra 的值放到 stack -> 進入 function a() -> 將 sp 的值存放到 stack -> 移動 sp 的位址 (Scope of function a) -> 一些運算 -> 返回到 main 之前恢復 sp 的值 -> 跳回 main() -> 恢復 main() 的 ra

Function args:a0–a7(x10–x17)#

a0–a7 共 8 個暫存器可供我們用於存取函式的參數值。當我們呼叫函式時,函式的參數會依序的存放到這些暫存器中。

Return values:a0–a1#

a0、a1 暫存器除了會被當作 args 使用外,當函式需要有 return value 時,我們可以將 return value 存放至 a0 與 a1 暫存器,供其他函式取得返回值。

RV32I 指令集總覽#

RV32I 是 32-bit 的基本整數指令集,會使用到 32 個暫存器(x0–x31),且一共有 47 道指令。RV32I 的指令可分成六大類:

指令類型說明
I-Type包含了暫存器與立即數的運算
R-Type此類型的指令會有 3 個暫存器作為 Input,分別是 rd、rs1、rs2
S-Type包含存取記憶體的指令
SB-Type分支指令(條件跳轉)
U-Type將立即數放到高位,這些指令被設計來實現完整的 32 bits 運算
UJ-Type跳轉指令

為了方便,RISC-V 將一個 WORD 的大小設成 4 Bytes(32 bits),同時,RISC-V 的指令長度也是 32 bits,這些空間會被分割成好幾個 fields,不同類型的指令都會有不同的分配方式。

I-Format#

這種類型的指令代表暫存器與常數之間的運算:

| 31                20 | 19   15 | 14      12 | 11  07 | 06      00 |
+----------------------+---------+------------+--------+------------+
|   immediate[11:0]    |   rs1   |   funct3   |   rd   |   opcode   |
+----------------------+---------+------------+--------+------------+

ADDI#

addi rd, rs1, simm12

常數部分(simm12)為 12 位元的有號數,運算時會將 12 位元擴展成 32 位元,與 rs1 暫存器做加法運算並將結果寫進 rd 暫存器中。

關於 simm12 擴展的邏輯,可以參考 rv32emu-next 的實作:

// FI_IMM_11_0  = 0b11111111111100000000000000000000
static inline int32_t dec_itype_imm(uint32_t inst){
   return ((int32_t)(inst & FI_IMM_11_0)) >> 20;
}

SLTI[U]#

slti rd, rs1, simm12

運算時會將 12 位元擴展成 32 位元,再與 rs1 暫存器當做 signed number 做比較(如果指令為 SLTIU 則視為無號數)。若 rs1 暫存器小於常數,則將數值 1 寫入 rd 暫存器,否則為 0。

補充 本篇文章的 simm[number] 代表有號的 number 個位元,若是 uimm[number] 則代表無號的 number 個位元。若出現在下方指令,就不再特別說明。

ANDI / ORI / XORI#

andi/ori/xori rd, rs1, simm12

將 simm12 擴展後與 rs1 暫存器做 AND / OR / XOR 運算,再將結果寫入 rd 暫存器中。

SLLI / SRLI / SRAI#

slli/srli/srai rd, rs1, uimm5

常數部分為 unsigned 5 bits,將 rs1 暫存器做 shift 運算,偏移量為 uimm5 的值(0–31),偏移後的結果會寫入 rd 暫存器。

  • SLLI 代表邏輯左移
  • SRLI 為邏輯右移
  • SRAI 為算數右移

LW / LH / LHU / LB / LBU#

lw/lh/lhu/lb/lbu rd, rs1, simm12

將 rs1 暫存器的內容加上 simm12 視為位址,並將該位址存放的值放到 rd 中。

  • LW 為載入 32 bits(WORD)資料寫入 rd 中。
  • LH/LHU 為載入 16 bits(Half of WORD)資料分別做有號與無號擴展成 32 bits 後寫入 rd 中。
  • LB/LBU 為載入 8 bits(BYTE)資料分別做有號與無號擴展成 32 bits 後寫入 rd 中。

U-Format#

包含 upper immediates 的指令。

LUI(Load upper immediate)#

lui rd, uimm20

將 uimm20 存放到 rd 暫存器的高位 20 bits 中,剩餘的 12 bits 皆為 0。

AUIPC(add upper immediate to pc)#

auipc rd, uimm20

unsigned 20-bit 放到最高 20 位元,剩餘 12 位元補 0,將此數值與 pc 相加寫入 rd 暫存器。

R-Format#

指令為暫存器與暫存器之間的運算。

ADD / SUB#

add/sub rd, rs1, rs2

將 rs1 與 rs2 做加法 / 減法運算後,再將結果寫入 rd 暫存器。

SLT / SLTU#

slt/sltu rd, rs1, rs2

將 rs1 與 rs2 暫存器當做有號或是無號數做比較,若 rs1 小於 rs2,將數值 1 寫入 rd,反之寫入 0。

AND / OR / XOR#

and/or/xor rd, rs1, rs2

將 rs1 與 rs2 做 AND / OR / XOR 運算,並將結果寫入 rd 中。

SLL / SRL / SRA#

sll/srl/sra rd, rs1, rs2

將 rs1 做 shift 運算,再將結果寫入 rd 暫存器,rs2 的最低 5 位為代表位移量。

NOP 指令#

NOP 指令即為不改變任何暫存器狀態,除了 pc 以外。NOP 指令會被編碼成 addi x0, x0, 0 替代。

UJ-Format#

UJ-Format 包含了無條件跳躍(Unconditional Jumps)類型的指令。

jal rd, simm21

常數部分為 21 位元的有號數(此常數必須為 2 的倍數,代表 LSB 必為 0)。因為此道指令編碼的常數位元數只有 20 位元,所以只會將 simm21 的最高 20 位元放入指令編碼中,跳躍範圍為 ±1 MiB。同時也會將下一道指令的位址 pc+4 寫入 rd 暫存器中。在標準的 calling convention 中,rd 暫存器會使用 x1。如果只是單純的 jump,並非是呼叫函式需要儲存其返回位址 pc+4,可用 jal x0, simm21 取代。

jalr rd, rs1, simm12

跳躍的位址為 rs 暫存器加上 simm12,並把下一道指令的位址 pc+4 寫入 rd 暫存器中。

SB-Format#

SB-Format 包含了條件跳躍(Conditional Branches)類型的指令。

BEQ / BNE / BLT / BLTU / BGE / BGEU#

beq/bne/blt/bltu/bge/bgeu rs1, rs2, simm13

常數部分必須為 2 的倍數,即最低位元為 0。因為此道指令編碼的常數位元數只有 12 位元,所以只會將 simm13 的最高 12 位元放入指令編碼中,跳躍範圍為 ±4 KiB。

  • BEQ/BNE:將 rs1 與 rs2 做相同與不同的比較,若成立則跳躍。
  • BLT/BLTU:將 rs1 與 rs2 分別做有號 / 無號的小於比較,若成立則跳躍。
  • BGE/BGEU:將 rs1 與 rs2 分別做有號 / 無號的大於等於比較,若成立則跳躍。跳躍的位址則為 pc 加上 simm13。

S-Format#

將內容存放到記憶體中的指令。

SW / SH / SB#

sw/sh/sb rs2, rs1, simm12

將 rs1 暫存器的內容加上 simm12 視為位址,並將 rs2 的資料寫到該位址中。

  • SW 將 rs2 暫存器完整 32 bits 資料寫入位址。
  • SH 將 rs2 暫存器最低 16 bits 資料寫入位址。
  • SB 將 rs2 暫存器最低 8 bits 資料寫入位址。

Memory model#

定義了一組 FENCE 指令,達到多個 thread 間的記憶體同步。

控制與狀態暫存器指令(Control and Status Register Instructions)#

CSR Instructions#

CSRRW / CSRRS / CSRRC / CSRRWI / CSRRSI / CSRRCI

定義了一組 CSR (Control and Status Register) 指令,可用來讀取寫入 CSR。

Timers and Counters#

RDCYCLE[H]#

rdcycle 用來讀取最低 31-bit cycle CSR,rdcycleh 用來讀取最高 31-bit cycle 數。

RDTIME[H]#

用來讀取 time CSR。

RDINSTRET#

用來讀取 instret CSR。

Environment Call and Breakpoints#

ECALL#

屬於 I-Type。

使用來呼叫 system call。

EBREAK#

Debugger 用來切換進 Debugging 環境。

原文出處#