## 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 & 尋址模式 & 大小端