如果您將自己限制在只有 16 條指令,您應該選擇哪些指令,如果沒有那些被擱置的指令,您將如何管理?
在我之前關于從頭開始構建 4 位 HRRG(Heath Robinson,Rube Goldberg)計算機項目的專欄中,我們介紹了 CPU 寄存器和指令集。您可能還記得,由于我們只有 4 位數據總線(以及 12 位地址總線),我們選擇只有 2^4 = 16 條指令以及 2^4 = 16 個 CPU 寄存器。
為了確保我們都跟著同一個鼓點跳舞,讓我們提醒自己,六個通用寄存器 R0 到 R5 用于存儲數據值并“累積”來自任何算術或邏輯運算的結果。狀態寄存器 S0 和 S1 主要用于存儲任何算術或邏輯運算的狀態結果,例如減法的結果是否為零。
另請參閱此索引,其中列出了構成我們的 4 位 HRRG 計算機項目的所有文章,以及一些有趣的相關專欄。
程序計數器 (PC) 用于跟蹤 CPU 在程序中的當前位置。堆棧指針 (SP) 用于跟蹤堆棧的頂部。索引寄存器 (IX) 主要用于保存計數值或訪問內存的偏移量。中斷向量 (IV) 用于保存稱為中斷服務路由 (ISR) 的特殊類型子程序的內存地址。
介紹堆棧指針
我們將在以后的專欄中考慮所有這些小寄存器流氓如何在令人難以忍受的細節中發揮它們的魔力,但如果這對您來說是新的,那么簡要描述 SP 的操作可能是個好主意。
我們大多數人都去過自助餐廳,那里有一堆盤子堆疊在一個彈簧機構的頂部。假設您是負責將板裝入機械裝置的人員。讓我們還假設板的編號為(1、2、3……),并且——作為一個強迫癥工程師——這是將前三個板加載到機制中的順序,如下圖所示:
現在假設一個顧客進來拿盤子。當然,它們會檢索您添加到堆棧頂部的最后一個盤子(在我們的示例中為 3 號)。在計算方面,這種形式的存儲和檢索將被歸類為后進先出 (LIFO)過程。
好吧,我們的 SP 以類似的方式工作。在我們的程序開始時,我們將使用我們不用于其他任何內容的內存區域中某個位置的地址加載 SP。隨后,我們每次執行 PUSH 操作時,CPU 都會將指定的數據寫入 SP 當前指向的內存位置(“棧頂”),然后將 SP 加一以指向下一個空閑位置。相比之下,每次執行 POP 操作時,CPU 都會先遞減 SP 以指向堆棧頂部的數據,然后從堆棧中讀取該數據并將其存儲到我們告訴它的任何位置。
介紹 6502
出于以下討論的目的,我們將使用MOS 技術 6502來提供比較的基礎。1975 年推出的 6502 有一個 8 位數據總線和一個 16 位地址總線,其寄存器包括一個 8 位累加器寄存器(A)、兩個 8 位索引寄存器(X 和 Y)、一個 7-位處理器狀態標志寄存器 (P)、一個 8 位堆棧指針 (S) 和一個 16 位程序計數器 (PC)。
與 HRRG 不同,我們可以將 12 位 SP 加載為我們想要的任何值,6502 的 8 位 SP 在上電時自動加載 $00(請記住,我們使用“$”字符表示十六進制值),并且堆棧的起始地址被硬連線到 $0100。這意味著 6502 的堆棧地址空間被限制在 256 個地址 $0100 到 $01FF 之間。
盡管與今天的微處理器產品相比,6502 看起來很簡單,但它在其誕生之初就被認為是相當了不起的,尤其是因為它的可承受的價格標簽(1975 年為 25 美元)。許多人繼續基于這款處理器創造出令人驚嘆的項目,例如這款基于 6502 的虛擬現實 (VR) 系統。6502 的新版本不斷出現,例如MOnSter 6502 CPU。
此外,與 HRRG 不同的是,我們可以將 12 位中斷向量 (IV) 加載為我們想要的任何值,6502 被硬連線以查看內存地址 $FFFE 和 $FFFF 以檢索其 16 位中斷向量,其中這個 2 字節的值將由用戶加載到內存中(當我們說“由用戶”時,我們真正的意思是“由用戶的程序”)。
在可用的 2^8 = 256 個可能的操作碼(指令)中,最初的 6502 使用 151 個組織成 56 條指令(取決于指令),一種或多種尋址模式。根據指令和尋址模式,6502 操作碼可能需要 0、1 或 2 個額外字節用于操作數;因此 6502 條機器指令的長度從 1 個字節到 3 個字節不等。
MOV(加載和存儲)
6502 允許用戶將值從內存加載到累加器(A)和索引寄存器(X 和 Y)。同樣,它允許用戶將這些寄存器中的值存儲到內存中。所有這些都需要六個指令,如下所示:
LDA(加載累加器)
LDX(加載 X 寄存器)
LDY(加載 Y 寄存器)
STA(存儲累加器)
STX(存儲 X 寄存器)
STY(存儲 Y 寄存器)
相比之下,HRRG 有一條 MOV 指令,可用于根據其操作數將數據從寄存器到寄存器、寄存器到內存、內存到寄存器和內存到內存移動(復制)。此外,這些指令適用于所有 HRRG 的寄存器(即使這樣做沒有意義 - 更多內容見下文)。
INC(遞增)和 DEC(遞減)
6502 允許用戶遞增(加 1)和遞減(減 1)指定內存位置或其索引寄存器(X 和 Y)中的值。為了做到這一點,它需要以下六個指令:
INC(增加內存位置的內容)
INX(增加 X 寄存器的內容)
INY(增加 Y 寄存器的內容)
DEC(減少內存位置的內容)
DEX(減少 X 寄存器的內容)
DEY(減少 Y 寄存器的內容)
“增加或減少累加器的內容呢?” 我聽到你哭了。好吧,為了使用 6502 實現這一點,您必須執行本專欄后面討論的常規加法或減法運算。
相比之下,HRRG 的 INC 和 DEC 指令可用于遞增內存位置以及任何 CPU 的 4 位和 12 位寄存器的內容。
“什么?任何寄存器——甚至是程序計數器?” 我聽到你緊張地尖叫。是的,您可以在任何寄存器上使用這些指令,即使這樣做似乎沒有意義。例如,增加程序計數器 (PC) 通常被認為是一件壞事,但 HRRG 允許在機器代碼和底層硬件中這樣做。
我們可能會在匯編程序中標記某些“愚蠢”(我們將在以后的專欄中討論),但如果用戶決定忽略并繞過匯編程序發出的任何警告和/或錯誤消息,那么就這樣吧,因為 (a)在沒有無數例外和特殊情況的情況下設計硬件更容易工作,(b)用戶可能會想出我們沒有想到的狡猾的使用模型,以及(c)我們不是“明智的警察”(除了別的,我沒有合適的褲子)。
ADDC 和 SUBB(加法和減法)
當我們在簡單計算機的情況下考慮加法時,我們通常會想到將兩個數字相加,例如 3 + 2 = 5。問題是我們可以表示的數字的大小是受限于我們的數據總線和數據字段的寬度。例如,在 HRRG 的情況下,單個 4 位 nybble 可用于表示 0 到 15 范圍內的無符號數或 -8 到 +7 范圍內的有符號數。
這顯然有點限制。幸運的是,我們可以使用多個 nybbles 來表示我們的值。例如,在 HRRG 的情況下,一對 4 位 nybbles 可用于表示 0 到 255 范圍內的無符號數或 -128 到 +127 范圍內的有符號數。
假設我們想將兩個 2-nybble 值相加。在這種情況下,我們將從添加兩個最不重要的 nybbles (LSN) 開始。根據它們的值,這將導致 0 或 1 值存儲在進位 (C) 狀態標志中。當我們添加下一對 nybbles 時,我們還需要包含(添加)進位標志的內容。
一些早期的 8 位處理器提供了兩個加法指令,例如 ADD(“無進位相加”)和 ADDC(“有進位相加”)。其他的,比如6502,只提供了“帶進位相加”的版本,由用戶自己實現“不帶進位相加”,先將進位標志加載0,再進行相加。
同樣的事情也適用于減法。在這種情況下,一些早期的 8 位處理器提供了兩個減法指令,例如 SUB(“無借位減法”)和 SUBB(“有借位減法”)。其他的,比如6502,只提供了“借位減法”的版本,由用戶來實現“不借位減法”,首先將進位標志加載1,然后執行減法。
“等一下,我們沒有借用狀態標志,”我聽到你在嗚咽。這是真的,但在減法的情況下,進位 (C) 標志承擔借位 (B) 標志的角色。基于唯一的物理標志是進位標志,一些設計師更喜歡說“有/沒有進位減法”,并使用類似 SUBC 助記符的東西,但是,在我看來,這最終會導致更多的混亂而不是它的價值.
底線是 6502 提供了如下兩條指令:
ADC(進位加法)
SBC(進位減法)
此外,這些指令只允許您將指定內存位置的內容添加/減去累加器的內容,結果存儲在累加器中。
同樣,HRRH 提供如下兩條指令:
ADDC(進位加法)
SUBB(借位減法)
然而,這些指令允許執行寄存器到寄存器、寄存器到存儲器、存儲器到寄存器以及存儲器到存儲器的加法和減法。(在我的下一篇專欄中,我們將考慮使用 ADDC 和 SUBB 指令來實現其對應的 ADD 和 SUB 的各種方式。)
ROLC 和 RORC(旋轉和移位)
可能有八種基本的旋轉和移位操作,我們可以為其分配助記符,如下所示:
ROL(左移)
ROR(右移)
ROLC(通過進位標志左移)
RORC(通過進位標志右移)
LSHL(邏輯左移)
ASHL(算術左移)
LSHR(邏輯右移)
ASHR(算術移位正確的)
請記住,不同 CPUS 的設計者對這類事情使用各種不同的助記符;我在上面展示的那些對我來說最有意義。現在,如果我們決定(我們沒有)在我們的 4 位 HRRG 中實現所有這八個指令,它們的操作的圖形表示將如下所示:
在 ROL(左移)的情況下,所有位都左移一位;此外,概念上“落下”的最高有效位 (MSB) 被復制到最低有效位 (LSB) 和進位標志中。相比之下,在 ROR(左移)的情況下,所有位都右移一位;此外,概念上“落下”的 LSB 被復制到 MSB 和進位標志中。
ROLC(通過進位向左旋轉)與ROL非常相似,只是進位標志的原始內容被復制到LSB中。類似地,RORC(通過進位向右旋轉)與ROR非常相似,只是進位標志的原始內容被復制到MSB中。
LSHL(邏輯左移)操作與ROL(向左旋轉)和ROLC(通過catty向左旋轉)操作非常相似,只是0被復制到LSB中。類似地,LSHR(邏輯右移)操作與ROR(向右旋轉)和RORC(通過進位向右旋轉)操作非常相似,只是0被復制到MSB中。
ASHL(算術左移)操作在功能上與LSHL(邏輯左移)相同-兩者都會導致將0復制到LSB中-因此沒有設計人員會費心將其作為CPU中的單獨指令來實現。另一方面,在編寫程序時,我們可能更喜歡使用這兩種不同的助記符作為注釋,以提醒自己(和其他讀者)當我們捕獲代碼時的想法。
最后,ASHR(算術右移)與LSHR(邏輯右移)類似,只是MSB(符號位)被復制回自身(另請參閱“C/C++>移位運算符的工作原理”)。
在HRRG的情況下,被限制為16條指令,我們決定只實現八個基本旋轉和移位中的兩個:
ROLC(通過進位標志向左旋轉)
RORC(通過進位標志向右旋轉)
我們選擇這兩條指令的原因是,很容易將它們作為實現其他指令功能的基礎。(我們將在下一篇專欄文章中考慮使用ROLC和RORC指令實現其ROL、ROR、LSHL、LSHR、ASHL和ASHR對應項的各種方式。)
AND、OR、XOR 和 CMP(邏輯運算)
這些指令的工作方式與地球上幾乎任何其他處理器上的對應指令類似,因此我們不會在此花太多時間討論它們。可以這么說,6502 的 AND(邏輯與)、EOR(異或)和 ORA(包括或)僅允許您使用內存中保存的另一個值對累加器的內容執行操作,結果存儲在蓄能器。相比之下,HRRG 的 AND、OR 和 XOR 等效項支持寄存器到寄存器、寄存器到內存和內存到寄存器操作。
在 HRRG 的 CMP(比較指令)的情況下,它還支持寄存器到寄存器、寄存器到內存、內存到寄存器和內存到內存操作,被比較的兩個值被視為是無符號二進制值。
CLR 和 SET(位操作操作)
一些處理器提供一套指令,可用于清除或設置狀態寄存器中的各個位。例如,6502 支持七種這樣的指令:
CLC(清除進位標志)
CLD(清除十進制模式標志)
CLI(清除中斷禁止標志)
CLV(清除溢出標志)
SEC(設置進位標志)
SED(設置十進制模式標志)
SET(設置中斷禁止標志)
HRRG 沒有提供任何這些說明,但如果提供了,他們的基因組學將如下(我們將看到我的瘋狂是有原因的):
CLRN(清除負標志)
CLRZ(清除零標志)
CLRC(清除進位標志)
CLRO(清除溢出標志)
CLRI(清除中斷屏蔽標志)
SETN(設置負標志)
SETZ(設置零標志)
SETC(設置進位標志)
SETO(設置溢出標志)
SETI(設置中斷屏蔽標志)
SETH(設置停止標志)
觀察沒有 CLRH(清除停止標志)。這是因為一旦暫停標志設置為 1,重置它的唯一方法是觸發中斷(假設中斷屏蔽標志設置為 1)或重置機器。
關鍵是我們可以使用 AND 和 OR 邏輯運算來實現所有這些指令。假設我們想將進位標志(狀態寄存器 S0 中的第 2 位)清除為 0,我們可以通過將 S1 的內容與 %1011 進行“與”運算來做到這一點(請記住,我們使用 '%' 字符來表示二進制值)。類似地,如果我們想將進位標志設置為 1,我們可以通過將狀態寄存器 S0 的內容與 %0100 進行或運算來實現。
說了這么多,如果我們在編寫匯編代碼時可以使用位操作指令,那就太好了,所以我們將在下一篇專欄中討論如何使用我們的匯編程序將它們添加到我們的指令庫中。
推入和彈出(或拉出)
這些指令用于將值壓入堆棧并再次彈出(或拉出)它們。對于 6502,有 6 條與堆棧相關的指令(請記住,正如我們之前討論的),6502 的 8 位堆棧指針本身在上電時會自動加載 00 美元。
TSX(將堆棧指針的值傳送到變址寄存器 X)
TXS(將變址寄存器 X 的內容傳送到堆棧指針)
PHA(將累加器的內容壓入堆棧)
PHP(將處理器狀態寄存器的內容壓入)
PLA(將棧頂的值拉入累加器)
PLP(將棧頂的值拉入處理器狀態寄存器)
對于 HRRG,我們只有兩條指令:
PUSH(將選定的寄存器或內存位置的內容壓入堆棧)
POP(將堆棧頂部的值彈出到選定的寄存器或內存位置)
HRRG 的指令適用于任何 CPU 的寄存器或內存位置。此外,HRRG 的 MOV 指令提供(并超過)6502 的 TSX 和 TXS 指令的功能。
JMP、JSR 和相關指令
JMP(無條件跳轉)指令允許 CPU 跳轉到程序的另一部分。JSR 指令告訴 CPU 跳轉到子程序。JSR 通常的工作方式是用戶將任何相關信息推送到堆棧上,然后調用 JSR。反過來,CPU 將程序計數器 (PC) 中的返回地址壓入堆棧,然后跳轉到子程序。
仍然在談論這通常的工作方式,在子程序結束時,使用 RTS(從子程序返回)指令將返回地址從堆棧頂部彈出到程序計數器(PC)中,并將我們返回到主程序程序。
還值得注意的是,中斷服務程序 (ISR) 的行為有點像子程序,因為中斷會導致 CPU 在服務中斷之前將返回地址壓入堆棧頂部。在 ISR 結束時,使用 RTI(從中斷返回)指令將返回地址從堆棧頂部彈出并返回到主程序。
6502 擁有所有這四個指令:
JMP(無條件跳轉)
JSR(跳轉到子程序)
RTS(從子程序返回)
RTI(從中斷返回)
處理器還將支持一組指令,這些指令將根據狀態標志的狀態觸發跳轉(或分支)。例如,6502 提供了 8 個這樣的指令,如下所示:
BCC(進位標志清除分支)
BCS(進位標志設置分支)
BEQ(零標志設置分支)
BMI(負標志設置分支)
BNE(零標志清除分支)
BPL(負標志清除分支)
BVC(溢出標志清除時分支)
BVS(溢出標志設置時分支)
與 6502 的 JMP 和 JSR 指令允許 CPU 跳轉到其 16 位地址空間內的任何位置不同,這些分支指令使用帶符號的 8 位相對地址將控制轉移到位于向前(之后)和 128 字節內的目標向后(之前)分支指令的字節。程序往往會進行大量跳轉——例如循環循環——因此在時鐘有限的日子里,使用 1 字節的分支地址而不是 2 字節的跳轉地址可能會顯著節省空間和時間速度、處理器周期和內存位置。
在 HRRG 的情況下,我們只有兩條與跳轉相關的指令:
JMP(無條件跳轉)
JSR(跳轉到子程序)
我們沒有 RTS 或 RTI 指令——我們通過簡單地從堆棧頂部檢索返回地址并使用 POP 指令將其加載到程序計數器 (PC) 中來實現相同的效果。
問題是我們實現 JMP 指令的方式意味著我們可以使用它來實現與使用以下指令套件相同的效果:
JMP(無條件跳轉)
JMPN(無條件跳轉,或“從不跳轉”)*
JPN(如果為負則跳轉;如果 N 標志為 1)
JPNN(如果非負跳轉;如果 N 標志為 0)
JPZ(如果為零則跳轉;如果Z 標志為 1)
JPNZ(如果不為零則跳轉;如果 Z 標志為 0)
JPC(如果進位則跳轉;如果 C 標志為 1)
JPNC(如果不進位則跳轉;如果 C 標志為 0)
JPO(如果溢出則跳轉;如果O 標志為 1)
JPNO(如果沒有溢出則跳轉;如果 O 標志為 0)
JPI(如果中斷屏蔽跳轉;如果 I 標志為 1)**
JPNI(如果沒有中斷屏蔽跳轉;如果 I 標志為 0)**
JPH(如果停止則跳轉;如果 H 標志為 1)***
JPNH(如果不停止則跳轉;如果 H 標志為 0)**
注意 * JMPN(“從不跳轉”)可用于調試目的。
注意 ** 基于 I 標志為 0 或 1 或 H 標志為 0 的狀態的跳轉并不是特別有用,因為程序員已經知道這些標志包含什么(不像 N、Z、C 和 O標志,其值由算術和邏輯運算的結果確定)。然而,它們是通過實現 HRRG 的 JMP 指令的方式實現的。
注意 *** JPH(如果 H 標志為 1 則跳轉)完全沒有意義,這是因為一旦程序將此標志設置為 1,CPU 就會停止操作,并且只能通過觸發中斷來重置標志(假設中斷屏蔽標志設置為 1) 或通過重置機器,所以這里包含這條指令只是為了完整起見。
在大多數處理器的情況下,JSR(跳轉到子程序)指令的行為方式類似于 JMP(無條件跳轉)指令。也就是說,沒有任何與 JPN、JPNN 等等效的 JSR。但是,由于 HRRG 的指令架構,我們可以使用 JSR 來實現與使用以下指令相同的效果:
JSR(無條件 JSR)
JSRN(無條件 JSR)*
JSN(如果為負,則為 JSR;如果 N 標志為 1)
JSNN(如果非負,則為 JSR;如果 N 標志為 0)
JSZ(如果為零,則為 JSR;如果 Z 標志為 1)
JSNZ(如果不為零,則 JSR;如果 Z 標志為 0)
JSC(如果進位,則 JSR;如果 C 標志為 1)
JSNC(如果不進位,則 JSR;如果 C 標志為 0)
JSO(如果溢出,則 JSR;如果 O 標志為 1)
JSNO(如果沒有溢出,則 JSR;如果 O 標志為 0)
JSI(如果中斷掩碼,則 JSR;如果 I 標志為 1)**
JSNI(如果不是中斷掩碼,則 JSR;如果 I 標志為 0)**
JSH(如果暫停,則 JSR;如果H 標志為 1)***
JSNH(如果不停止,則 JSR;如果 H 標志為 0)**
注意 *、** 和 ***;與上面討論的各種跳轉指令相同的警告適用。
即將推出……
呸!這比我預期的要花費更多的工作,但我希望它能提供一些見解,讓我們了解我們選擇指令形成我們的極簡集的方式、我們的指令與其他機器的比較,以及小騙子的使用方式。
在我的下一個 HRRG 專欄中,我們將開始研究 HRRG 的匯編語言,包括我們如何使用匯編器來實現不在我們核心集中的指令。此外,我們將討論與所有這一切的歷史方面有關的一些方面,例如第一個匯編程序是如何產生的。在此之前,我一如既往地期待您的評論、問題和建議。
審核編輯 黃昊宇
評論
查看更多