大家好,我是微微笑的蝸牛,??。
在上兩篇文章中,我們實(shí)現(xiàn)了一個(gè)最小的虛擬機(jī)。如果沒看過的同學(xué),可以回過頭先去看看。
今天,繼續(xù)升級打怪,不過難度有所提高,將會模擬一個(gè)更加真實(shí)的環(huán)境,Little Computer-3 的實(shí)現(xiàn),簡稱 LC-3。
LC-3 是用于教學(xué)的匯編語言,它有著相比于 x86 更為簡潔的指令集,同時(shí)包含了主流 CPU 的經(jīng)典思想。有關(guān) LC-3 的介紹可點(diǎn)此查看。
LC-3 麻雀雖小,五臟俱全。不過它的規(guī)范不算太多,我們模擬它的實(shí)現(xiàn)還是比較合適的。
LC-3 基礎(chǔ)
首先,介紹一些 LC-3 的前置知識:
- 內(nèi)存地址空間 16 位,也就是最多可訪問
0x0000~0xffff的范圍。 - 通用寄存器 8 個(gè),16 位。編號從
000~111,R0 ~ R7。 - 三個(gè)標(biāo)志寄存器,N(Negative)、P(Postive)、Z(Zero),每個(gè) 1 位。
- PC 寄存器。
- 指令長度 16 位,操作碼 op 固定高 4 位。
- 采用大端字節(jié)序。
- 系統(tǒng)調(diào)用表。
- 中斷向量表。
- ...
另外,還有一些沒有列出來。但我們只打算實(shí)現(xiàn)它的一個(gè)子集,其他可以不用關(guān)注。
內(nèi)存空間
由于 LC-3 工作在 16 位下,內(nèi)存最大可訪問空間是 2^16-1。
這里我們?nèi)匀挥脭?shù)組來模擬,給它分配最大空間 UINT16_MAX 即可。
uint16_t mem[UINT16_MAX];
寄存器
這里我們定義 10 個(gè)寄存器,分別是:
- 8 個(gè)通用寄存器
- PC 寄存器
- COND 標(biāo)志寄存器,用于表示 N/P/Z 狀態(tài)
// 寄存器定義
typedef enum
{
R_R0,
R_R1,
R_R2,
R_R3,
R_R4,
R_R5,
R_R6,
R_R7,
R_PC,
R_COND,
R_COUNT
} Registers;
// 寄存器數(shù)組
uint16_t reg[R_COUNT];
R_COUNT 用于快速得到寄存器數(shù)量,reg[x] 可得到寄存器的值。
標(biāo)志寄存器
標(biāo)志寄存器用來標(biāo)識 N/P/Z 的狀態(tài)。取值定義如下:
// 標(biāo)志定義
typedef enum
{
FL_POS = 1 << 0, // 正數(shù)
FL_ZRO = 1 << 1, // 0
FL_NEG = 1 << 2 //負(fù)數(shù)
} ConditionFlags;
指令格式
指定包括操作碼和操作數(shù),有著如下標(biāo)準(zhǔn)格式:
- 指令長度為 16 位,高 4 位為操作碼,最多可表示 16 種指令。操作碼范圍
0000~1111。 - 低 12 位用于表示參數(shù)。不同的指令,有著不同的參數(shù),占用長度和布局也不一樣。
指令的大體布局如下:

