exe_mem_reg
本模塊完成EXE和MEM兩個階段之間的信號流水。本模塊的時序圖如下。
圖 22 exe_mem_reg時序圖
mem_stage
本模塊完成對數(shù)據(jù)Cache的讀寫。模塊的對外接口符合Wishbone總線標準。本模塊的主要時序如下圖。
圖 23 mem_stage時序圖
mem_wb_reg
本模塊完成MEM和WB兩個階段之間的信號流水。本模塊的時序圖如下。
圖 24 mem_wb_reg時序圖
wb_stage
本模塊完成寫回指令的寄存器堆修改操作。本模塊的時序圖如下。
圖 25 wb_stage時序圖
except
本模塊完成流水線中的中斷及異常處理。為了完成精確中斷,即產(chǎn)生異常的指令前已經(jīng)在流水線中的指令完成執(zhí)行,而在異常指令后的指令不允許完成執(zhí)行(不修改CPU狀態(tài)),才能響應異常。因此,在實現(xiàn)精確中斷時,需要對流水線中的指令進行跟蹤,所有的異?;蛑袛嘈盘枌⒀舆t到流水線的特定階段(Writeback)進行響應,并且對不同類型的異常信號,中斷程序的返回地址不同。本模塊的主要時序圖如下。
圖 26 except時序圖
4.1.2.2Cache模塊詳細設計方案
功能描述
本模塊實現(xiàn)指令Cache和數(shù)據(jù)Cache。其中,指令Cache和數(shù)據(jù)Cache的映射策略都采用直接映射方式。指令Cache只讀,數(shù)據(jù)Cache的寫策略為寫通過(主存和Cache里的數(shù)據(jù)時鐘保持一致)。
- 子模塊列表
|
|
|
Instruction Cache top |
|
Data Cache top |
詳細設計
ic_top
本模塊的時序圖如下。
圖 27 ic_top時序圖
4.1.2.3動態(tài)翻譯硬件模塊詳細設計方案
功能描述
為了提高動態(tài)翻譯效率,我們在CPU中增加了硬件加速模塊。動態(tài)翻譯硬件加速包括以下部分:
在QS-I CPU的ALU模塊中增加x86 flag寄存器(MIPS架構中沒有flag標志寄存器),軟件可通過mtc0,mfc0兩條指令來訪問flag寄存器。這樣x86的算術邏輯或比較指令(如add, sub, cmp等),以及條件跳轉指令(如ja, jb, jg等)有效地得到了硬件支持,使得軟件的翻譯效率大大提高。
在QS-I CPU外增加了JLUT(Jump address Lookup Table),即跳轉地址查表。通過CAM(Content Address Memory)的硬件支持,跳轉指令的翻譯效率將比完全基于軟件的翻譯方式提高一個數(shù)量級。在QS-I中將新增4條用戶指令campi, ramri, camwi, camwi用于軟件對JLUT的訪問。
- 子模塊列表
|
|
|
|
|
JLUT top |
|
|
SPC is stored in CAMs, and it will take less than two clock cycles to get address of the CAMs content specified. |
|
|
TPC is stored in ubiquitous RAMs. |
詳細設計
如下方的JLUT詳細設計圖所示,JLUT模塊與QS-I CPU之間通過campi, camwi, ramwi, ramwi四條指令進行交互。
campi用于CAM的查表,camwi用于CAM的寫操作,ramwi用于RAM的寫操作,RAMRI用于RAM的讀操作。
4條指令的格式如下。
Instruction |
Format |
Usage |
campi |
opcode, rs, 5’h0, rd, 5’h0, func |
campi rd, rs |
camwi |
opcode, rs, rt, 5’h0, 5’h0, func |
camwi rt, rs |
ramwi |
opcode, rs, rt, 5’h0, 5’h0, func |
ramwi rt, rs |
ramri |
opcode, rs, 5’h0, rd, 5’h0, func |
ramri rd, rs |
圖 28 JLUT詳細設計圖
4.2.動態(tài)翻譯詳細設計方案
二進制翻譯技術能夠把一種體系結構的二進制程序翻譯成另一種體系結構的二進制程序,以在新的體系結構下運行。二進制翻譯主要有三類:解釋執(zhí)行、靜態(tài)翻譯及動態(tài)翻譯。
在系統(tǒng)總體框架圖中,二進制翻譯層可運行不同的翻譯程序,以在不同的體系之間進行轉換,如x86到MIPS、ARM到MIPS、x86到ARM等。本部分挑選了8086到MIPS的動態(tài)翻譯作為實現(xiàn)原型。
4.2.1.二進制翻譯介紹
二進制翻譯可以分為三大類:解釋執(zhí)行、靜態(tài)翻譯和動態(tài)翻譯。
解釋執(zhí)行的流程是:取指、解析、執(zhí)行。它對源機器代碼進行實時解釋并執(zhí)行,然后繼續(xù)下一條指令。系統(tǒng)不對已解釋的指令進行保存或緩存。在這個框架下,不能對代碼進行優(yōu)化。這種翻譯技術能取得高度兼容性,但執(zhí)行效率很低。
靜態(tài)翻譯是先將源可執(zhí)行文件轉換成目標機器可執(zhí)行文件,然后運行在目標機器上。這是離線翻譯,因此有充足的時間對代碼進行優(yōu)化,以提高代碼的執(zhí)行效率。但靜態(tài)翻譯很難做到正確性,如代碼的自修改問題,執(zhí)行過程中有些跳轉值只能在運行時才能獲知等問題。
解釋執(zhí)行是實時翻譯,靜態(tài)翻譯是離線翻譯,動態(tài)翻譯就像是兩者的折中。它不像解釋執(zhí)行那樣對每條指令進行翻譯并馬上執(zhí)行,也不像靜態(tài)翻譯那樣將指令完全翻譯好之后才執(zhí)行。它每次對一個基本塊進行翻譯并執(zhí)行,然后取另一個塊。一個基本塊一般包含多條算術類型指令,最后是一條控制流(Control Flow)類型指令。已翻譯的塊可進行緩存或保存。動態(tài)翻譯只對將要執(zhí)行的代碼進行翻譯,且能很好地解決代碼自修改問題。
4.2.2.二進制翻譯策略選擇
本項目采取的是軟硬協(xié)同動態(tài)翻譯策略,將源二進制代碼進行翻譯,當遇到控制流類型指令,如跳轉指令,系統(tǒng)調用等,翻譯過程掛起,將已翻譯的指令序列作為一個基本塊,然后運行基本塊。當基本塊執(zhí)行完以后,會跳到下一處執(zhí)行。若下一處已翻譯過,則繼續(xù)執(zhí)行,否則暫停執(zhí)行以進行翻譯,如此過程循環(huán)。完整的流程如下圖所示。
圖 29 x86程序翻譯執(zhí)行流程
基本塊執(zhí)行時有硬件模塊輔助,如圖 12所示。硬件模塊管理跳轉緩存,緩存的基本項為對。程序執(zhí)行到跳轉指令時,程序向跳轉緩存發(fā)送SPC,得到相應的TPC,再跳至TPC繼續(xù)執(zhí)行生成塊。簡單的示例如圖 30所示。源程序從塊A開始執(zhí)行,到末尾時,需要跳轉到塊C。翻譯后執(zhí)行,執(zhí)行完塊A’后將要跳轉,此時的跳轉地址是SMEM中地址,即SPC,要轉換成相應的TPC,該TPC就由跳轉緩存中尋找。
圖 30 SMEM與TMEM的映射
4.2.3.8086程序的載入
首先,由系統(tǒng)向服務器發(fā)送命令,命令格式為x86 *.com,它包含在自定義傳輸協(xié)議中,類型碼為86,要求.com文件僅使用一個段,大小限制為64KB。服務器找到所指定的文件,并將其傳送給系統(tǒng),系統(tǒng)將其存放在內存中。至此,完成8086可執(zhí)行程序的載入。
4.2.4.標志寄存器處理
8086中有個標志寄存器FLAGS,而MIPS中沒有與之相對應的標志寄存器,解決辦法有二,軟件模擬實現(xiàn)或硬件提供支持。
軟件模擬指的是,當一條8086指令執(zhí)行后,會影響哪些標志位,然后用軟件方法將其模擬出來,使兩者的結果一致。如執(zhí)行add ax, bx對溢出位的影響。模擬時,將ax移到MIPS的$t0寄存器的低16位,將bx移到MIPS的$t1寄存器的低16位,然后對$t0和$t1做加法,結果放到$t0,相對應的指令為add $t0, $t0, $t1。結果是否溢出則要查看$t0的第16位。最后,還要將溢出位存放至標志寄存器的對應位。這中間還要涉及移位運算、位運算等,所需代價很大,但有個好處是無需對硬件平臺做改動,使硬件平臺更為純粹。
若采用提供硬件支持,那么硬件平臺需稍做修改,增加一個類FLAGS寄存器。仍以上面的add ax, bx為例。將ax、bx分別放到$t0、$t1的高16位,然后進行相加,是否溢出的結果會自動保存到新添加的類FLAGS寄存器里,因而軟件層面無需再做處理。此種做法,增加了硬件工作,但大大簡化了軟件的操作。8086的FLAGS有多個標志位,若要完全實現(xiàn),那么對本身的硬件平臺改動會比較大,因此,我們只選擇了其中幾個進行實現(xiàn),如Z、O、C、S等。
4.2.5.寄存器映射
MIPS有32個通用寄存器,從0號到31號,每個寄存器為32位。8086的通用寄存器有8個:AX、CX、DX、BX、SP、BP、SI和DI。這8個通用寄存器都是16位,AX、CX、DX和BX還可以分成兩個8位寄存器,高8位和低8位,如AX可分為AH和AL。此外,段寄存器有CS、DS、ES和SS,都是16位。還有IP寄存器和FLAGS寄存器,也都是16位。
因為MIPS的寄存器數(shù)量比8086的寄存器多,可以采用直接映射,一個8086寄存器對應一個MIPS寄存器,而不需要對寄存器進行置換,簡化了寄存器的管理。[!--empirenews.page--]
Table 1 寄存器映射
8086 |
MIPS |
AX |
s0(R16) |
CX |
s1(R17) |
DX |
s2(R18) |
BX |
s3(R19) |
SP |
s4(R20) |
BP |
s5(R21) |
SI |
s6(R22) |
DI |
s7(R23) |
CS |
t4(R12) |
DS |
t5(R13) |
ES |
t6(R14) |
SS |
t7(R15) |
IP |
t8(R24) |
除了這些寄存器外,還給8086提供了3個輔助寄存器,t0、t1和t2。t0一般作為參數(shù)寄存器或返回值寄存器,t1和t2一般作為臨時寄存器。這幾個寄存器的存在有很大的必要性。
4.2.6.上下文切換
從翻譯流程可以看出,整個過程有兩個運行環(huán)境。翻譯過程是在MIPS環(huán)境中,執(zhí)行則是在MIPS的8086虛擬環(huán)境中。但硬件只有MIPS的一套寄存器,8086的寄存器是映射到MIPS的寄存器。在切換運行環(huán)境時,要先保存當前寄存器組,再載入新的寄存器組。方式可以有兩種。一種是將寄存器組保存至堆棧,另一種是將寄存器組保存至內存。這里將其保存至內存,因為這更利于調試,可將寄存器值調出來查看。
上下文切換實現(xiàn):
/**
* context switch
* @param type - 0:從x86切換到mips,other:從mips切換到x86
*/
void _contextSwitch(int type) {
if (type == _MIPS_TYPE) {
_saveRegisters(_X86_TYPE);
_loadRegisters(_MIPS_TYPE);
} else {
_saveRegisters(_MIPS_TYPE);
_loadRegisters(_X86_TYPE);
}
}
上下文切換的流程及實現(xiàn)如下。
圖 34 寄存器組切換
4.2.7.字節(jié)順序與邊界對齊問題
字節(jié)序(Byte Order)一般有兩種:大端序(big-endian)和小端序(little-endian)。大端序是將最高有效字節(jié)(MSB,Most Significant Byte)存放至低地址,小端序則是將最低有效字節(jié)(LSB,Least Significant Byte)存放至低地址。MIPS采用的是大端序,而8086使用的是小端序。因此,在二進制翻譯中,必須要處理這種差異。
8086在訪問內存時,如果是字節(jié)操作,那么翻譯時,可以使用對應的MIPS字節(jié)操作指令,如lb,sb等。字節(jié)的操作不會有字節(jié)序問題,處理多字節(jié)時會有字節(jié)序問題。8086訪問多字節(jié)時,不能使用相應的MIPS指令,而應拆分讀取,最后再拼湊。過程如圖 35所示。
圖 35 字節(jié)序問題解決
4.2.8.堆棧處理
MIPS有一個堆棧寄存器$sp,8086則是使用一個堆棧段寄存器SS與一個堆棧指針寄存器SP,實際地址為段基址加偏移地址。本設計MIPS和8086各自有自己的堆??臻g。8086堆棧操作對象有寄存器、立即數(shù)、內存等。這里以寄存器壓棧為例,出棧情況類似。壓棧時,首先要計算出絕對地址,然后將寄存器保存。計算地址方法:(SS << 4) + IP,是一個20位地址,尋址1M。push AX時,是對一個16位寄存器壓棧,由上面的字節(jié)序分析可知,要拆分成兩個字節(jié)壓棧。這里為了簡便,直接對32位MIPS寄存器進行壓棧,因為8086寄存器只占用MIPS寄存器的高16位。這里,絕對地址是放在輔助寄存器里的,因此要對該輔助寄存器進行保存,以防止要壓棧的寄存器就是該輔助寄存器而造成壓棧失敗。我們是將該輔助寄存器保存在MIPS的堆棧中。
4.2.9.操作數(shù)分析
8086的操作數(shù)類型比較多樣,有寄存器、立即數(shù)、內存等。每個類型的位寬還可以變化,有8位、16位之分。MIPS比較規(guī)則,寄存器為32位,立即數(shù)為16位。兩者之間的轉換是翻譯的重要方面。
寄存器如果是16位,則可以直接映射到對應MIPS寄存器的高16位。如果是8位寄存器,不管是高8位還是低8位,一般都要移出至輔助寄存器的高8位。移到高8位的原因是為了運算時能產(chǎn)生正確的標志位。運算完后再將運算結果搬回8086寄存器。
立即數(shù)有3種情況:8位,16位及由8位符號擴展來的16位立即數(shù)。8位及8位符號擴展可直接以字節(jié)訪問內存得到,16位立即數(shù)則要注意字節(jié)序問題,上面已分析過。
內存方面涉及到內存地址及所指向的內存內容。內存地址的拼接會在下文詳細分析,8086有多種段寄存器與偏移的組合方式。取內存內容時也會遇到字節(jié)序的問題。
操作數(shù)類型示意圖如下。
圖 36 操作數(shù)類型
4.2.10.二進制翻譯及代碼生成
這部分主要解析8086指令,將其拆分,并生成相應的MIPS代碼。在具體解析指令之前,先研究下8086二進制機器碼的特點,抽出公共特征。以下幾張表均來自于[6]。
Table 2 oo修飾位說明
oo |
功能 |
00 |
如果mmm=110,那么位移量在操作碼后面,否則沒有使用位移量 |
01 |
操作碼后面是8位有符號的位移量 |
10 |
操作碼后面是16位有符號的位移量 |
11 |
mmm指定一個寄存器而不是一種尋址方式 |
Table 3 16位寄存器/存儲器(mmm)字段描述
mmm |
16位寄存器 |
000 |
DS:[BX+SI] |
001 |
DS:[BX+DI] |
010 |
SS:[BP+SI] |
011 |
SS:[BP+DI] |
100 |
DS:[SI] |
101 |
DS:[DI] |
110 |
SS:[BP] |
111 |
DS:[BX] |
Table 4寄存器字段(rrr)的分配
rrr |
w=0 |
w=1 |
000 |
AL |
AX |
001 |
CL |
CX |
010 |
DL |
DX |
011 |
BL |
BX |
100 |
AH |
SP |
101 |
CH |
BP |
110 |
DH |
SI |
111 |
BH |
DI |
Table 5 對段寄存器的寄存器字段(rrr)的分配
rrr |
段寄存器 |
000 |
ES |
001 |
CS |
010 |
SS |
011 |
DS |
對oo字段的解析如下。當oo為00的時,mmm索引存儲器有點變化。當mmm為110b時,并不是按照表3來索引SS:[BP],而是DS+16位偏移量,其余情況則是按照表3,偏移量為0。當oo為01時,偏移量為8位有符號數(shù)。當oo為10時,偏移量為16位有符號數(shù)。根據(jù)oo和mmm來計算內存地址的過程如下。
圖 37 計算內存地址
rrr字段的實現(xiàn)。當w為1時,rrr對應的是16位寄存器,可直接映射到MIPS寄存器的索引值。當w為0時,rrr對應的是8位寄存器,需要對其重新標號,將AL作為0開始標號。代碼實現(xiàn)中,寄存器對應的宏如下,其中包含了段寄存器。
#define _X86_AL 0
#define _X86_CL 1
#define _X86_DL 2
#define _X86_BL 3
#define _X86_AH 4
#define _X86_CH 5
#define _X86_DH 6
#define _X86_BH 7
#define _X86_AX _MIPS_S0 // 0 16
#define _X86_CX _MIPS_S1 // 1 17
#define _X86_DX _MIPS_S2 // 2 18
#define _X86_BX _MIPS_S3 // 3 19
#define _X86_SP _MIPS_S4 // 4 20
#define _X86_BP _MIPS_S5 // 5 21
#define _X86_SI _MIPS_S6 // 6 22
#define _X86_DI _MIPS_S7 // 7 23
#define _X86_CS _MIPS_T4 // 8 12
#define _X86_DS _MIPS_T5 // 9 13
#define _X86_ES _MIPS_T6 // 10 14
#define _X86_SS _MIPS_T7 // 11 15
#define _X86_IP _MIPS_T8 // 12 24
8086二進制機器碼的主要字段已解析,還有個別字段。
Table 6 其他字段
字段 |
說明 |
d |
運算方向 |
w |
是否為字 |
s |
是否符號擴展 |
disp |
位移量 |
data |
數(shù)據(jù),如立即數(shù) |
翻譯時將二進制機器碼拆分成各個字段,然后根據(jù)其語義生成對應的MIPS指令。一條8086指令一般要翻譯成多條MIPS指令,其中一條為核心指令,其余為輔助指令。如加法指令中,將內容移出至輔助寄存器都是輔助指令,最終的加法是核心指令。一般,核心指令對應MIPS的一條指令,因此,可將其抽象出來,實現(xiàn)函數(shù)如下。
/**
* x86運算與mips對應關系
*/
int _keyGenerate(int type, int rt, int rd, int **target) {
int instruction;
switch (type) {
case _X86_ADD0:
instruction = _gen_add(rd, rt, rd);
break;
case _X86_AND0:
instruction = _gen_and(rd, rt, rd);
break;
case _X86_OR0:
instruction = _gen_or(rd, rt, rd);
break;
case _X86_SUB0:
instruction = _gen_sub(rd, rt, rd);
break;
case _X86_XOR0:
instruction = _gen_xor(rd, rt, rd);
break;
case _X86_MOV3:
instruction = _gen_add(rt, _MIPS_ZERO, rd);
break;
case _X86_CMP0:
instruction = _gen_sub(rd, rt, _MIPS_ZERO);
break;
default:
break;
}
*(*target)++ = instruction;
return instruction;
}