寫在前面的話
如果您對該系列感興趣的話,推薦您先看一下南京大學的計算機組成原理實驗(也就是PA)的講義,然後再來看這篇文章可能有更多地收穫。如果您是要完成該作業的學生,我推薦你先看講義,或者好好聽老師的講課,然後自己獨立完成這個作業,但是如果你沒有聽懂,或者你無論如何也無法理解講義上面的字,又或者說對講義上面的某點知識某個問題不瞭解而又覺得太簡單不好意思問老師,那麼您可能會從這篇文章裏面獲得一些你需要的信息。本篇文章將會包括筆者自己做PA的所有經過,希望你並不將該文章當成抄襲的根源,而是成爲你思考的源泉。
現在已經到了PA2的階段,這才真正開始了組成原理的道路,在PA0是搭建環境,PA1複習複習C語言,寫寫功能函數,而現在纔開始真正去做計算機結構內部的東西,難度也上了一個檔次。
PA系列傳送門
PA0:https://blog.csdn.net/qq_41983842/article/details/88921427
PA1.1:https://blog.csdn.net/qq_41983842/article/details/88934779
PA1.2:https://blog.csdn.net/qq_41983842/article/details/89714479
PA1.3:https://blog.csdn.net/qq_41983842/article/details/89714689
PA2.1:https://blog.csdn.net/qq_41983842/article/details/95232055
PA2.2&2.3:https://blog.csdn.net/qq_41983842/article/details/101164495
PA3.1:https://blog.csdn.net/qq_41983842/article/details/103094859
PA3.2:https://blog.csdn.net/qq_41983842/article/details/103843093
PA4:https://blog.csdn.net/qq_41983842/article/details/104667951
文章目錄
思考題
-
設某指令執行前
eip
值爲x1
,該指令執行後eip
值爲x2
,那麼x2 - x1
的這個差值都包括了一條指令的哪些組成成分?opcode、操作數的地址、存儲地址、指令地址,指令類型不同包括不同的成分。
-
opcode_table
數組中存放了所有指令的信息,請問表中每個表項是什麼類型?NEMU
又是如何通過這個表項得知操作數長度、應該使用哪個譯碼函數、哪個執行函數等信息的?opcode_entry
類型,NEMU
通過set_width
知道操作數長度,通過make_DHelper
函數知道要用哪個譯碼函數,通過make_EHelper
函數知道要執行函數信息。 -
操作數結構體/共同體中都包括哪些成員,分別存儲什麼信息?他們是如何實現協同工作的?
typedef struct { uint32_t type; //操作數類型 int width; //操作數寬度 union { uint32_t reg; //寄存器操作數 rtlreg_t addr; //操作數的地址 uint32_t imm; //立即操作數 int32_t simm; //無符號立即操作數 }; rtlreg_t val;//RTL寄存器 char str[OP_STR_SIZE];//操作數指令 } Operand;
-
復現宏定義
-
make_EHelper(mov) //mov 指令的執行函數
#define make_EHelper(name) void concat(exec_, name) (vaddr_t *eip)
->
#define concat(x, y) concat_temp(x, y)
->
#define concat_temp(x, y) x ## y
把x和y這兩個參數粘在一起變成
xy
,所以在宏定義中將exec_
和mov
粘在一起,執行mov
操作 -
make_EHelper(push) //push 指令的執行函數
#define make_EHelper(name) void concat(exec_, name) (vaddr_t *eip)
->
#define concat(x, y) concat_temp(x, y)
->
#define concat_temp(x, y) x ## y
把x和y這兩個參數粘在一起變成
xy
,所以在宏定義中將exec_
和push
粘在一起,執行push
操作 -
make_DHelper(I2r) //I2r 類型操作數的譯碼函數
#define make_DHelper(name) void concat(decode_, name) (vaddr_t *eip)
與上一個一樣,把
decode_
和I2r
連接起來,執行immediate to register
譯碼操作 -
IDEX(I2a, cmp) //cmp 指令的 opcode_table 表項
#define IDEXW(id, ex, w) {concat(decode_, id), concat(exec_, ex), w}
->
id
變量表示譯碼函數參數名稱,傳給譯碼函數,ex
變量表示執行函數參數名稱,傳給執行函數,w表示width
,處理的數據的寬度。 -
EX(nop) //nop 指令的 opcode_table 表項
#define EX(ex) EXW(ex, 0)
->
ex
變量表示執行函數參數名稱,傳給執行函數,EX(nop)
=EXW(nop, 0)
-
make_rtl_arith_logic(and) //and 運算的 RTL 指令
#define make_rtl_arith_logic(name) \ static inline void concat(rtl_, name) (rtlreg_t* dest, const rtlreg_t* src1, const rtlreg_t* src2) { \ *dest = concat(c_, name) (*src1, *src2); \ } \ static inline void concat3(rtl_, name, i) (rtlreg_t* dest, const rtlreg_t* src1, int imm) { \ *dest = concat(c_, name) (*src1, imm); \ }
這個我在實現RTL指令的時候詳細說明了。
-
-
立即數背後的故事
由於大端小端兩種方式數據的字節保存的地址位置相反,需要注意大端小端兩種保存數據模式的不同,先判斷機子是大端還是小端存儲方式,如果是大端,就傳到一個將小端存儲方式轉換成大端存儲方式的函數裏面就行了。
-
神奇的 eflags
當運算的結果超過字長所能表示的範圍時,產生溢出,CF當然不能代替OF,對於運算的數來說,只要符合進位的情況,CF就置1,怎麼可能所有的進位情況都會超過字長所表示的範圍呢?在運算的過程中,當兩個符號相同的數相加,結果的符號與之相反,則OF=1,否則OF=0. 或者當兩個符號不同的數相減,結果的符號與減數相同,則OF=1,否則OF=0.
實驗內容
實現所有 RTL 指令
一開始打開rtl.h
文件發現裏面這麼多TODO()
可給我嚇壞了,之後好好看了PPT和講義才找清楚他們都是幹什麼的,然後纔好開始寫,一些rtl
指令很簡單,完全就是跟着註釋走,基本上註釋已經把答案寫出來了。
在開始寫所有的指令之前,有一段代碼引起了我的注意
這是幹啥的啊,看起來定義了好多的運算,但是定義的又不是那麼清楚直接,我想了半天才明白了,重點在他定義的make_rtl_arith_logic
裏面,這個函數有一個參數名字叫name
,下面緊接着一個concat
函數,作用應該是將rtl_
和name
連接起來,就構成了rtl_name
這樣一個東西,然後呢,在這個函數裏面就把這個東西當成c_name
,找到上面的宏定義來實現相應的功能,第35到44行,一共定義了這麼多運算,這可就省了好多事了,在接下來完善rtl
指令的時候就可以直接調用這些定義好的運算了。於是就開始實現相關指令
rtl_mv
函數,就是簡單賦值:
rtl_not
函數:
rtl_eq0
函數:
rtl_eqi
函數:
rtl_neq0
函數:
rtl_msb
函數:請注意,這個函數的寫法是錯誤的!將在PA2.2中解決
然後這些最簡單的操作寫完了,就可以寫其他的比較複雜的操作了。
rtl_push
函數:
rtl_pop
函數:
rtl_sext
函數,這個函數寫的時候是費了我好大的勁,想了很多種情況,然後問了助教之後,不管src
是多少位,通通擴展到32位,這樣以來就簡單一些,可以通過與一個數相或或者左移右移的方法來實現符號擴展,這裏我選擇的是左移右移的方式。因爲寄存器都是無符號整型,所以左移右移沒辦法移入1,所以要設置一個帶符號整數,通過帶符號整數來左移右移,最後賦給dest
。
make_rtl_setget_eflags
,在寫這裏的時候遇到了一些困難,這個函數沒有註釋提示,然後我只知道和寄存器有關,然後就無從下手,把這個任務拖了拖,結果後面一看講義,要我自己實現eflags
結構體,然後就轉去先做任務3再回來做這個,其實就是簡單的賦值操作。
rtl_update_ZF
函數,簡單的判斷之後就傳到rtl_set_ZF
函數裏面設置給ZF
相應的值。請注意,這個函數的寫法是錯誤的!將在PA2.2中發現並解決
rtl_update_SF
函數,找到符號位,傳到rtl_set_SF
函數裏面設置給SF
相應的值。請注意,這個函數的寫法是錯誤的!將在PA2.2中發現並解決
實現 eflags 寄存器
查詢了位域的相關概念,把講義搬過來
31 23 15 7 0
+-------------------+-------------------+-------+-+-+-+-+-+-+-------+-+-+
| |O| |I| |S|Z| | |C|
| X | |X| |X| | | X |1| |
| |F| |F| |F|F| | |F|
+-------------------+-------------------+-------+-+-+-+-+-+-+-------+-+-+
類似於這樣子(從百度百科上面抄了個例子)
struct bs {
unsigned a:4;
unsigned :0 ;/*空域*/
char b:4 ;/*從下一單元開始存放*/
unsigned c:4;
}data;
//a 爲unsigned,空域爲unsigned
那這樣以來豈不是就很好實現,對照講義可以看出來,這些符號位肯定都是無符號整型,這幾個符號位各佔一位,其他的都是空域。由於下面有初值問題,所以還要定義一個變量存放初值。由於struct
裏面是位域,所以只能在struct
外面再搞一個struct
,然後設置一個變量來存放值。
struct {
struct {
uint32_t CF:1;//CF佔一位
unsigned : 5;//之後是5位空域
uint32_t ZF:1;//ZF佔一位
uint32_t SF:1;//SF佔一位
unsigned : 1;//1位空域
uint32_t IF:1;//IF佔一位
unsigned : 1;//1位空域
uint32_t OF:1;//OF佔一位
unsigned : 20;//20位空域
};
uint32_t value;//賦初值要用
}eflags;
然後要設置eflags
的初值,在手冊第10章,一下子就找到了
要設置2爲初值,在monitor.c
中的restart
函數裏面添加
從任務6回來,發現自己的sub
指令實現的有問題,找了半天的錯誤,發現邏輯上面沒啥錯誤,然後想到sub
指令跟寄存器結構體有關,不會是我結構體實現的有問題把…就反過來檢查自己的結構體,思考了一下好像是有點問題的,寄存器的位域是結構體,但是eflags
寄存器的初值應該是大家共用的而不是單單隻有一個纔對,並且最開始union
申請空間直接要申請32位的初值空間,這樣纔可以在這個空間裏面定義位域。所以把外面哪個struct
改成了union
實現 6 條基本指令
先把call
指令實現:
/* 0xe8 */ IDEXW(J,call,0), EMPTY, EMPTY, EMPTY,
然後找到decode.c
文件中的make_DHelper(J)
函數,根據i386
手冊裏面第275頁,找到call
指令的相關僞代碼並在make_EHelper(call)
函數裏面寫出來相關操作:
實現完call
指令以後興致勃勃運行dummy
單步執行一下,結果發現了這個情況
找到Assertion '0' failed
這個問題所在,發現是make_DoHelper(SI)
這個函數沒有實現,然後頓時就懵了,不是說好填完表然後寫好執行函數就行了嘛,怎麼還要寫這個譯碼函數??然後我在講義裏面找到了這樣一句話:
make_Dophelper(name)
:名爲 decode_op_name
的操作數譯碼函數的原型說明,那這個函數應該是和帶符號立即數的進一步譯碼有關係,雖然還是不太懂其中的原理,但是根據註釋還有前面的make_DoHelper(I)
可以很快寫出來相應的功能,照貓畫虎嘛。這裏實現的也有問題呀!!! 將在PA2.2中發現並解決
之後再嘗試運行dummy
,成功實現。
現在來實現push
指令:
接着上一步繼續si
告訴我0x55
沒有實現,就查表找到0x55
位置,然後查閱i386
手冊
從50到57都是跟push
相關的,只要實現32位的就行了,找到make_DHelper(r)
,那麼開始填表,雖然要運行我們這個程序的話填0x55
就行了,但是爲了以後着想,從0x50
到0x57
都要填。
/* 0x50 */ IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4),
/* 0x54 */ IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4),
找到data-mov.c
文件中的make_EHelper(push)
函數,開始實現。
make_EHelper(push) {
rtl_push(&t0);//把寄存器壓棧
if (id_dest->type == OP_TYPE_REG) { rtl_sr(id_dest->reg, id_dest->width, &t0); }//如果目的操作數是寄存器操作數,寫入寄存器
else if (id_dest->type == OP_TYPE_MEM) { rtl_sm(&id_dest->addr, id_dest->width, &t0); }//如果是在內存裏面,寫入內存
else { assert(0); } //不可能出現其他情況了。
print_asm_template1(push);
}
從第6步回來,怎麼想感覺自己都寫的沒錯啊…問題出在哪兒了呢?於是用printf
一行一行打印來確定問題所在之處,最後確定到rtl_sm
這個函數,寫入寄存器的操作出了問題,我不是push ebp
麼?爲啥寫入寄存器的值出現問題了啊!返回來從頭查看程序代碼,發先我的第一行rtl_push(&t0);//把寄存器壓棧
這是啥?我爲什麼要把一個啥也不是的temp
壓棧?我說爲什麼cpu.ebp
一直是0,然後馬上更改,給t0
賦上了寄存器的值,然後再入棧,最後再寫入操作數,就OK了。這裏的push仍然有一步實現有問題!!!將在PA2.2中發現並解決
si
測試實現push
功能
但是由上圖可知在0x83
這個地址處他又不知道幹啥了,於是查看反彙編發現是sub
指令,接下來就實現sub
指令
在i386
中找到0x83
位置,發現要找grp1
,在grp1
中找到sub
,在第6個位置
回到我們的表裏,找到make_group(gp1,
找到第六個EMPTY
,改一下,無譯碼函數,直接執行,v表示字長不定,所以直接用EX
在arith.c
裏面找到執行函數,根據手冊上面寫的僞代碼開始實現,一開始完全照着手冊上面來實現,寫個if寫個else,本來覺得沒啥問題,結果一跑就出錯了,我想想這邏輯也沒問題啊,怎麼就跑不對了呢?然後就往下翻,還是在這個文件裏面,我看到了有個sbb
執行的執行函數,哎呦,這個跟sub
好像哎,於是我就繼續查手冊,找到了這個指令的相關內容:
這跟sub
不就差一個CF
的問題麼,有了sbb
那麼sub
也好實現了,對照着他就可以開始寫了:
寫完sub
指令發現之前自己的push
第3、4、5行完全可以直接調用operand_write
函數…我竟然沒注意到這個函數!
成功實現
繼續si
直到報錯
發現0x31
這個地址處的指令沒有實現,查看反彙編以後是xor
指令,現在就去實現它:
查手冊,從0x30
開始到0x35
都是要實現異或的,0x30
、0x31
是E->G
,反過來對應譯碼函數要找G2E
,0x32
、0x33
正好相反要找E2G
,0x34
要找I2a
,0x35
要找I2r
返回到表中開始填寫相關位置
在logic.c
文件中開始寫執行函數
還是查手冊獲得xor
指令怎麼寫,CF和OF位都是0,目標操作數等於src
和dest
的異或
成功實現
再運行一步又出現問題了!真是問題不斷,這次查閱了反彙編,發現是pop
指令沒有實現
i386
手冊裏面,pop
就在push
的旁邊,0x58
到0x5F
回到我們的opcode
表,其實跟push
一模一樣,譯碼函數也是r
就連執行函數也和push
的邏輯一模一樣,就反過來,一個壓棧,一個出棧就行了。
成功實現
繼續si
,這回是0xc3
地址處的指令沒有實現,還是老慣例,查看反彙編,發現是最後一個ret
指令,現在開始實現吧:
老樣子查i386
手冊
找到opcode
中0xc3
的位置,因爲是空白,所以沒有譯碼函數,直接執行。
/* 0xc0 */ IDEXW(gp2_Ib2E, gp2, 1), IDEX(gp2_Ib2E, gp2), EMPTY, EX(ret),
找到之前寫call
指令的那個文件,開始寫make_EHelper(ret)
函數
回想理論課中ret
指令做了些什麼?將eip
退棧就行了,並且參照call
指令,還要設置跳轉標誌。
成功實現
對已實現指令增加標誌寄存器行爲
好像在實現第2個任務的時候,對照着i386
手冊就直接把這些指令的符號位設置實現了,當時沒看到第四個任務…
call
、push
、ret
、pop
指令不改變任何標誌位
sub
、xor
指令改變除了IF
的其他標誌位
這些都在任務2實現了
運行第一個客戶程序
由於上面幾個任務的完成,指令的實現,現在可以成功運行這個程序
不要忘記在all-instr.h
裏面聲明所有的指令執行函數!
實現 differential testing
在common.h
中把#define DIFF_TEST
的註釋取消,講義用了很長的篇幅來講解這個東西,但是實現的話卻只要一個if
語句
成功發現自己編寫失敗的指令
哭着回到第二步檢查自己的sub
指令的問題…
從eflags
寄存器回來,再次測試
這回sub
沒問題了,push
又出現了問題,然後回到第二步,之後就可以成功運行了。
利用 differential testing 檢查已實現指令
在經歷了任務6以後,成功實現對了所有的指令
PA2.1的內容到此結束,感謝您的耐心閱讀,文中加黑加粗的寫錯的地方將會在後來的PA完成過程中更改。