注意:指令的參數(shù)中可能會包含寄存器、立即數(shù)等等。寄存器統(tǒng)一用 3 位來標(biāo)識,因?yàn)榭偣仓挥?8 個(gè)通用寄存器。
指令定義
這次,我們打算實(shí)現(xiàn)如下指令。
// 指令定義
typedef enum
{
OP_BR = 0, // 條件分支
OP_ADD = 1, // 加法
OP_LD = 2, // 取內(nèi)存地址的值到寄存器
OP_ST = 3, // 存寄存器的值到內(nèi)存地址
OP_JSR = 4, // 跳轉(zhuǎn)函數(shù)
OP_AND = 5, // 與運(yùn)算
OP_LDR = 6, // 取值到寄存器
OP_STR = 7, // 存儲寄存器的值
OP_RTI = 8, // unused
OP_NOT = 9, // 取反
OP_LDI = 10, // 間接取值
OP_STI = 11, // 間接存值
OP_JMP = 12, // 跳轉(zhuǎn)
OP_RES = 13, // reserved
OP_LEA = 14, // 加載地址到寄存器
OP_TRAP = 15, // 系統(tǒng)調(diào)用
} InstructionSet;
主要分為如下幾類:
- 運(yùn)算
- 數(shù)據(jù)存取
- 跳轉(zhuǎn)
- 系統(tǒng)調(diào)用
- 保留指令,暫時(shí)為使用,可用于占位
由于指令有點(diǎn)多,且實(shí)現(xiàn)更復(fù)雜。全部寫完文章會拖的太長,因此打算分為 3 篇來講。這篇主要介紹運(yùn)算、跳轉(zhuǎn)和保留指令,使用匯編的方式說明用法(?? 0101.. 看著頭大)。
指令實(shí)現(xiàn)
ADD
加法指令,操作碼為 0001,總共三個(gè)參數(shù)。指令執(zhí)行之后,會更新標(biāo)志寄存器的值。
它有兩種模式,不同的模式通過指令第 6 位標(biāo)志位來區(qū)分。主要區(qū)別在于第三個(gè)參數(shù)。
- 若標(biāo)志位為 1,則為立即數(shù)模式;
- 若標(biāo)志位為 0,則為寄存器模式。
以下代碼中用于取出標(biāo)志位,instr 表示指令。
// 取出 flag,在第 6 位。
uint16_t flag = (instr >> 5) & 0x1;
1. 立即數(shù)模式
ADD R0, R1, #5 → RO = R1 + 5
第三個(gè)參數(shù)為數(shù)字,可直接使用,所以稱之為立即數(shù)。
指令布局如下,標(biāo)志位為 1。

低 5 位為立即數(shù)。由于寄存器是 16 位,與立即數(shù)相加時(shí),需將立即數(shù)以有符號的方式擴(kuò)展到 16 位。
擴(kuò)展方式比較簡單,若最高位為 1,則全部填 1;若最高位為 0,則全部填 0。如下圖所示:

由于符號擴(kuò)展在很多指令中都會用到。因此,我們寫個(gè)通用的函數(shù),專用于處理符號擴(kuò)展。
// 符號擴(kuò)展
uint16_t sign_extend(uint16_t x, int bit_count)
{
// 最高位 1
if ((x >> (bit_count - 1)) & 0x1)
{
// 全部補(bǔ) 1
x |= 0xFFFF << bit_count;
}
return x;
}
搞清楚指令布局和符號擴(kuò)展后,實(shí)現(xiàn)就比較簡單了。
- 取出「目的寄存器 r0」,注意寄存器相關(guān)的值都是寄存器下標(biāo)。
- 取出「源寄存器 r1」。
- 取出「立即數(shù) imm」,并進(jìn)行符號擴(kuò)展。
- 相加求和,將結(jié)果放入「目的寄存器 r0」。
// 取出目的寄存器 r0,9~11,占 3 位,與上 111
uint16_t r0 = (instr >> 9) & 0x7;
// 源寄存器 r1,6~8 位
uint16_t r1 = (instr >> 6) & 0x7;
// 低五位,取出立即數(shù)。
uint16_t data = instr & 0x1F;
// 符號擴(kuò)展,若高位是 1,則全部補(bǔ) 1
uint16_t value = sign_extend(data, 5);
// 求和
reg[r0] = reg[r1] + value;
// 更新標(biāo)志寄存器
update_flags(r0);
最后,還剩一步,lc-3 規(guī)范中要求更新標(biāo)志寄存器。根據(jù)當(dāng)前操作寄存器的符號位,設(shè)定不同的狀態(tài)。如下所示:
// 更新標(biāo)志寄存器
void update_flags(uint16_t r)
{
uint16_t value = reg[r];
if (value == 0)
{
COND = FL_ZRO;
}
else if ((value >> 15) & 0x1)
{
// 最高位 1,負(fù)數(shù)
COND = FL_NEG;
}
else
{
COND = FL_POS;
}
}
這樣,立即數(shù)模式的實(shí)現(xiàn)就完成了。
下面再來舉個(gè)實(shí)際的 ??,說明下指令的布局。
假設(shè)我們想做這樣的操作:ADD R1,R2,#6,那么指令數(shù)據(jù)如下圖所示:

