一份刻在石頭上的承諾
在基礎那一級,我們畫出了軟體與硬體之間的大接縫,並替住在那裡的那一層取了名字:指令集架構,也就是 ISA。這一級就要把那一層打開來看。把 ISA 想成一份有兩個簽名的契約。一邊,每個編譯器與寫組合語言的人都承諾,只用清單上的指令來說話。另一邊,每塊宣稱實作了該 ISA 的晶片,都承諾要完全照字面執行那些指令。只要雙方都守信,程式就永遠不必知道自己跑在哪一塊特定晶片上,晶片也永遠不必知道自己跑的是哪個程式。
這就是硬體/軟體介面,而它刻意是整台機器裡最保守的東西。微結構每隔一兩年就被重新發明;ISA 卻是要撐上數十年的。原因是二進位相容性:一份 1995 年編譯出來的程式,應該仍能在 2025 年同一家族的晶片上跑,因為它當初對著打造的那份契約並沒有改變。打破 ISA,你就讓所有已經為它寫好的程式全部擱淺。所以結構設計師死命守著它,往上添加的頻率,遠高於更動它的頻率。
契約裡到底寫了什麼
一份這麼重要的契約,需要的條款清單卻短得出奇。ISA 命名了一小群快速的暫存位置,叫做架構暫存器——程式被允許用名字直接稱呼的具名格子。它命名了一個特殊的暫存器,叫做程式計數器,永遠存著下一條要執行的指令的位址。它替程式需要做的每一件事都定義了一類指令:做算術、把資料搬進搬出記憶體、決定接下來要跑什麼。它還精確地寫明每條指令該如何寫成一串位元的圖案。這基本上就是整份文件了。
在這一整級,我們會用 RISC-V 當例子,因為它是一個乾淨、現代、開放的標準,沒有什麼好藏的——身上沒有黏著幾十年的歷史包袱。上面那三類指令家族,正是它的脊椎。像加法、位移這樣的算術指令,對著已經坐在暫存器裡的值做計算。載入與儲存指令是唯一會碰記憶體的指令——載入把一個字組從記憶體複製進暫存器,儲存則往另一個方向複製。而控制流指令——分支與跳躍——會改寫程式計數器,讓機器去跑下一條以外的東西。一顆 CPU 所做的一切,都是這三條線交織出來的。
# A tiny RISC-V snippet: add 10 to a value in memory, store it back. # x10, x11 are architectural registers. 0(x10) means address in x10. lw x11, 0(x10) # LOAD: x11 <- memory[x10] addi x11, x11, 10 # ARITH: x11 <- x11 + 10 (10 is an immediate) sw x11, 0(x10) # STORE: memory[x10] <- x11 bne x11, x0, loop # BRANCH: if x11 != 0, set PC to 'loop'
一條指令如何變成位元
因為儲存程式的想法,一條指令本身不過就是記憶體裡的一個數字。ISA 釘死了指令格式——把一個固定寬度的字組切成數個欄位的規則手冊。在 RISC-V 裡,一條典型指令是 32 位元,分成幾個具名的格子。其中一個欄位,運算碼(opcode),說明這是哪一種操作。其他欄位以編號指出來源與目的暫存器。剩下一個欄位則可以放一個直接烤進指令裡的小常數,叫做立即運算元——剛剛那條 addi 裡的 10 就住在那裡。
為什麼要固定寬度、大小一致的欄位?為了解碼夠快。當每條指令都一樣長、暫存器欄位永遠坐在同樣的位元位置上,硬體甚至還沒搞清楚運算碼,就能開始讀取暫存器了。這份規律性,是結構刻意送給微結構的一份禮物。我們會在第 3 篇導覽裡,把這些格式一個位元一個位元拆開——並追蹤那個讓短立即值頂替成全寬度數字的「符號擴展」把戲。
搆到記憶體:定址與載入-儲存的想法
一條指令必須說出要讀寫記憶體裡的哪個位置,而 ISA 用來算出那個位置的方法菜單,就是它的一組定址模式。RISC-V 刻意把這份清單保持得很短。日常用的模式是「基底加位移」:取出坐在某個暫存器裡的位址,再加上一個小常數,剛好就是 0(x10) 的意思——「x10 裡的位址,加上位移 0」。分支則對程式計數器用類似的把戲——把一個位移加到我們現在的位置上,來決定要跳去哪裡。少數幾個簡單、可預測的模式,對硬體來說遠比一整座奇異模式的動物園更容易快速解碼。
這份克制,反映了一個決定性的設計選擇,叫做載入-儲存架構。規則很直白:算術指令只能對暫存器運算,絕不直接對記憶體運算。如果你想加一個住在記憶體裡的數字,就必須先把它載入暫存器、做運算、再把結果存回去——正是我們程式片段裡那三行的舞步。這道分隔讓常見的算術指令保持短而快,並把每一次緩慢的記憶體存取,都關進兩種標示清楚的指令種類裡。我們會在第 4 篇導覽裡,徹底探討定址模式與這套載入-儲存紀律。
ISA 對外呈現的記憶體模型
ISA 還替程式畫了一幅記憶體的圖,而這幅圖簡單得令人安心:一條又長又平的位元組陣列,每個位元組都有自己的數字位址,從零一路排到字組大小所允許的最大位址。一台 32 位元機器能命名 2^32 個不同的位元組位址(大約 4 GB);一台 64 位元機器能命名 2^64 個,是個幾乎無法想像的範圍。這條平坦、以位元組定址的陣列,就是每一條載入與儲存指令所對話的記憶體模型。程式被鼓勵去相信,這整條陣列只屬於它自己。
那份相信是個美麗的虛構。後面的階級會揭露,虛擬記憶體會悄悄把每個程式的私有位址,翻譯到真實、共享的晶片上——每個程式都以為自己擁有整棟房子,而一位翻譯員默默把它的房間對應到實際的建築。ISA 的記憶體模型是那個簡單的承諾;這個模型還得交代一些更棘手的事,例如一個多位元組數字的哪一個位元組排在前面(位元組順序),以及當另一顆核心寫入時這顆核心會看到什麼(一致性模型)。現在,先握住那幅乾淨的圖:一條可定址位元組的平坦陣列,由載入讀取、由儲存寫入。
RISC 對 CISC,誠實地說
你會聽到 ISA 被分成兩個陣營。像 RISC-V 這樣的 RISC(精簡指令集電腦),保有一小組簡單、定長、載入-儲存的指令——就是我們前面描述的那種精瘦、規律的風格。像 x86 這樣的 CISC(複雜指令集電腦),則長出了一大組強而有力、變長的指令,其中有些能一次做好幾件事,包括在單一條指令裡讀取記憶體、計算、再寫回去。舊版的故事是一場乾淨的對決:RISC 簡單又快,CISC 豐富但慢。真相要有趣得多。
現代 x86 晶片把這條界線模糊到幾乎消失。在內部,處理器並不直接執行它的複雜指令。一個解碼器把每條複雜指令拆成一小撮類似 RISC 的微指令(micro-ops),底下那台快速引擎跑的正是這些微指令——管線化、亂序執行,就跟一顆 RISC 核心一樣。所以表面上那份可見的 CISC 契約被遵守了,而隱藏的微結構本質上是 RISC。這場爭論早已不再是 RISC 對 CISC 的硬體之爭;它如今多半是關於指令流的編碼,而編碼仍會影響解碼有多容易、多省電。