JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

指令、暫存器,與程式計數器

打開契約,看看它會動的零件:一小撮程式親手點名的暫存器、那唯一永遠知道下一步該跑什麼的指標,以及那個一次一個數字、把程式取出、解碼、執行的迴圈。

暫存器:你把手邊工作擱著的那張桌子

第 1 篇導覽替契約裡的零件取了名;現在我們把它們拿起來翻面細看。先從工作空間開始。記憶體很龐大,但很遠——每一次存取都是走一趟去圖書館的遠路。於是 ISA 給程式一小撮、親密、快速、可以直接點名的格子:架構暫存器。RISC-V 交給你 32 個,每個存著一個機器字組,叫做 x0 到 x31。它們就是你的桌面:一個讓你把正在動手處理的那幾個值擱在手邊的地方,這樣你就很少需要走到書架那邊去。

為什麼這麼少?速度的代價就是稀缺。一個暫存器必須在單一個快速的滴答內就搆得到,而它們的數目越多,每條指令光是要點名其中一個就得花掉越多位元,儲存本身也跟著變慢。於是 ISA 定下一個小小的、具名的俱樂部——一座 暫存器檔,硬體能在一個週期內從中讀出兩個值、寫入一個值。注意「架構」這個詞:這些是契約以名字承諾的暫存器。底下的晶片或許偷偷多擁有了好幾十個(我們會在很後面的階級遇到暫存器重新命名),但一個程式永遠只能談論這 32 個。

程式計數器:按在書頁上的那根手指

在所有暫存器之中,有一個做的事是別人都做不來的:程式計數器,也就是 PC。因為基礎那一級提到的儲存程式想法,一個程式的指令本身不過就是坐在記憶體裡、一個接一個的數字。總得有東西記住我們此刻在那份清單裡的哪一行。那個東西就是 PC——一個存著下一條即將執行的指令位址的暫存器。把它想成按在你正在朗讀的書的某一行上的一根手指:它標出確切的位置,而在手指指到它之前,什麼都不會被讀出來。

預設情況下,手指每一步往前滑一條指令的寬度——在 RISC-V 裡是 4 個位元組,因為指令長 4 個位元組——於是執行就順著書頁一路往下流。控制流指令全部目的,就是把那根手指挪到下一行以外的地方。一條條件分支會測試某件事——這個暫存器是零嗎?這一個比那一個小嗎?——如果測試通過,就把 PC 設成另一個位址,也就是分支目標。一個跳躍則無條件地挪動它。迴圈、if 判斷與函式呼叫,在底下,全都不過是把程式計數器推來推去的有紀律的手法。

取指-解碼-執行的迴圈

暫存器與 PC 在一個不眠不休的迴圈裡活了過來——那就是指令週期,每一顆處理器的心跳。它簡單得驚人,而機器除此之外什麼都不做,每秒做上數十億次,只要它開著就一直做。迴圈的每一圈,讀出 PC 所指的那條指令、弄清楚它是什麼意思、把它執行掉,然後推進 PC,好讓下一圈讀到下一條指令。同樣這四個動作,一遍又一遍,就足以跑完所有寫過的程式。

  1. 取指:讀出存放在程式計數器所指位址上的那個指令字組,把它從記憶體複製進處理器裡。
  2. 解碼:把指令拆成它的各個欄位——哪一種操作、哪幾個暫存器、哪個立即值——好讓控制邏輯知道該擺出什麼陣勢。
  3. 執行:做真正的工作——在 ALU 裡做加法、算出一個記憶體位址,或測試一個分支條件。
  4. 寫回與推進:把任何結果存進目的暫存器,再更新 PC——若是下一條指令就加 4,若控制流有要求就設成分支目標。

一段小程式,用手追蹤一遍

讓我們親眼看看暫存器與 PC 真的動起來。這是把前 n 個數字加總的內層迴圈:在一個暫存器裡累計總和、在另一個暫存器裡倒數,再分支回去直到計數器歸零。別把它當成文字讀,而要當成一台運轉中的小機器——每一行就是迴圈的一圈,PC 每次往下滑 4,只有那條分支會把它彈回頂端。

# sum = 0; while (i != 0) { sum += i; i -= 1; }
# x5 = sum (total), x6 = i (counter). Assume x6 already holds n.

  addi  x5, x0, 0      # x5 <- 0            sum starts at zero (x0 is the zero reg)
loop:                                       # PC value of this line is the branch target
  add   x5, x5, x6     # x5 <- x5 + x6      add current i into the total
  addi  x6, x6, -1     # x6 <- x6 - 1       count down by one
  bne   x6, x0, loop   # if x6 != 0, PC <- loop      ...else fall through (PC += 4)

# Trace with n = 3:  add->5, dec->2, branch taken;  add->7? no: x5=3 then 5 then 6...
# i=3: sum 0->3   i=2: sum 3->5   i=1: sum 5->6   i=0: branch NOT taken, loop ends.
三個暫存器加上 PC,就是故事的全部。算術都待在暫存器裡;bne 操控程式計數器;x0 供應一個免費的零。完全沒有碰到記憶體。

注意這裡沒有的東西:一條載入或儲存都沒有。因為 RISC-V 是一個載入-儲存架構,像 add、addi 這樣的算術指令只能對暫存器做計算。一旦某個值改住在記憶體裡,我們在做任何運算之前,就得用一條載入把它取進暫存器,再用一條儲存把結果放回去——這兩種是唯一被允許跨過鴻溝去搆記憶體的指令。正是這套紀律,讓上面那個內層迴圈這麼快:它從不離開桌面跑去圖書館。

ISA 藏起了什麼,又絕不能藏什麼

暫存器、程式計數器與週期,是契約讓人看得見的那一部分機器——程式被允許點名與推理的狀態。其餘的一切都是刻意藏起來的。一個程式看不見快取、看不見管線有多深,也看不見此刻有多少條指令偷偷在飛;它只看得見暫存器在變、記憶體在變,而且是按契約承諾的順序。正是那份隱藏起來的自由,讓一份 ISA 能活得比許多晶片都久:底下的組織——也就是怎麼做——被一再重新發明,而看得見的狀態——也就是做什麼——則原地不動。

但有一條線晶片絕不能越過,不管它在底下多麼聰明地重疊與重排工作。一個程式所觀察到的結果——它的暫存器與記憶體裡的最終值——必須恰好等於把每條指令一次一條、嚴格按 PC 順序執行所會產生的結果。一條管線可以同時耍弄五條指令,一台亂序引擎可以先動手做剛好備齊材料的那一條,就像廚房先煮材料最早到齊的那張單子;但兩者仍然按原本的順序提交結果,照點餐的次序一道一道上菜。契約保證了那份循序的錯覺。硬體要如何一邊跑得快、一邊守住這個承諾,正是接下來幾級的全部戲劇。