其中 操作碼=0001;第一個(gè)參數(shù)=001;第二個(gè)參數(shù)=010;第三個(gè)參數(shù)=00110,最終的十六進(jìn)制表示為:0x12A6。
2. 寄存器模式
ADD RO, R1, R2 → RO = R1 + R2
第三個(gè)參數(shù)為寄存器,需從寄存器中取值。
指令布局如下,標(biāo)志位為 0。

寄存器模式的實(shí)現(xiàn)更加簡單,取出「源寄存器 2」的值,與「源寄存器 1」相加,結(jié)果放入「目的寄存器」。
// 取出源寄存器 2,低 3 位
uint16_t r2 = instr & 0x7;
// 相加
reg[r0] = reg[r1] + reg[r2];
// 更新標(biāo)志寄存器
update_flags(r0);
寄存器模式下的指令,你也可以試著舉個(gè)實(shí)例栗子,動手寫寫看。
AND
與運(yùn)算,操作碼 0101。和 ADD 一樣,有兩種模式,指令格式一模一樣。把求和改成求與就好,就不重復(fù)了。
兩種模式的指令布局如下圖所示:

NOT
NOT R0, R1; R0 = ~R1
取反指令,操作碼 1001。取反之后同時(shí)更新標(biāo)志寄存器。
指令布局如下所示,低六位沒有使用,全是 1。

目的寄存器和源寄存器的取法跟 ADD 指令是一樣,代碼實(shí)現(xiàn)如下:
// 取出目的寄存器 r0,9~11,占 3 位,與上 111
uint16_t r0 = (instr >> 9) & 0x7;
// 源寄存器 r1,6~8 位,
uint16_t r1 = (instr >> 6) & 0x7;
reg[r0] = ~reg[r1];
// 更新標(biāo)志寄存器
update_flags(r0);
JMP
JMP R1
跳轉(zhuǎn)指令,操作碼 1100,只有一個(gè)寄存器參數(shù),在 6~8 位。作用是更新 PC 為寄存器的值。
指令布局如下:

代碼實(shí)現(xiàn)如下:
// 取出寄存器
uint16_t r1 = (instr >> 6) & 0x7;
// 更新 PC
PC = reg[r1];
RET
RET 并不在我們要實(shí)現(xiàn)的指令列表中。這里之所以單獨(dú)拎出來講,是因?yàn)?RET 是 JMP 指令的特例。
RET 是在函數(shù)調(diào)用完成后,用于恢復(fù) PC 寄存器的值。
只不過這時(shí)候寄存器固定是 R7,所以參數(shù)就為 111。如下圖所示:

也就相當(dāng)于:
RET = JMP R7
JSR
全稱為 Jump to Subroutine,也就是函數(shù)調(diào)用,操作碼 0100。
它有兩種模式,通過指令第 12 位 flag = instr[11] 來區(qū)分。
兩種模式的前置操作均為保存現(xiàn)場:將當(dāng)前 PC 值保存到 R7 中,目的是為了在函數(shù)調(diào)用完成之后恢復(fù)原 PC 值。
1. 標(biāo)簽?zāi)J?/strong>
JSR label
跳轉(zhuǎn)到 label 標(biāo)簽處的函數(shù)執(zhí)行,標(biāo)簽就是一個(gè)名稱。
注意,實(shí)際上 JSR 的參數(shù)是「相對于 PC 的偏移量」,是個(gè)數(shù)值,其中 PC 指向要執(zhí)行的下一條指令。因?yàn)樽罱K肯定是跳轉(zhuǎn)到某個(gè)地址。這里是匯編的形式說明用法。
另外,這里 PC 的含義跟前面文章中說的稍微有些區(qū)別,前面的 PC 指的是當(dāng)前執(zhí)行指令,這里指的是下一條指令。這對理解無大礙,只不過實(shí)現(xiàn)方式不一樣而已。
此時(shí),flag = 1。 指令布局如下:

