## ISA Design Principle
在硬體層面越容易實作越好。
## Interface Design
一個好的 Interface:
- portability, compatibility
- 具有兼容性,底層可以安心的更新
- generality
- 同個介面可以用於很多不同的面相,或者換了一個面向但是核心保持不變
- 對高層提供方便的功能
- 同時在低層可以有效率的實作
# General Purpose Register ISA
這種 ISA 會有一群有==專門用途的(General Purpose)== REG。
GPR ISA 是現代處理器最常見的設計,取代了早期的架構:
- **累加器架構 (Accumulator Architecture):** 只有一個隱式的特殊暫存器(**累加器**)來儲存其中一個操作數和運算結果。
- **堆疊架構 (Stack Architecture):** 運算的操作數和結果都隱式地位於**堆疊頂部**。
- by Gemini 3 :)
General Purpose Register ISA 還可以細分成下列兩種。
## register-memory
這種的特色是==算術/邏輯運算==的其中一個操作數可以直接是記憶體位址。
這種的有兩種結構:
- 2 address: `add R1, A // R1 = R1 + mem[A]`
- 有一個(通常是第一個) REG 同時擔任 Source 跟 Destination,例如上面的 R1
- 這樣的好處是指令比較緊湊,壞處是因為一定會覆寫,如果 REG 資料還要用的話就要先存起來
- x86 系列就是代表。
- 3 address: `add R2, R1, A // R2 = R1 + mem[A]`
- 把負責 Destination 的 REG 獨立出來
主流的 CISC 架構大都採用 register-memory 類型的 GPR ISA。
:::warning
- x86-64 其實有兩種語法,**Intel** 跟 **AT&T**,這兩個語法有許多差異,其中一個是**順序上的不同**,下面都是把東西從 `RBX` 搬到 `RAX`:
- **Intel**:`MOV RAX, RBX`,方向是 RAX **FROM** RBX
- **AT&T**:`MOV %RBX, %RAX`,方向是 RAX **TO** RBX
- 此外還有很多差異,像是 AT&T 的 reg 前面有 %、立即數前面有 \#、會用後綴表示大小等等,但是 Intel 都沒有
:::
## register-register (load-store)
**所有**算術/邏輯運算的**操作數和結果都必須在暫存器中**。只有專門的 **載入 (Load)** 和 **儲存 (Store)** 指令才能存取記憶體。
```=
...
load R3 A // R3 = mem[A]
add R2 R1 R3 // R2 = R1 + R3
store R2 A // mem[A] =R3
```
ARM, MIPS, RISC-V 就是這種的代表。
主流的 RISC 架構大都採用 register-register 類型的 GPR ISA。
# RISC vs. CISC
例如做一個乘法:
- RISC-V
```=
LOAD A, 2:3
LOAD B, 5:2
MULTI A, B
STORE 2:3, A
```
- CISC
```=
MULT 2:3, 5:2
```
>`n:m`的表示法是一個矩陣中第 n row 第 m col 個值
下面是整合了 Gemini 3 跟 ChatGPT 的總結(2025)。
:::spoiler
---
# 🧠 **RISC vs CISC:現代 CPU 架構的精準比較(2025 更新版)**
RISC(Reduced Instruction Set Computer,精簡指令集)
CISC(Complex Instruction Set Computer,複雜指令集)
兩者最初的差別來自於 **指令集設計哲學 (ISA)**,但在現代 CPU 架構中,它們的差異已逐漸融合。以下比較將以「現代 CPU 觀點」進行精準整理。
---
# 📌 **RISC vs CISC — 核心差異表(現代觀點)**
| 特徵 | RISC(精簡指令集) | CISC(複雜指令集) |
| --------------- | ------------------------------------------------------------ | --------------------------------------------- |
| **設計理念** | 精簡且規則化的指令,目標是讓 CPU 更易於 pipeline、提升每 cycle 的執行效率。 | 用更強大的指令減少程式碼長度(code density),早期降低編譯器負擔與記憶體需求。 |
| **指令集大小 / 複雜度** | 指令數少、格式規則、操作一致。 | 指令數多、格式不規則、操作多樣。 |
| **指令長度** | 大多為固定長度(如 ARM A32、RISC-V 32bit),但也可能有變長(ARM Thumb 16/32bit)。 | 變長指令(x86: 1–15 bytes),解碼更複雜。 |
| **記憶體存取模型** | **Load/Store**:只有 Load/Store 指令存取記憶體。 | **Register-Memory**:許多指令可直接存取記憶體。 |
| **暫存器數量** | 多(一般 16–32 個以上),編譯器容易寄存器分配。 | 現代 x86 也有 16 個整數暫存器 + 多個 SIMD 暫存器,但歷史包袱較多。 |
| **控制邏輯(微碼)** | 主要使用硬體邏輯;遇複雜指令時也可能使用 microcode。 | 多使用 microcode,複雜指令透過 microcode 執行。 |
| **代表架構** | ARM、RISC-V、MIPS | x86(Intel/AMD) |
| **現代實現** | 精簡 ISA,但加入壓縮指令、複雜 load/store 以提升 code density。 | 外部為 CISC,內部將指令拆成 RISC 風格的微操作(micro-ops)。 |
---
# 🔍 **重點一:指令執行時間(常被教科書誤解)**
舊觀念:
* RISC 指令 = 1 cycle
* CISC 指令 = 多 cycle
現代觀念(正確):
✨ **RISC 指令比較容易 pipeline,因此 CPU 可以做到「每 cycle retire 一個指令」,但指令本身仍經過多個 pipeline stage。**
✨ **CISC 指令被拆成多個微指令(μops),也能 pipeline。**
因此:
* **現代 RISC ≠ 單一 cycle 完成一條指令**
* **現代 CISC 也能做到高 IPC(instructions per cycle)**
---
# 🔍 **重點二:現代 x86 為何「外 CISC、內 RISC」**
以 Intel/AMD 為例:
1. x86 指令複雜(可能有多個 memory operand)
2. CPU 會將其 decode → micro-ops(精簡型指令)
3. micro-ops 才是真正送進 pipeline 執行的指令
4. 這些 micro-ops 和 RISC 非常相似
因此:
> **現代 x86 CPU 的核心本質是 RISC-like micro-ops 引擎。**
而外層維持 CISC 指令只是因為生態系與相容性。
---
# 🔍 **重點三:現代 RISC 也吸收了 CISC 的優點**
例如:
* **ARM Thumb/Thumb-2** 使用 16-bit 與 32-bit 混合指令 → 提升 code density
* **RISC-V Compressed (RV32C)** 讓常用指令縮短成 16-bit
* ARM 與 RISC-V 都加入複雜 load/store、複雜 addressing mode
因此:
> **現代 RISC 已不是「非常純的精簡」指令集,而是平衡了性能與代碼密度。**
---
# 🧠 **總結:現代 RISC vs CISC 的本質差異**
如果只用一句話概括:
> **RISC:讓硬體更容易 pipeline,靠 ISA 的規整性提升效率。**
> **CISC:保留複雜指令,但靠微架構(micro-ops)彌補複雜度。**
而現代 CPU 都往同一方向融合:
| 現代 CISC | 現代 RISC |
| -------------------------------- | --------------------- |
| 外部看起來複雜,但內部用 RISC-like micro-ops | 外部簡潔,但加入壓縮指令與進階特性 |
| 依賴強大解碼器與 micro-op cache | 依賴編譯器與整齊的 pipeline 設計 |
差異已不再如 1980 年代那麼鮮明。
---
:::
# RISC Design Principle
下面介紹三個 RISC 設計的原則
## Principle:Simplicity favors regularity
給予限制可以帶來簡易性。
例如 RISC 中的 Arithmetic operations 都限制為 3 個 operands:
$$
\text{Add a, b, c}
$$
## Principle:Smaller is faster
上面提到 RISC 這類的 ISA 有一個重要角色是 REG。
在 RISC 中有個固定為 `32X64 bit` 的 register file。
將每個 REG 大小限制為 `64 bit` 帶來的好處就是快,相比其他例如 `256 bit` 之類的 ISA 就是快。
## Principle:Make the common case fast
由於程式中常常會出現對某些變數加上常數的操作,或者叫 Constant or Immediate operands,所以 RISC 支援常數的操作,例如常數加法:
```=
addi x22, x22, 4
```
---
# 大小的名稱
## **x86 / x86-64(Intel / AMD)**
x86 是最「有名、最常見」的命名體系,許多語言習慣也源自這裡。
| 名稱 | 大小 | 常見用途 |
| --- | --- | --- |
| **byte** | 8-bit (1 byte) | 8-bit 數值 |
| **word** | 16-bit (2 bytes) | 16-bit 數值 |
| **dword / doubleword** | 32-bit (4 bytes) | 32-bit 數值、指標 (x86) |
| **qword / quadword** | 64-bit (8 bytes) | 64-bit 數值、指標 (x64) |
| **tbyte / ten byte** | 80-bit | x87 浮點數 |
| **oword** | 128-bit | SSE |
| **yword / ymmword** | 256-bit | AVX |
| **zword / zmmword** | 512-bit | AVX-512 |
## **ARM(AArch32 / AArch64) / MIPS / RISC-V**
| 名稱 | 大小 |
| --- | --- |
| **byte** | 8-bit |
| **halfword** | 16-bit |
| **word** | 32-bit |
| **doubleword** | 64-bit |
有些 RISC-V 的擴展集會有 ==quadword 128-bit==。
也就是說主要差異在 word 的部分:
| 架構 | 8-bit | 16-bit | 32-bit | 64-bit |
| --- | --- | --- | --- | --- |
| **x86** | byte | word | dword | qword |
| **ARM / MIPS / RISC-V** | byte | halfword | word | doubleword |
# General Purpose REG
## x86-64
x86 的暫存器在不同大小下都有不同名稱:
| 64-bit | 32-bit | 16-bit | 8-bit | 用途 |
| --- | --- | --- | --- | --- |
| **RAX** | EAX | AX | AL / AH | Accumulator |
| **RBX** | EBX | BX | BL / BH | Base |
| **RCX** | ECX | CX | CL / CH | Counter |
| **RDX** | EDX | DX | DL / DH | Data |
| **RSI** | ESI | SI | SIL | Source index |
| **RDI** | EDI | DI | DIL | Destination index |
| **RBP** | EBP | BP | BPL | Base pointer |
| **RSP** | ESP | SP | SPL | Stack pointer |
| **R8–R15** | R8D–R15D | R8W–R15W | R8B–R15B | 其他通用暫存器 |
x86 的特色:
- **不同大小會換名字**(AX / EAX / RAX),但他們實際上是同一個 REG。
- 會這樣設計是為了向下兼容
- **有高 8-bit 暫存器**(AH, BH, CH, DH),是 x86 特有的歷史產物。
## MIPS
MIPS 用 **數字 + 對應別名**。
| 名稱 | 別名 | 用途 |
| --- | --- | --- |
| **$0** | zero | 永遠為 0 |
| **$1** | at | assembler temporary |
| **$2–$3** | v0–v1 | return values |
| **$4–$7** | a0–a3 | arguments |
| **$8–$15** | t0–t7 | temporaries |
| **$16–$23** | s0–s7 | saved registers |
| **$24–$25** | t8–t9 | temporaries |
| **$26–$27** | k0–k1 | reserved for OS kernel |
| **$28** | gp | global pointer |
| **$29** | sp | stack pointer |
| **$30** | fp | frame pointer |
| **$31** | ra | return address |
## RISC-V
RISC-V 有 **32 個 GPR x0–x31**,但有一組標準別名:
| **編號** | **ABI 名稱** | **呼叫慣例分類** | **用途說明** |
| --- | --- | --- | --- |
| **x0** | **zero** | **零值** | **永遠保持零值**,寫入無效。用於產生零值常數或作為丟棄結果的目標。 |
| **x1** | **ra** | **呼叫者儲存** | **回傳位址 (Return Address)**。儲存呼叫子函式後應返回的指令位址。 |
| **x2** | **sp** | **被呼叫者儲存** | **堆疊指標 (Stack Pointer)**。指向程式執行堆疊的頂部。 |
| **x3** | **gp** | **被呼叫者儲存** | **全域指標 (Global Pointer)**。指向全域資料區的特定位置,用於快速存取全域變數。 |
| **x4** | **tp** | **被呼叫者儲存** | **執行緒指標 (Thread Pointer)**。指向當前執行緒的本地儲存空間 (Thread-Local Storage)。 |
| **x5** | **t0** | **呼叫者儲存** | **臨時暫存器** (Temporary Register 0)。用於臨時計算,**呼叫子函式後不保證保留**。 |
| **x6-x7** | **t1-t2** | **呼叫者儲存** | **臨時暫存器** (Temporary Registers 1-2)。 |
| **x8** | **s0/fp** | **被呼叫者儲存** | **儲存暫存器** (Saved Register 0) 或 **框架指標 (Frame Pointer)**。 |
| **x9** | **s1** | **被呼叫者儲存** | **儲存暫存器** (Saved Register 1)。 |
| **x10-x11** | **a0-a1** | **呼叫者儲存** | **引數/回傳值暫存器** (Argument/Return Values 0-1)。用於傳遞前兩個函式引數,也用於儲存函式的回傳值。 |
| **x12-x17** | **a2-a7** | **呼叫者儲存** | **引數暫存器** (Argument Registers 2-7)。用於傳遞後續的函式引數。 |
| **x18-x27** | **s2-s11** | **被呼叫者儲存** | **儲存暫存器** (Saved Registers 2-11)。 |
| **x28-x31** | **t3-t6** | **呼叫者儲存** | **臨時暫存器** (Temporary Registers 3-6)。 |
### Saved Registers & Temporary Registers
Saved Registers 直白來說是他是 ==被儲存的==,意思是裡面的值不會因為 call 了某個函數而被變動;相對的就是 Temporary Registers。
因此當 ==被呼叫者 callee== 需要使用 ==Saved Registers== 時,有義務維持他原始的狀態;要嘛就需要把原本的值先儲存起來,用完後要復原回原本的值;要嘛是跳回去前要把值想辦法變回原始的值。
這就是所謂的 ==被呼叫者儲存==;相對的就是呼叫者儲存。
### 4 個特殊指標
- sp:理論應該要永遠指向 stack 的頂端;注意 stack 是向低位址增長的
- gp、tp:一個幫助存取全域資料(`.data`, `.bss`, `.rodata`)一個則是存取執行緒的區域資料
- fp:理論上代表當前 ==**procedure call stack frame**== 的底端。(見下方)
- fp 的負方向是傳入的參數,正方向代表 procedure 的變數
# 區段 Segment
- .data & .rodata:用來存放變數的區段
- .bss:存放**未初始化**的**全域/靜態變數**,或需要大片空間且初始值為零的陣列。
- .text:程式碼區段
通常來說結構是 .data, .rodata, .bss, .text 由上往下編寫。
```risc=
#####################################
# 1. 唯讀資料 (常數和字串)
#####################################
.rodata
prompt: .string "請輸入數據: "
newline: .byte 0x0A # 換行符
#####################################
# 2. 可變的已初始化資料
#####################################
.data # 宣告接下來的資料屬於 .data 區段
# 1. 儲存一個初始值為 42 的 32 位元整數 (Word)
my_variable:
.word 42 # 佔用 4 個位元組
# 2. 儲存一個初始值為 120 (ASCII 'x') 的單一位元組
my_byte:
.byte 120 # 佔用 1 個位元組
# 3. 儲存一個包含數字的陣列
my_array: # 使用時 my_array 就是第一個值的地址
.word 10, 20, 30, 40 # 佔用 4*4 = 16 個位元組
# 4. 儲存一個初始化字串
prompt_msg:
.string "請輸入一個數字:" # .string 會自動在結尾添加 NULL 終止符 \0
#####################################
# 3. 未初始化資料 (保留空間)
#####################################
.bss
buffer: .space 1024 # 1KB 的緩衝區,啟動時會清零
#####################################
# 4. 程式指令
#####################################
.text
.globl main # 讓一個標籤成為外部可見的程式進入點或函式, 對 linker 很重要!!!
main:
# ... 程式開始執行
# 載入常數字串的位址 (使用 la 偽指令)
la a0, prompt
# ...
# 存取可變資料 (使用 lw/sw)
lw t0, global_counter
addi t0, t0, 1
sw t0, global_counter
# 存取緩衝區
la a1, buffer
# ...
```
# Procedure & 編寫技巧
Procedure 就是高階語言的函數。
用組合語言寫 Procedure 的時候,因為需要處理 Caller、Callee Saved 的問題,所以會分成 Leaf 跟 NonLeaf Procedure 有兩種處理方式。
讓我們先分析一下所有的暫存器:
- s 家族(saved):callee-saved
- t 家族(temp):caller-saved
- a 家族(argument):caller-saved
- 特殊 4 指標:ra 是 caller-saved,另外 3 個是 callee-saved
## Non-Leaf
Non-Leaf 的意思是會 call 其他 Procedure。
因為同時是 caller 跟 callee,所以 caller-saved 跟 callee-saved 的 reg 都要小心的維護。
- callee 的部分
- 使用 reg 前:callee-saved 的 reg 時,要先把原本在裡面的值存到 stack 裡面
- 使用完畢後:把值再放回 callee-saved reg 裡面
- caller 的部分
- 跳之前:要把 caller-saved 的 reg 的值先保存在 stack 裡面
- 跳回來後:把 caller-saved 的東西從 stack 拿回來
:::warning
可以注意到,每個 Procedure 其實在 stack 中都有一塊連續的區塊是自己的,這塊就叫做 **Procedure Call Stack frame**
:::
:::info
- 所以編寫的時候會發現,每做一次~~放進去拿出來~~,`addi sp sp -needed_size` 跟 `addi sp sp needed_size` 會成對的出現
- caller-saved 的 reg 只是暫時用的話~~可以用完就不管他~~,不一定跳之前要存起來
- ra 會變動其實不是 callee 手動變更的,而是 jump 家族會幫你做的
:::
### Prologue 和 Epilogue 約定
這兩個約定代表在區塊的開頭跟結尾應該要怎麼做。
- Prologue 開頭(第一行):
- 存 ra(如果是 non-leaf) 跟 fp(有需要的話) 到 stack 裡面
- 維護 sp 指針,更新 fp 指針為當前 call stack frame 的==底端==
- Epilogue 結尾:
- 還原 ra 跟 fp,以及維護 sp 指針
## Leaf
Leaf 的意思是不會再 call 其他 Procedure 的 Procedure。
因此他只會是 callee 而不會是 caller,所以只要維護
- callee 的部分
- 使用 callee-saved 的 reg 時,要先把原本在裡面的值存到 stack 裡面
- 當使用完畢後要記得把值再放回 callee-saved reg 裡面
# Memory operands & Word Address & Bytes Address
下面是 RISC 讀取或儲存記憶體的語法
```=
lw x9, 8 (x22) // x9 = mem[8+reg[x22]]
sw x9, 8 (x22)// mem[8+reg[x22]] = x9
```
不過要注意的是,`8` 這個值,是代表 8 個 bytes 的意思。
因為 RISC 在處裡地址時是 Bytes Address,也就是說第 0000 位置跟第 0001 位置相差的是 1 個 Bytes;另一種 Word Address 型的第 0000 位置跟第 0001 位置相差的則是 1 個 Word。
由於 RISC 中每個 REG 大小固定為 doubleword 或者說 8 Bytes,所以如果要移動到下一個地址,一次要加 8。
:::warning
講義的例子是,如果 `x22` 是某個陣列 `A` 的起始位置,那麼 `mem[8 + (x22)]` 就是 `A[1]`。
:::
## 尋址模式
不同的 ISA 都有其獨特的存取記憶體格式。
- x86-64:`基址 + 變址 × 比例 + 位移`
- `基址` 跟 `變址` 可以是 Imm 或 reg,如果是 reg 的話就是用裡面存的值作為地址。==可以空著==
- 比例只能是 1,2,4,8
- **Intel**:`[基址 + 變址 × 比例 + 位移]`
- **AT&T** :`位移(基址, 變址, 比例)`
- risc-v:`基址 + 位移`
- `基址` 必須是暫存器
- `位移` 只支援 12 bit ==有符號==整數
- 如果只想用位移可以放 `x0`;如果要跨很遠請先把值算好後放進 reg
## Big Endian vs. Little Endian
RISC 是 Little Endian。
Little Endian 是把一個值的[低位放在較小的位址處,高位放在較大的位址處](https://zh.wikipedia.org/wiki/%E5%AD%97%E8%8A%82%E5%BA%8F)。
也就是說如果把一個 4B 的 int `0x01234567` 存進 `0x100` 這個起始位置,並且是 Byte Address,也就是說 `0x101` 是下一個 Byte,那麼:
- Little Endian 低位放在小位址,高位在大位址
| `0x100` | `0x101` | `0x102` | `0x103` |
| ------- | ------- | ------- |:-------:|
| `0x67` | `0x45` | `0x23` | `0x01` |
- Big Endian 低位放在大位址,高位在小位址
| `0x100` | `0x101` | `0x102` | `0x103` |
| ------- | ------- | ------- |:-------:|
| `0x01` | `0x23` | `0x45` | `0x67` |
## Alignment
所謂的 Alignment ,舉例來說,對於一個 word 的 object,如果他儲存的位置並非在 `0x000,0x004,0x008...`等以 4 為倍數的位置,那麼該 object 就是 unaligned。
RISC 中並沒有要求 object 需要 Alignment,但是有 Alignment 的速度會快很多,而且可以避免[一些問題](https://stackoverflow.com/questions/68245606/risc-v-ram-address-alignment-for-sw-sh-sb)
## Registers vs. Memory
REG 就是比 MEM 更快的記憶體,一個寫的好的 Compiler,可以盡量減少對 MEM 的讀寫操作。
# Basic Block
對於一連串 sequence 的 instructions,只要在他們當中:
- No branch targets
- 也就是沒有其中的某一行長得像 `label:....`
- 白話來講就是不會有人從其他地方跳進來
- 但是除了第一行開頭可以是,畢竟他就是開始的地方
- No embedded branches
- 也就是沒有其中的某一行長得像 `be a,b label` 或是其他會分枝的 instruction
- 白話來講就是不會執行到一半突然跳去別的地方
- 但是除了最後一行可以是,畢竟他就是結束了要去別的地方
而劃定一個 Block 的好處是 Compiler 可以對一個 Block 做優化,像是 instruction reordering。
每個 label 其實都是一個地址,也就是存放 instruc 的地址,然後 jump table 存放的是這些地址,每個地址是 4B,所以才乘以 4 的方式移動
---
## 回顧
- GPR ISA 可無直接存取 mem,3 or 2 address
- 不同 ISA 對 word 的差異
- risc-v 的 reg 們:4 個特殊 + 3 大家族,誰 er 誰 ee
- risc 區段 & 變數語法
- bytes address & 尋址模式 & 大小端