Learning Objectives
- 說出 CPU 內部的控制單元、算術邏輯單元與暫存器各自負責什麼
- 看懂指令格式:運算碼與運算元,並能把組合語言翻成機器碼
- 解釋旗標(零、負數、溢位)與條件跳躍如何做出「如果」和「迴圈」
- 用一個小指令集寫出相加、倒數迴圈、找較大值的組合語言程式
為什麼軟體要分 x64 版和 ARM64 版
下載軟體時,常看到同一個程式提供好幾個版本:「x64」「ARM64」。手機 App 也要分別為不同的處理器準備。 同樣是「電腦」,為什麼程式不能通用?
因為每一種 CPU 都有自己認得的一套指令集(instruction set):哪些位元代表「加法」、哪些代表「跳躍」,各家設計不同。 編譯好的程式就是一長串指令的位元,換了指令集,同樣的位元意思就完全不同。
上一課看過 CPU 怎麼擷取、解碼、執行一條指令。這一課往裡面再走一層:
- CPU 裡面有哪些零件?暫存器是什麼?
- 一條指令的位元怎麼安排?組合語言和機器碼是什麼關係?
- 只有加、減和跳躍,怎麼寫出「如果」和「迴圈」?
最後你會用 UC8 教學處理器的 10 條指令,自己寫三個小程式。
CPU 裡面有什麼
| 零件 | 做什麼 | UC8 裡的例子 |
|---|---|---|
| 控制單元(control unit) | 擷取指令、解碼、送出控制訊號,指揮其他零件 | PC、IR、解碼器 |
| 算術邏輯單元(ALU) | 做加減等算術、比較與邏輯運算 | ADD、SUB 的計算 |
| 暫存器(register) | CPU 內部的小儲存格,存正在處理的值 | ACC、PC、IR、MAR、MDR |
| 旗標(flag) | ALU 算完後順便記下的狀態,給條件跳躍用 | Z、N、V |
暫存器和記憶體的差別:暫存器在 CPU 裡面,數量很少(UC8 只有一個能讓你放計算結果的 ACC),但存取最快; 記憶體在 CPU 外面,空間大很多,但每次存取都要經過匯流排。所以程式常見的模式是: 從記憶體載入暫存器 → 在暫存器裡計算 → 存回記憶體。
時脈(clock):CPU 依照一個固定節拍的訊號一步一步動作。上一課模擬器的每個「微步驟」,可以想成一個節拍(示意,真實 CPU 的微步驟安排更複雜)。
旗標是條件判斷的基礎。UC8 的 ALU 每次算完都會更新:
| 旗標 | 什麼時候是 1 | 例子 |
|---|---|---|
| Z(zero,零) | 結果等於 0 | 5 − 5 |
| N(negative,負數) | 結果是負數(最高位元是 1) | 5 − 7 = −2 |
| V(overflow,溢位) | 結果超出 8 位元能表示的 −128~127 | 127 + 1 |
指令集與機器碼
一條 UC8 指令有 8 個位元,分成兩個欄位:
運算碼(3 位元) 運算元:位址(5 位元)
┌─────────┐ ┌───────────────┐
0 1 1 0 0 1 1 0 → ADD 6:ACC ← ACC + 記憶體[6]
3 個位元的運算碼可以表示 2³ = 8 種指令;5 個位元的位址可以指到 2⁵ = 32 格記憶體,這就是 UC8 只有 32 格記憶體的原因。
運算碼 000 這一組不需要位址,用後 5 位元區分 HLT、OUT、IN。
| 指令 | 機器碼 | 做什麼 | 會改的旗標 |
|---|---|---|---|
HLT |
000 00000 |
停止執行 | |
OUT |
000 00001 |
把 ACC 送到輸出裝置 | |
IN |
000 00010 |
從輸入裝置讀一個數到 ACC | Z N |
LDA a |
001 aaaaa |
載入:ACC ← 記憶體[a] | Z N |
STA a |
010 aaaaa |
儲存:記憶體[a] ← ACC | |
ADD a |
011 aaaaa |
加法:ACC ← ACC + 記憶體[a] | Z N V |
SUB a |
100 aaaaa |
減法:ACC ← ACC − 記憶體[a] | Z N V |
JMP a |
101 aaaaa |
跳躍:PC ← a | |
JZ a |
110 aaaaa |
Z 是 1 就跳到 a | |
JN a |
111 aaaaa |
N 是 1 就跳到 a |
組合語言(assembly language)是機器碼給人看的寫法:用 ADD 這種好記的名稱代替 011,用標籤 a 代替位址 6。
把組合語言翻成機器碼的程式叫做組譯器(assembler)。組合語言和機器碼大致是一行對一條;
而 Python、C 這類高階語言的一行,常常要變成好幾條甚至很多條機器指令。
UC8 的組合語言還有一個寫法 DATA n:在這一格放一個數字(−128~127),不是指令。
組譯器:把組合語言翻成機器碼
組譯器其實很簡單:查表找出運算碼,乘以 32(等於往左移 5 個位元,空出後 5 位),再加上位址。看 code 的值怎麼算出來,以及每一行印出的 8 個位元裡,前 3 位元和後 5 位元分別是什麼。
用跳躍做出「如果」和「迴圈」
UC8 沒有 if、也沒有 while,只有跳躍:JMP 一定跳,JZ、JN 看旗標決定跳不跳。
高階語言的判斷和迴圈,翻成機器指令後都是「比較 → 條件跳躍」。
如果:要判斷 a 是否比 b 大,就算 b − a,看結果是不是負數:
if b - a < 0: # a 比較大
print(a)
else:
print(b)
LDA b
SUB a ; ACC ← b − a,同時設定 N 旗標
JN aBig ; 負數就跳去 aBig
LDA b ; 沒跳:b 比較大
OUT
HLT
aBig: LDA a
OUT
HLT
迴圈:在程式後面放一個「跳回前面」的指令,再用條件跳躍決定什麼時候離開:
n = 3
while True:
print(n)
n = n - 1
if n == 0:
break
LDA n
loop: OUT
SUB one ; ACC ← ACC − 1,結果是 0 時 Z=1
JZ done ; 減到 0 就離開
JMP loop ; 否則跳回 loop
done: HLT
這就是上一課 PC 的妙用:改 PC 就能改變下一條要執行的指令。
真實的 CPU 比 UC8 多了什麼
UC8 刻意做得很小,好讓你看清楚每一步。真實的處理器原理相同,但多了很多讓速度變快的設計:
| UC8 | 真實的 CPU |
|---|---|
| 只有一個 ACC | 有十幾到幾十個通用暫存器,計算時少跑幾趟記憶體 |
| 8 位元,數字只能到 127 | 常見 64 位元,一次處理的數字大得多,可用的記憶體位址也多很多 |
| 一條指令做完才擷取下一條 | 管線化(pipelining):上一條還在執行,下一條已經在解碼、再下一條已經在擷取 |
| 一個核心 | 多核心:一顆晶片裡有好幾個 CPU 核心,可以同時執行不同的工作 |
| 直接讀寫記憶體 | 中間有好幾層快取(cache),常用的資料不必每次都到主記憶體拿 |
常見的指令集有 x86(多數桌上型與筆記型電腦)、ARM(多數手機與平板,也有筆電採用)、RISC-V(開放的指令集,任何人都可以拿來設計處理器)。 指令集的設計有兩種常見取向:
- 複雜指令集(CISC):指令種類多、一條指令能做比較多事,指令長度不固定,例如 x86
- 精簡指令集(RISC):指令種類少而規則,每條指令做簡單的事、長度固定,讓硬體容易做成管線化,例如 ARM、RISC-V
同一個 Python 或 C 程式,可以由不同的編譯器分別產生 x86 版和 ARM 版的機器碼,這就是開頭「x64 版、ARM64 版」的由來。
想一想:暫存器與機器碼
UC8 的 STA 9 組譯成機器碼是哪一個?(STA 的運算碼是 010)
哪一個暫存器存放「下一條要擷取的指令位址」?
UC8 執行 SUB 之後結果是 0,哪個旗標會變成 1?
想一想:組合語言程式
下面這段 UC8 程式會依序輸出哪些數?
LDA n
loop: OUT
SUB two
JN done
JZ done
JMP loop
done: HLT
n: DATA 7
two: DATA 2
組合語言和機器碼是什麼關係?
為 x86 處理器編譯好的程式,為什麼不能直接拿到 ARM 處理器上執行?
重點整理
- CPU 由控制單元(擷取、解碼、指揮)、ALU(計算)和暫存器(CPU 內部最快的小儲存格)組成;ALU 算完會更新旗標(Z 零、N 負數、V 溢位)。
- 一條指令分成運算碼和運算元。UC8 用 3 位元運算碼、5 位元位址,所以最多 8 組運算碼、32 格記憶體。
- 組合語言是機器碼給人看的寫法,組譯器把它翻成機器碼:查表找運算碼、左移、加上位址。
- 機器層級沒有
if和while:判斷和迴圈都是「計算或比較 → 看旗標 → 條件跳躍」,跳躍就是改變 PC。 - 每種 CPU 有自己的指令集(x86、ARM、RISC-V…),編譯好的程式只能在相同指令集的 CPU 上直接執行。
- 真實 CPU 有更多暫存器、更寬的位元數、管線化、多核心與快取,基本流程仍是擷取、解碼、執行。
下一課:CPU 很快,記憶體相對慢。電腦怎麼用暫存器、快取、主記憶體、硬碟組成「記憶體階層」,讓 CPU 不必一直等資料? 想用真正的程式語言練習判斷和迴圈,可以接著上 Python 軌。