0~10 位為相對于 PC 的偏移量,將其取出來進(jìn)行符號擴(kuò)展,然后與 PC 相加。
// long_pc_offset
uint16_t long_pc_offset = sign_extend(instr & 0x7ff, 11);
// 更新 pc
PC += long_pc_offset;
2. 寄存器模式
JSRR R1
跳轉(zhuǎn)到 R1 寄存器中的地址,寄存器在 6~8 位。
此時(shí),flag = 0。 指令布局如下:

取出寄存器,將 PC 更新為寄存器中的值即可。
// 取出寄存器
uint16_t r1 = (instr >> 6) & 0x7;
// 更新
PC = reg[r1];
BR
根據(jù)「條件標(biāo)志位」進(jìn)行跳轉(zhuǎn),操作碼是 0000。跳轉(zhuǎn)參數(shù)是相對于 PC 的偏移量,在 0~8 位。
指令布局如下:

condition_flag 中包含 n/z/p 的狀態(tài),只要任意一個(gè)狀態(tài)與當(dāng)前標(biāo)志寄存器相符,則進(jìn)行跳轉(zhuǎn)。
// 取出標(biāo)志參數(shù)
uint16_t cond_flag = (instr >> 9) & 0x7;
// 偏移量
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
// 傳入標(biāo)識是否與標(biāo)志寄存器的值相符,n,z,p
if (cond_flag & COND)
{
PC += pc_offset;
}
保留指令
RTI 和 RES 未使用,不處理即可。不過它們倒是可以用作占位指令,下面會講到。
手寫指令
實(shí)現(xiàn)了這么些指令,還得上手操練一下,檢查下正確性。可現(xiàn)在我們還沒有定義程序退出的指令,不然運(yùn)行虛擬機(jī)會進(jìn)入死循環(huán)。
LC-3 中的退出指令定義在 TRAP 系統(tǒng)調(diào)用中,后面的文章會專門講 TRAP 指令,操作碼是 1111。
那我們先簡單處理下,約定只要遇到操作碼是 1111 的指令,就認(rèn)為是程序退出,不用管后面的參數(shù)。所以暫且將退出指令定為 0xf000。
最后,我寫了些指令,不得不說真的很麻煩??。它的功能如下:
- 先執(zhí)行兩個(gè) ADD 指令。
- 執(zhí)行 JSR, 跳轉(zhuǎn)到第六條指令 RES。
- 執(zhí)行 RES 指令,繼續(xù)執(zhí)行,直至退出。
// 內(nèi)存區(qū)
uint16_t mem[UINT16_MAX] = {
// ADD R0,R1,R2
0x1042,
// ADD R1,R2,#6
0x12A6,
// JSR 2;
// 下一條是第 4 條指令,PC=3,跳轉(zhuǎn)到 PC=3+2,也就是第 6 條指令 RES。
0x4802,
// JMP R1;跳轉(zhuǎn)到第 6 條指令
0xC040,
// RTI;占位
0X8000,
// RES;占位
0XD000,
// NOT R3, R1
0x967F,
// trap-halt
0xf000,
};
為了方便調(diào)試,在每條指令執(zhí)行時(shí),打印了當(dāng)前操作碼。指令執(zhí)行完成后,最后再打印出了 r0~r3 寄存器的值。
執(zhí)行完上述代碼后,結(jié)果如下所示:
exe op:ADD
exe op:ADD
exe op:JSR
exe op:RES
exe op:NOT
exe op:TRAP
r0,0
r1,6
r3,65529
另外,你也可以添加自己的指令試試,絕對值得體驗(yàn)一下效率暴跌的感覺??。